Phase 0 spike for crew: checks the agent-team APIs crew depends on and logs what happens. Not for release.

Security-reviewed mods for Claude Code, with mods made here to follow: live panes, bands above the prompt, status-line entries, toasts, slash commands, and hooks on tool calls or prompts that change how Claude Code looks and behaves.
This repository is a Claude Code plugin marketplace. It holds mods made here, in mods/, and community mods that passed a security review, each pinned to the exact commit that was reviewed. Any of them installs with one line.
At the prompt of a Claude Code terminal session (2.1.287 or later):
/plugin install <mod> --marketplace PeterTing/claude-mod
For example, /plugin install usage-meter --marketplace PeterTing/claude-mod.
Answer y to add the marketplace, then pick a scope (user scope is the default).
From a shell:
claude plugin install <mod> --marketplace PeterTing/claude-mod
In the desktop app, where /plugin does not run: click + next to the prompt box → Plugins → Add marketplace with PeterTing/claude-mod → Add plugin.
Mods by other authors are not sandboxed: they run with your permissions. Each one listed here was reviewed in full before listing, and installing it from this marketplace gets exactly the reviewed commit. COLLECTION.md has every mod's caveats, and docs/vetting.md explains the review.
| Mod | What it does | Verdict |
|---|---|---|
usage-meter | 5-hour and 7-day plan usage bars above the prompt | Recommend |
context-bar | Context window as a stacked bar by category | Recommend |
token-weather-usage | Context, limits, cache time left and cost in one line | Recommend |
flightdeck | Live dashboard pane for the model, permission checks and subagents | Recommend |
whats-agent-doing | A box above the prompt saying what Claude is doing now | Recommend |
agent-radar | One live line per running subagent | Recommend |
filetree | File tree with git status that highlights files Claude touches | Recommend |
prompt-rail | A rail of your past prompts to reread and jump to | Recommend |
human-in-the-loop | Tasks only you can do, waiting in a pane until you answer | Caution |
prismantis | Themed tables, code and Mermaid diagrams in replies | Caution |
leakstop | Masks or holds secrets before they leak | Caution |
cache-tax | Keeps the prompt cache warm during breaks | Caution |
Caution means the mod is acceptable but has trade-offs to read first, such as token cost, system-prompt additions or rewritten prompts.
| Mod | What it does | Kind |
|---|---|---|
zh-reply | Claude and its agents reply in Chinese (Traditional by default); a reply in another language is asked for again | System prompt, agent tasks, messages; sends a rewrite prompt |
crew | A resident PM and QA work your PRD into features, acceptance criteria and test cases, bring gaps and questions back to you, and remember decisions in the repo. Work in progress | Spawns subagents, MCP tools, a pane; writes prd-tracker.json and .crew/; uses Haiku once a day |
.claude-plugin/marketplace.json # marketplace manifest: mods made here + pinned community mods
docs/vetting.md # how community mods are reviewed
mods/<mod-name>/
.claude-plugin/plugin.json # name, version, description
hooks/hooks.json # { "modules": ["./register.tsx"] }
hooks/register.tsx # the hooks module
hooks/*.test.ts # behaviour tests
README.md # what it does, screenshots, options
COLLECTION.md # reviewed community mods: verdicts, caveats, pinned commits
claude --plugin-dir mods/<mod-name> # run Claude Code with the mod loaded (hot-reloads on save)
claude plugin validate mods/<mod-name> # check the manifest and hooks module
claude plugin test mods/<mod-name> # run the mod's *.test.ts files
claude plugin validate . # check the marketplace manifest
To add a mod: create mods/<mod-name>/, add an entry { "name": "<mod-name>", "source": "./mods/<mod-name>" } to plugins in .claude-plugin/marketplace.json, add a row to the table above, and make sure validate and test both pass.
Issues and pull requests are welcome, whether it's a new mod, a fix, or a community mod you'd like added. For a community mod, include the repository URL, the plugin path, the license and one line on what it does; it is listed after it passes the review.
MIT for this repository. Community mods are fetched from their authors' repositories and keep their own licenses.
hooks/register.ts 219 lines1import type { Register } from 'claude-code'
2
3/**
4 * Phase 0 spike for crew. One headless run walks S1–S8 and writes every
5 * observation to <cwd>/crew-spike.jsonl:
6 *
7 * S1 a plugin-defined agent type spawned as a named teammate, its model and effort
8 * S2 the teammate calls this plugin's tool, and the call carries its agentId
9 * S3 per-request usage (context size) for the teammate's loop
10 * S4 stopping the teammate with TaskStop and respawning it under the same name
11 * S5 a delivery to the teammate can carry appended text (memory injection)
12 * S6 session.compact for an agent loop (logged if it ever fires)
13 * S7 the teammate's fixed cost: a lean agent against one with default tools and CLAUDE.md
14 * S8 rewriting effort, then model, on a step, and what that does to the prompt cache
15 */
16
17const MARK = 'MEM-SPIKE-7: the staging database is called aurora-blue.'
18
19const spike = {
20 logPath: '',
21 lines: [] as string[],
22 phase: 'start',
23 started: false,
24 echoId: undefined as string | undefined,
25 echoWakes: 0,
26 crewIds: new Set<string>(),
27 dropNotifications: true,
28}
29
30async function log($: any, kind: string, data: unknown) {
31 spike.lines.push(JSON.stringify({ t: Date.now(), phase: spike.phase, kind, data }))
32 if (spike.logPath !== '') await $.fs.write(spike.logPath, `${spike.lines.join('\n')}\n`)
33}
34
35async function spawnEcho($: any, type: string, prompt: string) {
36 const r = await $.agent.spawn({ subagentType: type, name: 'echo', description: 'crew spike echo', prompt })
37 await log($, 'spawn.result', { type, r })
38 if (r.agentId !== undefined) spike.crewIds.add(r.agentId)
39 return r
40}
41
42async function sendEcho($: any, text: string) {
43 await log($, 'send.result', await $.session.send({ to: 'echo', text }))
44}
45
46export const register: Register = on => {
47 on('session.start', async ($: any, e: any, next: any) => {
48 spike.logPath = `${e.cwd}/crew-spike.jsonl`
49 const tool = await $.tool.register({
50 name: 'note',
51 description: 'Record one short note for the crew spike. Call it exactly as instructed.',
52 inputSchema: { type: 'object', properties: { text: { type: 'string' } }, required: ['text'] },
53 isDeferred: false,
54 })
55 await log($, 'tool.register', tool)
56 const lean = await $.agent.register({
57 name: 'echo',
58 description: 'Crew spike test agent. Only used by the crew-spike plugin.',
59 prompt: 'You are a test agent for a plugin spike. Do exactly what each message asks, using the note tool when told, then end your turn with the single word the message asks for. Be brief.',
60 tools: ['mcp__crew-spike__note'],
61 model: 'haiku',
62 effort: 'low',
63 omitClaudeMd: true,
64 autoCompactWindow: 100000,
65 })
66 await log($, 'agent.register lean', lean)
67 const full = await $.agent.register({
68 name: 'echo-full',
69 description: 'Crew spike baseline agent with default tools and CLAUDE.md.',
70 prompt: 'You are a test agent for a plugin spike. Reply with the single word the message asks for.',
71 model: 'haiku',
72 effort: 'low',
73 })
74 await log($, 'agent.register full', full)
75 const dev = await $.agent.register({
76 name: 'echo-dev',
77 description: 'Crew spike agent with the tools a working role needs.',
78 prompt: 'You are a test agent for a plugin spike. Reply with the single word the message asks for.',
79 tools: ['Read', 'Edit', 'Write', 'Bash', 'mcp__crew-spike__note'],
80 model: 'haiku',
81 effort: 'low',
82 omitClaudeMd: true,
83 })
84 await log($, 'agent.register dev', dev)
85 return next(e)
86 })
87
88 on('prompt.submit', async ($: any, e: any, next: any) => {
89 const isCrewNotice =
90 e.origin?.kind === 'task-notification' && [...spike.crewIds].some(id => String(e.text).includes(id))
91 await log($, 'prompt.submit', { origin: e.origin, isMeta: e.isMeta, turnId: e.turnId, isCrewNotice, text: String(e.text).slice(0, 160) })
92 if (isCrewNotice && spike.dropNotifications) {
93 return { drop: 'crew: a routine agent update, folded into the digest' }
94 }
95 const r = await next(e)
96 if (!spike.started && e.origin?.kind !== 'plugin' && (await $.env.get('CREW_SPIKE_BASELINE')) === '1') {
97 spike.started = true
98 spike.phase = 'S7'
99 for (const type of ['crew-spike:echo', 'crew-spike:echo-dev', 'crew-spike:echo-full']) {
100 const b = await $.agent.spawn({ subagentType: type, name: type.split(':')[1], description: 'crew spike baseline', prompt: 'Reply with the single word DEV.' })
101 await log($, 'spawn.result', { type, r: b })
102 }
103 return r
104 }
105 if (!spike.started && e.origin?.kind !== 'plugin') {
106 spike.started = true
107 spike.phase = 'S1'
108 const spawned = await spawnEcho($, 'crew-spike:echo', 'Call the note tool with the text "hello from echo". Then reply DONE1.')
109 spike.echoId = spawned.agentId
110 }
111 return r
112 })
113
114 on('agent.spawn', async ($: any, e: any, next: any) => {
115 const r = await next(e)
116 await log($, 'agent.spawn', {
117 origin: next.origin,
118 subagentType: e.subagentType,
119 name: e.name,
120 isTeammate: e.isTeammate,
121 background: e.background,
122 model: e.model,
123 parentModel: e.parentModel,
124 result: r,
125 })
126 return r
127 })
128
129 on('tool.call', { tool: 'mcp__crew-spike__note' }, async ($: any, e: any) => {
130 await log($, 'note', { agentId: e.agentId, input: e.input ?? e })
131 return { result: 'noted' }
132 })
133
134 on('turn.step', async function* ($: any, e: any, next: any) {
135 let input = e
136 if (e.agentId !== undefined && e.agentId === spike.echoId) {
137 if (spike.phase === 'S8-effort') input = { ...e, effort: 'medium' }
138 if (spike.phase === 'S8-model') input = { ...e, model: 'claude-sonnet-5-5' }
139 }
140 await log($, 'step.in', { agentId: e.agentId, model: input.model, effort: input.effort, messageCount: e.messageCount, asked: { model: e.model, effort: e.effort } })
141 const stream = next(input)
142 for await (const chunk of stream) {
143 if (chunk.kind === 'stop') {
144 await log($, 'step.stop', { agentId: e.agentId, stopReason: chunk.stopReason, usage: chunk.usage })
145 }
146 yield chunk
147 }
148 return await stream.result
149 })
150
151 on('session.send', async ($: any, e: any, next: any) => {
152 const r = await next(e)
153 await log($, 'session.send', { to: e.to, origin: e.origin, agentId: e.agentId, text: String(e.text).slice(0, 200), result: r })
154 return r
155 })
156
157 on('session.receive', async ($: any, e: any, next: any) => {
158 const isForEcho = e.agentId !== undefined && e.agentId === spike.echoId
159 const text = spike.phase === 'S5' && isForEcho ? `${e.text}\n\n[crew memory] ${MARK}` : e.text
160 const r = await next({ ...e, text })
161 await log($, 'session.receive', { agentId: e.agentId, origin: e.origin, injected: text !== e.text, text: String(text).slice(0, 300) })
162 return r
163 })
164
165 on('turn.start', async ($: any, e: any, next: any) => {
166 if (e.agentId !== undefined) await log($, 'turn.start', { agentId: e.agentId, text: String(e.text).slice(0, 400) })
167 return next(e)
168 })
169
170 on('session.compact', async ($: any, e: any, next: any) => {
171 await log($, 'session.compact', { agentId: e.agentId, trigger: e.trigger, messages: e.messages?.length })
172 return next(e)
173 })
174
175 on('turn.complete', async ($: any, e: any, next: any) => {
176 const r = await next(e)
177 await log($, 'turn.complete', { agentId: e.agentId, reason: e.reason, usage: e.usage, answer: String(e.answer).slice(0, 300) })
178 if (e.agentId === undefined || e.agentId !== spike.echoId) {
179 if (e.agentId !== undefined && spike.phase === 'S7' && String(e.answer).includes('DEV')) {
180 spike.phase = 'done'
181 await log($, 'agents', await $.agent.list())
182 $.prompt.submit({ text: 'The crew spike is finished. Reply with the single word FINISHED.' }).catch(() => undefined)
183 }
184 return r
185 }
186 spike.echoWakes += 1
187 await log($, 'agents', await $.agent.list())
188 if (spike.echoWakes === 1) {
189 spike.phase = 'S5'
190 await sendEcho($, 'Read the line that starts with [crew memory] in this message, then call the note tool with the name of the staging database it mentions. Then reply DONE2.')
191 } else if (spike.echoWakes === 2) {
192 spike.phase = 'S8-effort'
193 await sendEcho($, 'Call the note tool with the text "effort check". Then reply DONE3.')
194 } else if (spike.echoWakes === 3) {
195 spike.phase = 'S8-model'
196 await sendEcho($, 'Call the note tool with the text "model check". Then reply DONE4.')
197 } else if (spike.echoWakes === 4) {
198 spike.phase = 'S4'
199 const stopped = await $.tool.call({ tool: 'TaskStop', task_id: spike.echoId }).catch((err: unknown) => ({ error: String(err) }))
200 await log($, 'TaskStop', stopped)
201 await log($, 'agents after stop', await $.agent.list())
202 const again = await spawnEcho($, 'crew-spike:echo', 'You were restarted. Call the note tool with the text "respawned". Then reply DONE5.')
203 spike.echoId = again.agentId
204 spike.echoWakes = 4
205 spike.phase = 'S4-respawned'
206 } else {
207 spike.phase = 'S7'
208 await log($, 'agents', await $.agent.list())
209 const full = await $.agent.spawn({ subagentType: 'crew-spike:echo-full', name: 'echo-full', description: 'crew spike baseline', prompt: 'Reply with the single word BASELINE.' })
210 if (full.agentId !== undefined) spike.crewIds.add(full.agentId)
211 await log($, 'spawn.result', { type: 'crew-spike:echo-full', r: full })
212 const dev = await $.agent.spawn({ subagentType: 'crew-spike:echo-dev', name: 'echo-dev', description: 'crew spike dev baseline', prompt: 'Reply with the single word DEV.' })
213 if (dev.agentId !== undefined) spike.crewIds.add(dev.agentId)
214 await log($, 'spawn.result', { type: 'crew-spike:echo-dev', r: dev })
215 }
216 return r
217 })
218}
219