Pins the model each subagent type runs on, set in /config or with /agent-models

Function-hook mods for Claude Code. Each folder is one plugin.
| Mod | What it does | ||
|---|---|---|---|
context-watch | One-line gauge above the prompt for context fill, cost and hot rate limits; toasts at 60/80/90%; compaction keeps your instructions and edited files; /ctx opens a context breakdown pane | ||
live-spinner | Spinner shows what is running: the Bash description or a trimmed command, the ssh host, file being read or edited, search, subagent, MCP tool; a tool's own elapsed time after 15s; "Waiting for signing approval" when a signed git commit or tag sits quiet | ||
pr-watch | Tracks PRs from gh pr / gt submit / git push output, shows checks above the prompt, toasts on green/red/merged, blocks foreground CI polling loops; /prs | ||
house-style | No em dashes: system prompt rule, reminder to the model after a write that adds one, toast when a reply uses one | ||
handoff | /carryover [focus] writes a state note from the conversation and clears; the next prompt carries it. /carryover save, show, drop | ||
agent-jobs | /jobs opens a pane of running subagents and background shells with elapsed time and a Stop button; toasts when background work finishes | ||
zsh-safe | Quotes glob flags zsh would expand (--include=*.ts, find -name *.ts) and keeps a grep that finds nothing from failing a cd ... && grep chain | ||
pm-guard | Blocks npm/npx/yarn where the lockfile says bun or pnpm, and steers to bun where there is no lockfile | ||
remote-guard | Asks before a destructive command (restart, rm, kill, reboot, pct/qm stop, zfs destroy) runs over ssh | ||
xcode-status | Only in a folder with an .xcodeproj/.xcworkspace: one line with the last build or test result (flags runs that executed 0 tests) and the booted simulator; blocks a second xcodebuild while one is running; re-checks on /cd and keeps each project's last result | ||
agent-models | Pins the model each subagent type runs on (Explore, general-purpose, Plan, any other, plus custom type=model pairs), set in /config or with /agent-models <type> <model>; a model the caller names wins unless "Override explicit models" is on | ||
worktrees | /worktrees pane: each git worktree with branch, uncommitted changes, ahead/behind and PR state; Remove (after asking) on clean worktrees whose PR is merged or closed, plus "Remove N merged" to clear every merged one at once | ||
pr-rules | Blocks gh pr create / gt submit when the repo keeps an [Unreleased] changelog and the branch adds no entry (skip with --label no-changelog); where the repo's CLAUDE.md says to label every PR (or requireLabels is on), blocks a PR with no --label and lists the repo's labels. Repos with neither are untouched; `/pr-rules on | off | auto` overrides per repo (kept across sessions) |
claude plugin marketplace add narrowstacks/claude-code-mods
claude plugin install context-watch@claude-code-mods
Or load from a checkout without installing:
claude --plugin-dir ./context-watch --plugin-dir ./live-spinner
claude plugin validate ./context-watch
claude plugin test ./context-watch
tsc -p ./context-watch works once Claude Code has loaded the mod and written .claude-plugin/types/.
hooks/register.ts 97 lines1import { atom, update, read } from 'claude-code'
2import type { PluginOptions, Register } from 'claude-code'
3
4import type { Spawn } from '../types'
5
6const recent = atom({ plugin: 'agent-models', key: 'recent' } as const, [] as Spawn[])
7
8const INHERIT = 'inherit'
9const MODELS = ['haiku', 'sonnet', 'opus', 'fable']
10// Subagent types with their own row in /config, and that row's field.
11const FIELDS: Record<string, string> = { Explore: 'explore', 'general-purpose': 'generalPurpose', Plan: 'plan' }
12
13export const parseCustom = (text: string): Record<string, string> =>
14 Object.fromEntries(
15 text
16 .split(',')
17 .map(pair => pair.split('=').map(part => part.trim()))
18 .filter((pair): pair is [string, string] => pair.length === 2 && pair[0] !== '' && pair[1] !== ''),
19 )
20
21const formatCustom = (rules: Record<string, string>) =>
22 Object.entries(rules).map(([type, model]) => `${type}=${model}`).join(', ')
23
24// The pinned model for a subagent type, or null to leave it alone.
25export const pinFor = (type: string, options: PluginOptions): string | null => {
26 const str = (key: string) => (typeof options[key] === 'string' ? (options[key] as string) : INHERIT)
27 const custom = parseCustom(str('custom') === INHERIT ? '' : str('custom'))
28 const field = FIELDS[type]
29 const model = custom[type] ?? (field !== undefined ? str(field) : str('other'))
30
31 return model === INHERIT || model === '' ? null : model
32}
33
34export const register: Register = (on, options) => {
35 on('session.start', async ($, e, next) => {
36 await $.command.register({
37 name: 'agent-models',
38 description: 'Show subagent model pins, or set one: /agent-models Explore haiku',
39 })
40
41 return next(e)
42 })
43
44 on('agent.spawn', async ($, e, next) => {
45 if (e.fork) {
46 return next(e)
47 }
48
49 const pinned = pinFor(e.subagentType, options)
50 const keepCaller = e.model !== undefined && options.force !== true
51 const model = pinned === null || keepCaller ? e.model : pinned
52 const source: Spawn['source'] = model === undefined ? 'inherited' : model === e.model ? 'caller' : 'pinned'
53 await update($, recent, list =>
54 [...list, { type: e.subagentType, model: model ?? e.parentModel, source }].slice(-6),
55 )
56
57 return next(model === e.model ? e : { ...e, model })
58 })
59
60 on('command.run', { command: 'agent-models' }, async ($, e) => {
61 const [type, model] = e.args.trim().split(/\s+/)
62
63 if (type !== undefined && type !== '') {
64 if (model === undefined || ![INHERIT, ...MODELS].includes(model)) {
65 return { text: `Usage: /agent-models <type> <${[INHERIT, ...MODELS].join('|')}>` }
66 }
67 const field = FIELDS[type] ?? (type === 'other' ? 'other' : undefined)
68 const custom = parseCustom(typeof options.custom === 'string' ? options.custom : '')
69 if (field === undefined) {
70 if (model === INHERIT) delete custom[type]
71 else custom[type] = model
72 }
73 const set = field !== undefined
74 ? await $.config.set({ key: `agent-models.${field}`, value: model })
75 : await $.config.set({ key: 'agent-models.custom', value: formatCustom(custom) })
76
77 return { text: 'deny' in set && set.deny !== undefined ? `Not saved: ${set.deny}` : `${type} subagents: ${model}.` }
78 }
79
80 const rows = [...Object.keys(FIELDS), ...Object.keys(parseCustom(typeof options.custom === 'string' ? options.custom : ''))]
81 .map(name => ` ${name.padEnd(20)} ${pinFor(name, options) ?? INHERIT}`)
82 const spawns = (await read($, recent)).map(s => ` ${s.type.padEnd(20)} ${s.model} (${s.source})`)
83
84 return {
85 text: [
86 'Pins:',
87 ...rows,
88 ` ${'any other'.padEnd(20)} ${typeof options.other === 'string' ? options.other : INHERIT}`,
89 options.force === true ? 'Explicit models are overridden.' : 'A model the caller names wins over a pin.',
90 ...(spawns.length > 0 ? ['', 'Recent subagents:', ...spawns] : []),
91 '',
92 'Change with /agent-models <type> <model> (or /config in a terminal).',
93 ].join('\n'),
94 }
95 })
96}
97types/index.d.ts 8 lines1export type Spawn = { type: string; model: string; source: 'pinned' | 'caller' | 'inherited' }
2
3declare module 'claude-code' {
4 interface PluginState {
5 'agent-models': { recent: Spawn[] }
6 }
7}
8