SLOPSHOPPER

modes

How a session works with you and with its subagents. Two session modes, each [on|off|status], answered with no model call, Claude told once at each switch and…

spinnerguardcommandprompt
★ 7v0.7.0MITupdated 2026-10-16proxify-dev/agentic-kit/plugins/modes
A shopper browsing a rack in a slop shop
README

modes

How a session works with you and with its subagents: it asks you until the direction is clear, stops to check with you when the work drifts, hands work to agents by model tier, and answers without changing anything when you only want answers.

Install: ak setup and pick modes, or /plugin install modes@agentic-kit. Needs Claude Code 2.1.298 or newer: an older one loads nothing from it, silently (/ask is then an unknown command; checked on 2.1.295–2.1.301). It is a Claude Mod, so it loads only with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 in the env block of ~/.claude/settings.json (ak setup sets it). Was agentic-workflows until 0.7.0. To try a change in this folder: claude --plugin-dir plugins/modes (the engine refuses a hooks module whose real path lies outside the plugin folder, so not through a symlink).

whatdoes
/modes:interviewrounds of AskUserQuestion until you and Claude agree on the direction; you start it, Claude never does
/modes:align [what looks off]mid-task: no more changes until agreed; Claude plays back the goal, what it did and what was next, names the step where the work drifted, asks with AskUserQuestion, then continues on the answer; you start it
`/orchestrate [on\off\status]`a session mode: hand parts to subagents on another model; orchestrating in the prompt footer while on
`/ask [on\off\status]`a session mode: answer only, change nothing; Edit, Write and NotebookEdit are refused while on (a subagent's too); ask mode in the prompt footer while on

A session mode is for this session only, answered by the mod with no model call, and Claude is told at each switch (below); both can be on at once. The modes are a Claude Mod: hooks/register.ts, the plugin's one hooks module, registers them through hooks/mode.ts (the switch, the notes, the chip, for every mode; each event hooked once, as the engine requires) from one ModeSpec each (hooks/orchestrate.ts, hooks/ask.ts). It loads only with CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1. A new mode is a ModeSpec, its entry in register.ts and its prompts/ files.

What Claude reads is in prompts/, one file per moment, read when it is sent: prompts/<moment>.<mode>.md (switch-on, switch-off, each-prompt, after-compaction; ask also before-edit, the reason an edit is refused), its frontmatter (why, from) stripped, {{rules}} filled from prompts/_rules.<mode>.md. Change the words there; the mod holds none.

_rules.ask.md names every kind of change, the shell's included: the mod refuses the file tools, but a Bash command that writes is held back by the rules alone, since telling one from a read takes more than its text.

_rules.orchestrate.md is the delegation rules: work stays in the main session unless a part is independent, worth more than the cost of starting an agent (~5k tokens), and wants another model (haiku · sonnet · opus). The rules name models, never agents: Claude picks the agent type that fits, the project's own included, and sets the model on the call.

What Claude is told, and when

A plugin can add text to the conversation but never take it back. So each switch is written in once, where it happens, and the history says what is in force at every point:

The same for each mode; the sizes are orchestrate's.

momentClaude getssize
/orchestrate onthe command's row, and as its hidden note "Orchestration switched ON …" + the rules~500 tokens, once
each prompt while onone reminder line, attached to the prompt~50 tokens
/orchestrate offthe command's row, and as its hidden note "… the rules earlier in this conversation no longer apply"~50 tokens, once
the first prompt after a compaction, while onthe rules again, in place of the reminder: a summary can drop them~500 tokens

Before 0.3.0 the rules came on every prompt and off was never said: asked after off, a model still answered that orchestration was ON.

The state is $.state (modes.modes, one entry per mode by name, contract types/index.d.ts) for the running process, which the footer chip reads, and $.store key <mode>:<session id> (orchestrate:…, ask:…), present only while on, so claude --resume finds it; an entry older than 90 days is removed at session start.

Checks

From the repo root:

claude plugin validate plugins/modes
npx -y -p typescript@5 tsc -p plugins/modes/tsconfig.json
cd plugins/modes && CC_SYSPROMPT_FILE=/dev/null CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude plugin test .

tsc reads the API from .claude/types/claude-code.d.ts (git-ignored): copy it from the plugin-authoring skill's types/ folder after a Claude Code update.

Source 5 files
hooks/register.ts 13 lines
1// The plugin's one hooks module (hooks.json takes one, and each event once): the session modes, all
2// registered by mode.ts, and /ask's refusal of the file tools.
3import type { Register } from 'claude-code'
4
5import { ASK, refuseEdits } from './ask'
6import { modes } from './mode'
7import { ORCHESTRATE } from './orchestrate'
8
9export const register: Register = on => {
10  modes(on, [ORCHESTRATE, ASK])
11  refuseEdits(on)
12}
13
hooks/ask.ts 27 lines
1// /ask [on|off|status]: this session answers and changes nothing. The switch, its reminders and its chip
2// are mode.ts; the words are prompts/*.ask.md.
3//
4// The rules ask for no change of any kind. The file tools are also refused here, in every loop (a
5// subagent's too), with prompts/before-edit.ask.md as the reason: a Bash command that writes is not, since
6// telling one from a read takes more than its text.
7import type { On } from 'claude-code'
8
9import { isOn, words } from './mode'
10import type { ModeSpec } from './mode'
11
12export const ASK: ModeSpec = {
13  name: 'ask',
14  label: 'Ask mode',
15  chip: 'ask mode',
16  description: 'Answer only, change nothing, for this session: on, off or status; nothing toggles',
17}
18
19const EDITS = new Set(['Edit', 'Write', 'NotebookEdit'])
20
21export const refuseEdits = (on: On) => {
22  on('tool.call', async ($, e, next) => {
23    if (!EDITS.has(e.tool) || !(await isOn($, ASK))) return next(e)
24    return { deny: await words($, ASK.name, 'before-edit') }
25  }).catch(($, e, next) => next(e))
26}
27
hooks/mode.ts 157 lines
1// A session mode: /<name> [on|off|status], what Claude is told about it, and its chip. /orchestrate and
2// /ask are each a ModeSpec, registered together by modes(on, specs) from the plugin's one hooks module, register.ts.
3//
4// A plugin can add to the conversation but never take back, so each switch is written in once, where it
5// happens, and the history says what is in force at every point:
6//
7//   /<name> on | off           the command's own row, no model call; its hidden note is the rules (on) or
8//                              "they no longer apply" (off)
9//   each prompt while on       one reminder line, attached to the prompt
10//   the prompt after a         the full rules again in place of the reminder: a summary can drop them
11//   compaction, while on
12//   the prompt footer          the mode's chip among the mode labels while on
13//
14// The words are prompts/<moment>.<name>.md, read when they are sent: the file name says when Claude reads
15// it, the frontmatter why, and {{rules}} stands for prompts/_rules.<name>.md. Nothing to edit here for them.
16//
17// The state is $.state for this process (one entry per mode, by name, in modes.modes), and $.store (key <name>:<session id>, present only while on) so
18// `claude --resume` finds it; an entry untouched for 90 days is removed at session start.
19import { atom, read, update } from 'claude-code'
20import type { EngineInterface, On } from 'claude-code'
21
22import type { ModeState } from '../types'
23
24const STALE_MS = 90 * 24 * 3600 * 1000
25
26type Saved = { at: number }
27
28export type ModeSpec = {
29  /** The command, the prompts/ suffix and the store prefix: `orchestrate` is /orchestrate. */
30  name: string
31  /** How the command's answers name it: `Orchestration` gives "Orchestration ON for this session." */
32  label: string
33  /** The prompt footer's mode label while on. */
34  chip: string
35  description: string
36}
37
38/** A prompts/ file's body: its frontmatter (why, from) is for people and never sent. */
39async function body($: EngineInterface, file: string): Promise<string> {
40  const text = await $.fs.read(`${$.plugin.root}/prompts/${file}.md`)
41  return text.replace(/^---\n[\s\S]*?\n---\n/, '').trim()
42}
43
44/** What Claude reads at MOMENT of mode NAME: prompts/<moment>.<name>.md, {{rules}} from prompts/_rules.<name>.md. */
45export async function words($: EngineInterface, name: string, moment: string): Promise<string> {
46  const text = await body($, `${moment}.${name}`)
47  return text.includes('{{rules}}') ? text.replace('{{rules}}', await body($, `_rules.${name}`)) : text
48}
49
50const MODES = atom({ plugin: 'modes', key: 'modes' } as const, {})
51
52/** Rewrites mode SPEC's entry in MODES; none is left out when it is undefined. */
53async function set($: EngineInterface, spec: ModeSpec, to: (s: ModeState | undefined) => ModeState | undefined) {
54  await update($, MODES, modes => ({ ...modes, [spec.name]: to(modes[spec.name]) }))
55}
56
57const storeKey = (spec: ModeSpec, sessionId: string) => `${spec.name}:${sessionId}`
58
59/** Mode SPEC's state in this session, loaded from the store when the held value is another session's or none. */
60async function current($: EngineInterface, spec: ModeSpec): Promise<ModeState> {
61  const sessionId = await $.session.id()
62  const held = (await read($, MODES))[spec.name]
63  if (held?.sessionId === sessionId) return held
64  const loaded: ModeState = {
65    sessionId,
66    isOn: (await $.store.get(storeKey(spec, sessionId))) !== undefined,
67    needsRules: false,
68  }
69  await set($, spec, () => loaded)
70  return loaded
71}
72
73/** Whether mode SPEC is on in this session: for a mode's own hooks beside the switch (ask.ts's refusal). */
74export async function isOn($: EngineInterface, spec: ModeSpec): Promise<boolean> {
75  return (await current($, spec)).isOn
76}
77
78async function switchTo($: EngineInterface, spec: ModeSpec, s: ModeState, turnOn: boolean) {
79  await set($, spec, () => ({ ...s, isOn: turnOn, needsRules: false }))
80  const key = storeKey(spec, s.sessionId)
81  if (turnOn) await $.store.set(key, { at: await $.clock.now() } satisfies Saved)
82  else await $.store.delete(key)
83}
84
85async function sweep($: EngineInterface, spec: ModeSpec) {
86  const now = await $.clock.now()
87  for (const key of await $.store.keys()) {
88    if (!key.startsWith(`${spec.name}:`)) continue
89    const saved = (await $.store.get(key)) as Partial<Saved> | undefined
90    if (typeof saved?.at !== 'number' || now - saved.at > STALE_MS) await $.store.delete(key)
91  }
92}
93
94const commandOf = (spec: ModeSpec) => new RegExp(`^/(modes:)?${spec.name}(\\s|$)`)
95
96/** Registers the hooks of every mode in SPECS, once each: the commands, the notes Claude reads, the chips. */
97export function modes(on: On, specs: readonly ModeSpec[]) {
98  on('session.start', async ($, e, next) => {
99    for (const spec of specs) {
100      await $.command.register({ name: spec.name, description: spec.description, argumentHint: '[on|off|status]' })
101      await sweep($, spec)
102      await current($, spec)
103    }
104    return next(e)
105  })
106
107  on('command.run', async ($, e, next) => {
108    const spec = specs.find(s => s.name === e.command)
109    if (spec === undefined) return next(e)
110    const s = await current($, spec)
111    const arg = e.args.trim().split(/\s+/)[0] ?? ''
112    if (arg === 'status') return { text: `${spec.label}: ${s.isOn ? 'ON' : 'OFF'} (this session)` }
113    if (arg !== '' && arg !== 'on' && arg !== 'off') {
114      return { text: `Unknown argument '${arg}'. Use: /${spec.name} [on|off|status]` }
115    }
116    const turnOn = arg === '' ? !s.isOn : arg === 'on'
117    if (turnOn === s.isOn) return { text: `${spec.label} is already ${turnOn ? 'ON' : 'OFF'} for this session.` }
118    const note = await words($, spec.name, turnOn ? 'switch-on' : 'switch-off')
119    await switchTo($, spec, s, turnOn)
120    return { text: `${spec.label} ${turnOn ? 'ON' : 'OFF'} for this session.`, context: [note] }
121  })
122
123  on('prompt.submit', async ($, e, next) => {
124    if (specs.some(spec => commandOf(spec).test(e.text))) return next(e)
125    const notes: string[] = []
126    for (const spec of specs) {
127      const s = await current($, spec)
128      if (!s.isOn) continue
129      notes.push(await words($, spec.name, s.needsRules ? 'after-compaction' : 'each-prompt'))
130      if (s.needsRules) await set($, spec, v => (v ? { ...v, needsRules: false } : v))
131    }
132    if (notes.length === 0) return next(e)
133    return next({ ...e, context: [...(e.context ?? []), ...notes] })
134  }).catch(($, e, next) => next(e))
135
136  on('session.compact', async ($, e, next) => {
137    const done = await next(e)
138    if (e.trigger === 'precompute' || e.agentId !== undefined || done.skip !== undefined) return done
139    for (const spec of specs) {
140      if (await isOn($, spec)) await set($, spec, v => (v ? { ...v, needsRules: true } : v))
141    }
142    return done
143  }).catch(($, e, next) => next(e))
144
145  on('session.end', async ($, e, next) => {
146    await update($, MODES, () => ({}))
147    return next(e)
148  })
149
150  on('ui.render', { component: 'SessionMode' }, async ($, e, next) => {
151    const held = await read($, MODES)
152    const chips = specs.filter(spec => held[spec.name]?.isOn).map(spec => spec.chip)
153    if (chips.length === 0) return next(e)
154    return next({ ...e, props: { ...e.props, modes: [...e.props.modes, ...chips] } })
155  })
156}
157
hooks/orchestrate.ts 11 lines
1// /orchestrate [on|off|status]: this session's delegation mode. The switch, its reminders and its chip are
2// mode.ts; the words are prompts/*.orchestrate.md.
3import type { ModeSpec } from './mode'
4
5export const ORCHESTRATE: ModeSpec = {
6  name: 'orchestrate',
7  label: 'Orchestration',
8  chip: 'orchestrating',
9  description: 'Delegation mode for this session: on, off or status; nothing toggles',
10}
11
types/index.d.ts 10 lines
1/** One session mode's state: whether it is on, and whether its rules are owed again after a compaction. */
2export type ModeState = { sessionId: string; isOn: boolean; needsRules: boolean }
3
4declare module 'claude-code' {
5  interface PluginState {
6    /** Each mode's state by its name (`orchestrate`, `ask`); none while the mode has not been read this process. */
7    'modes': { modes: Record<string, ModeState | undefined> }
8  }
9}
10