135 KiB
Approaches
This page is the index of every engineering approach the brewcode plugin suite applies, and of the links between them: which setup skill installs which hook, which skill writes which agent, which text is injected at which moment. It is for the maintainer who forgot where a file comes from, and for the user who wants to know what a plugin actually does to a session.
Rule: every new hook, agent, generated skill or injection gets a row here - in its domain table and in "Where each thing lives". A feature without a row is undocumented.
How to read this page
Column meanings are the same in every table below.
| Column | Meaning |
|---|---|
| Scope | plugin-shipped = live as soon as the plugin is installed. global = written into ~/.claude/. project = written into <repo>/.claude/. choice = the skill asks you which of the two, and never guesses. |
| Trigger | The Claude Code hook event, or "context auto-load" for always-loaded text, or "user runs the skill". |
| Fires when | Plain English. The real condition, not the event name. |
| Excerpt | Real quoted text from the file. ... marks cut middles. Never the whole prompt. |
| Project-specific | yes = the content differs per repository. no = the same bytes everywhere. |
Two more conventions used across the suite:
- Fail-open, with one deliberate exception. A discipline hook returns
{}when anything goes wrong, which means "do nothing, carry on". So a broken discipline layer costs you a good habit, never the ability to keep working. Only five hooks ever block on purpose: the hard wall, the agent router, the deadline guard, the return guard and the docsync gate. Two of those five depart from the rule.compact-recall.mjsfails open loudly rather than silently: every error path, including its outercatch, degrades to its[INTENT]fragment, because after a compaction saying nothing is itself the failure.hardmode-guard.mjsfails closed (v6, BT-F01): it is a security guard, so an unparseable payload, a corruptstate.jsonnext to an installed manager dir, or any internal error DENIES (hardmode-guard.mjs:26-31,:326,:399-403). The only pass-throughs left are "the manager was never installed here" and the subagent discriminator, and the self-exempt exit CLI survives both. - Say it at the moment it applies. Contracts are injected at session open, at prompt submit, at subagent spawn or at tool call - never left to be remembered from earlier in the context.
Map
Two pictures. The first is the structure: the blocks this page is made of, what lives inside each, and how they act on one another. The second is one turn in time: when each mechanism speaks, and which ones can stop you.
The blocks and what connects them. One box per numbered section below; each row names the kind of artefact and then the artefact. Blue = live the moment the plugin is installed. Amber = the one file two generators contend over. Every other box is opt-in, or a convention that is text rather than code. A solid arrow acts on its target; a dotted arrow means conforms to or reuses, never a second write.
classDiagram
direction TB
class S1["1 - Always-on hooks"] {
hook forced-eval.mjs - ROLE SPLIT BRANCH
hook brewcode session-start.mjs - banner and plan link
hook role-recall.mjs - same frame after a compaction
hook compact-recall.mjs - plan intent and task graph
lib reminder.mjs - the one copy of that text
hook brewtools session-start.mjs - wall notice
hook manager-prompt.mjs - codewords ++m ++a ++rr ++r
hook hardmode-guard.mjs - ships inert and fails closed
config brewdoc hooks.json - empty on purpose
recipe project root - env then git then walk then PWD
}
class S2["2 - Delegation and manager discipline"] {
text ++m manager role and 6-field spawn brief
text ++a architecture first
text ++rr anti-regression review
text ++r two-phase double check
guard hard wall - balanced or strict
state manager-state.json - self-exempt exit
}
class S3["3 - Subagent control"] {
hook agent-return-contract.mjs - at spawn
hook agent-return-guard.mjs - at stop
lib agent-return-budget.mjs - 1000 and 2500 est-tokens
hook agent-deadline-guard.mjs - warn at 80 percent
hook agent-deadline-cleanup.mjs - silent
hook agent-router.mjs - tier 1 deterministic
agent haiku judge - tier 2 strict only
config agent-return agent-deadline agent-router json
text APPROVAL REQUIRED envelope - no AskUserQuestion in a subagent
}
class S4["4 - Context economy"] {
hook think-short - open and every 10th prompt
text think-short-prompt.md
mcp semble-code pinned 0.5.4
rule semble-first.md - always loaded
hook semble-session and semble-reminder.mjs
hook semble-prefetch.mjs - 3 ranked paths
hook semble-stats.mjs - telemetry only
script semble-agents.sh - tools migration
}
class S5["5 - Review and anti-drift"] {
skill superreview - MODE by DEPTH
gate mechanical gates run first
ladder CONFIRMED then EVIDENCE then UNVALIDATED
doc references scope.md - sanctioned baseline
team team.md and trace.jsonl
vote quorum 2 of 3
skill brewcode-review - this repo only
report reports TIMESTAMP superreview
}
class S6["6 - Project generators"] {
skill superreview-setup and teams-setup
skill task-board-setup - agent skills rule board
skill docsync-setup - 3 hooks and config
skill memory-sync-setup - 1 skill
skill manager-setup and think-short-setup
skill semble-setup - MCP at user scope
skill agent-return agent-deadline agent-router setup
skill setup-status - reports and writes nothing
template ssh and deploy admin - carry the approval envelope
rule generated hooks inline their own project root
}
class S7["7 - Agent authoring conventions"] {
section Return Contract - 8 of 8
section Scope guard - 7 of 8
section Checkpointing - 8 of 8
frontmatter maxTurns sized to the role
frontmatter Bash declared in tools - 8 of 8
path CLAUDE_PLUGIN_ROOT - 6 of 8
section Approval Contract - 2 of 8
stamp four provenance keys
suite suite-creator-contract.mjs - 27 checks on the 2.1.233 facts
}
class S8["8 - Skill contract"] {
script validate-skill.sh - exits non-zero
doc prompt-contract.md and mode-sync.md
contract prompt-first argument-hint
contract PLAN block - 5 fields
table mode keywords EN and RU
modes canonical 7 - enable and disable 11 of 11
rule invocation invariant 27 of 27
suites 9 regression harnesses - 565 checks
}
class IG["intent-guard - one file two writers"] {
agent project agents intent-guard.md
rule tiered sources of truth
budget 15 tool calls and 10 minutes
verdict ALIGNED or MINOR or MAJOR DRIFT
rule REUSE leaves it byte-untouched
rule never the code reviewer
}
S1 --> S2 : injects the codeword blocks every turn
S6 --> S2 : arms the inert hard wall
S6 --> S3 : installs the router deadline and return hooks
S6 --> S4 : installs think-short and semble
S6 --> S5 : emits the superreview skill and the team roster
S6 ..> S8 : every emitted skill carries the prompt contract
S6 ..> IG : teams-setup reuses that file - no second write
S5 --> IG : generate.sh emit-agent is the ONE writer
S5 --> S7 : the member agents it spawns carry these sections
S2 --> S3 : the 6-field brief opens every spawn
S4 --> S3 : re-injects tone and search rule at each spawn
S3 --> S7 : its budget is quoted in every Return Contract
S8 ..> S7 : shares the four provenance stamp keys
style S1 fill:#1f6feb,stroke:#0b3d91,color:#ffffff
style IG fill:#bf8700,stroke:#7d5300,color:#ffffff
One turn in time. A blue band is a moment in the main session, a violet band a moment inside a subagent; the band note names the hook event. A solid arrow injects text or passes control. A crossed arrow marks a mechanism that can deny the call or block the return - everything else fails open and stays silent. One band is not part of the turn sequence: the compaction band fires whenever the context is squeezed, mid-turn included, and it can fire several times in one session.
sequenceDiagram
autonumber
actor U as User
participant BC as brewcode hooks
participant BT as brewtools hooks
participant MS as Main session
participant PH as Installed hooks and rules
participant SA as Subagent
rect rgba(31,111,235,0.14)
Note over BC,PH: SessionStart
BC->>MS: banner, update check
BC->>MS: plan symlink on clear
BT->>MS: hard wall notice, only if armed
PH->>MS: think-short tone directive
PH->>MS: semble repo directive
PH->>MS: semble-first rule, always loaded
end
rect rgba(31,111,235,0.14)
Note over BC,MS: SessionStart matcher compact, after each compaction
BC->>MS: role-recall, same ROLE SPLIT BRANCH text
BC->>MS: compact-recall, PLAN from this transcript or LATEST.md
BC->>MS: compact-recall, TASKS call TaskList first
end
rect rgba(31,111,235,0.14)
Note over U,PH: UserPromptSubmit, every turn
U->>MS: prompt
BC->>MS: ROLE, SPLIT, BRANCH
BT->>MS: codewords ++m ++a ++rr ++r
BT->>MS: auto manager block while wall is on
PH->>MS: think-short, every 10th prompt
PH->>MS: semble prefetch, 3 ranked paths
end
rect rgba(31,111,235,0.14)
Note over MS,PH: PreToolUse, main session
MS->>PH: Write, Edit or Bash
PH--xMS: hard wall denies, delegate instead
PH->>MS: semble wrong-tool nudge
end
rect rgba(130,80,223,0.16)
Note over MS,SA: Subagent spawn
PH--xMS: router names the real expert
PH->>MS: haiku judge, strict level only
MS->>SA: Task spawn
PH->>SA: think-short tone directive
PH->>SA: return contract announced
PH->>SA: semble subagent nudge
Note over SA: no AskUserQuestion here - approvals leave as envelopes
end
rect rgba(130,80,223,0.16)
Note over PH,SA: Subagent running
SA->>PH: every tool call
PH->>SA: 80 percent, start wrapping up
PH--xSA: 100 percent, finalize tools only
end
rect rgba(130,80,223,0.16)
Note over MS,SA: SubagentStop
SA->>MS: final answer
PH--xSA: over budget, compress once
PH->>PH: deadline state cleanup, silent
end
rect rgba(31,111,235,0.14)
Note over MS,PH: PostToolUse and Stop
PH->>PH: docsync track, semble telemetry
PH--xMS: docsync gate, stale docs, once
MS->>U: answer
end
1. Always-on hooks
These run the moment the plugin is installed. No setup skill, no config file, no opt-in.
| Approach | File | Scope | Trigger | Fires when | Excerpt | Purpose | Problem solved | Project-specific |
|---|---|---|---|---|---|---|---|---|
| Delegation reminder on every prompt | brewcode/hooks/forced-eval.mjs |
plugin-shipped | UserPromptSubmit, timeout 2 s |
Every prompt, slash commands included. Skipped only for one-word replies: yes, no, ok, continue, a bare number, a single letter |
[ROLE] Manager: scan agents ... expert for this domain exists -> delegate regardless of size; ... [SPLIT] One agent for an hour = drift you cannot observe ... [BRANCH] Stay on the current branch; none chosen -> main. - the text itself is not in this file: it is REMINDER_TEXT from brewcode/hooks/lib/reminder.mjs, shared with role-recall.mjs |
Re-state role, subagent sizing and branch default every turn | The model does domain work itself while a project expert agent exists; one giant agent runs for an hour; a feature branch nobody asked for | no |
| Session banner, update check, plan symlink | brewcode/hooks/session-start.mjs |
plugin-shipped | SessionStart, timeout 3 s |
Every session start. The symlink part runs only when source === 'clear', and only if the newest .md in ~/.claude/plans/ is under 60 s old (PLAN_FRESHNESS_MS, :35); it then points <repo>/.claude/plans/LATEST.md at that global file. Two containment checks, both refusals: <repo>/.claude/plans that is itself a symlink is left alone and nothing is linked - mkdirSync/symlinkSync follow it and would write LATEST.md outside the project root (:140-148); and an existing LATEST.md is replaced only when it is a symlink whose target resolves inside the plans dir, so a hand-written file, a directory or a foreign symlink is preserved and logged as a conflict (claimLatestLink, :74-107). The whole output is a systemMessage for the human - the model is sent nothing |
brewcode: <root> | session: <id> | perm: <mode> and, when out of date, UPDATE brewcode <installed> -> <released>: <link to the releases page>; the link step logs Linked: .claude/plans/LATEST.md -> <plan name> |
Show the live build and permission mode; keep the just-written plan reachable from the project | Running an old build silently; losing the plan file when Plan Mode clears the session | no |
| Role frame re-injected after a compaction | brewcode/hooks/role-recall.mjs |
plugin-shipped | SessionStart with "matcher": "compact", timeout 2 s |
Only after a compaction, auto or /compact. Belt-and-braces on top of the matcher: anything but input.source === 'compact' returns {}, so startup, resume, clear and fork are silent - they still carry the frame. Unconditional otherwise, and compactions chain |
The same three lines as forced-eval.mjs, byte-for-byte: both import REMINDER_TEXT from brewcode/hooks/lib/reminder.mjs, which exists so [ROLE] / [SPLIT] / [BRANCH] cannot drift between two hooks on two different events |
Put the manager role back in front of the model at the one moment the summary has just collapsed every earlier copy of it | An auto-compaction has no prompt, so forced-eval.mjs never fires; after a few compactions the session quietly stops delegating |
no |
| Plan, intent and task graph re-anchored after a compaction | brewcode/hooks/compact-recall.mjs |
plugin-shipped | SessionStart with "matcher": "compact", timeout 2 s |
Same moment, same source === 'compact' guard, and it ALWAYS injects there. It scans this session's transcript_path only - statSync must report a regular file (a FIFO reports size 0 and then blocks readFileSync forever) of at most MAX_TRANSCRIPT_BYTES, 64 * 1024 * 1024; then Buffer.lastIndexOf / includes, no JSONL parsing, ~6 ms scan (one buffer read plus five substring scans) on an 8.13 MB transcript, ~30 ms full process wall clock standalone / ~55 ms spawned from a node parent (node startup dominates). Ladder, first match wins, five rungs since v6: plan-file -> plan-latest -> plan-missing -> plan-in-summary -> intent. plan-latest reads <root>/.claude/plans/LATEST.md (compact-recall.mjs:150-164) and exists because a plan that PREDATES the transcript - --resume, or a session resumed after /clear - leaves no planFilePath to scan for; that link is hook-owned and project-scoped, so it can never be another repo's plan, and it sits above plan-missing because a real file beats a dead path. [TASKS] is appended only when the transcript contains "name":"TaskCreate" |
[PLAN] Read <path> with the Read tool before doing any work. It holds the role model and the delegation split for this session ..., or [PLAN] Read <root>/.claude/plans/LATEST.md ... It is this project's latest plan, carried over from before the compact ..., or [PLAN] The plan file for this session is gone or unreadable at <path>. Rebuild the frame from the compact summary plus TaskList, not from scratch., or [PLAN] This session ran in plan mode; no plan file is available. ..., or [INTENT] Re-read the user ORIGINAL task and intent from the compact summary and keep executing THAT. Do not continue from the most recently remembered fragment, and do not re-scope the work. Plus, when a graph exists: [TASKS] Then call TaskList: a task graph created before the compact ALREADY EXISTS in this session. ... The built-in reminder lags several turns and may show empty, so TaskList is the authority. Then resume the work. |
Say what we were doing, from this session's own record, before the model decides for itself | The session loses the user's original task and starts a brand-new task graph on top of the old one | no |
| Hard-wall awareness at session open | brewtools/hooks/session-start.mjs |
plugin-shipped, reads project state | SessionStart, timeout 2 s |
Every session start, but speaks only when .claude/brewtools/manager/state.json has hard: true in this project |
Manager HARD wall active (project, level=...): main session is orchestration-only - delegate in bounded units ... /brewtools:manager-setup disable to exit. |
Tell the model at turn zero that its own tools are blocked, and how to leave | A whole session of denied tool calls with no explanation | yes |
| Codeword injection, plus an automatic block while the wall is on | brewtools/hooks/manager-prompt.mjs |
plugin-shipped; text resolves project -> global -> plugin default | UserPromptSubmit, timeout 3 s |
Every prompt. Matches ++m, ++a, ++rr, ++r as standalone tokens, so ++rr never matches as ++r. With no codeword it still injects the manager block when state.hard === true |
User typed \++m` - Manager mode is active for this turn:then the block resolved frommanager-setup/references/full.md, which opens [ROLE: MANAGER] ... You are a Manager, not an executor. Your only actions: think, plan, build the TaskGraph, delegate, observe, integrate.Several blocks join with---`. The five block texts are quoted row by row in section 2 |
Switch on a heavy behaviour rule for one turn by typing two characters | Standing instructions cost tokens every turn; typed by hand they drift out of use | yes, text is overridable |
| Physical main-session tool wall, shipped but not registered | brewtools/hooks/hardmode-guard.mjs |
project, and only after install. The file ships inert: it is not listed in brewtools/hooks/hooks.json |
PreToolUse matcher * |
Before every tool call, but three conditions must all hold. (1) /brewtools:manager-setup install has copied the file into the project and registered it in .claude/settings.local.json - the shipped copy is never registered. (2) The project state.json has hard: true; a project with no manager dir at any candidate root returns {}, but a manager dir whose state.json is missing, unparseable or not an object resolves to { hard: true, level: 'strict' } and denies (readProjectState, :98-121). (3) The stdin payload carries agent_id (:339-342) - that key is present only inside a real subagent, which always passes through. agent_type alone is not enough: a main session started with claude --agent <name> carries it too, so it stays walled. Neither key present also means walled |
Hard wall: Write is blocked in the main session - delegate to a subagent. ... To exit run \/brewtools:manager-setup disable`` |
Make the manager role a machine rule, not advice | The model reads "delegate everything" and edits a file anyway | yes |
| No hooks at all | brewdoc/hooks/hooks.json |
plugin-shipped | none | Never. The file is {"hooks":{}} |
- | brewdoc is a pure skill plugin, zero runtime cost | A hook process on every prompt for a plugin with nothing to inject | no |
No hooks and no hooks.json |
brewui/ |
plugin-shipped | none | Never. There is no brewui/hooks/ directory at all, and brewui/skills/ holds only .gitkeep |
- | A registered placeholder plugin that costs a session nothing | Assuming from the three tables above that every plugin in the suite has a hook layer | no |
| One shared stdin/stdout/logging helper per plugin | brewcode/hooks/lib/utils.mjs (227 lines), brewtools/hooks/lib/utils.mjs and brewdoc/hooks/lib/utils.mjs (91 lines each) |
plugin-shipped | none | Never on its own. Imported by the entry hooks for readStdin, output, capText, log, loadConfig, getState, saveState. capText and TEXT_CHANNEL_CAP live here rather than in one hook for the same reason lib/reminder.mjs exists: three brewcode hooks cap the same way and a copy would drift. The three copies are not identical, and brewdoc's is an orphan - brewdoc registers no hooks, so nothing imports it |
export async function readStdin() ... export function output(response) |
One place per plugin for the fail-open stdin/stdout shape every hook repeats | Each hook re-implementing stdin parsing and the empty-{} reply, each with its own bug |
no |
Notes for this section:
- All four text-injecting hooks in this section use
hookSpecificOutput.additionalContext, neverupdatedInput, which is silently dropped onUserPromptSubmitin CC 2.1.x: the two on that event (forced-eval.mjs,manager-prompt.mjs) and the two onSessionStartmatchercompact(role-recall.mjs,compact-recall.mjs). All four cap injected text at 9000 chars, below a 10K disk-spill threshold noted for CC 2.1.174 - the three brewcode ones through the sharedcapText/TEXT_CHANNEL_CAPinbrewcode/hooks/lib/utils.mjs:40,:48, brewtools' through its own localcapText(s, max = 9000)(manager-prompt.mjs:34). - Channel semantics, verified against the CC 2.1.232 bundle:
updatedInput(PreToolUse) andupdatedToolOutput(PostToolUse) are single-writer/last-wins - every hook on the event receives the same original value and the runner keeps only the last hook's edit, so two hooks writing either one clobber each other; both are schema-validated, and a malformed value denies the tool call rather than being ignored.additionalContextaccumulates across every hook registered on the same event - no clobbering, any number of hooks compose.PreToolUseadditionalContextreaches only the parent session, never a subagent;SubagentStartadditionalContextreaches the subagent and supports anagent_typematcher. This is why think-short's subagent injection (below) moved offupdatedInput. - brewcode registers three hooks on
SessionStart, and they split cleanly.session-start.mjsruns on every start and sends the model nothing - its whole output is asystemMessagefor the human. The other two,role-recall.mjsandcompact-recall.mjs, sit in a"matcher": "compact"group, run only after a compaction, and speak only to the model throughadditionalContext, with nosystemMessageat all. So "brewcode's SessionStart is human-only" holds for the unmatched hook and is false for the compact pair. - One project-root recipe, copied on purpose. Every hook and installer in the suite resolves the root
in this fixed order:
CLAUDE_PROJECT_DIR-> git toplevel -> upward walk for.git/.claude->PWD(brewcode/agents/hook-creator.md:373-423, the JS form omitting the git step).CLAUDE_PROJECT_DIRis exported unconditionally into every hook child process, both exec and shell form - it is empty in an interactive or Bash-tool shell, and that is expected, not evidence it is unset for hooks - and it is substituted inside skill markdown since 2.1.196.input.cwdhas exactly one job, resolving relative paths out oftool_input; it drifts mid-session (seeCwdChanged) so it never keys config, state or log placement. Plugin-shipped hooks importprojectRootfrom their plugin'slib/utils.mjs(brewcode/hooks/lib/utils.mjs:15); every generated hook inlines a byte-identical copy instead, and that duplication is deliberate, not debt - a generated hook lands alone in someone else's repo with no plugin lib to import (docsync-track.mjs:9,-watch.mjs:11,-gate.mjs:15, all "SELF-CONTAINED: helpers inlined, Node built-ins only"). Same reasonhardmode-guard.mjs:5-7carries no imports at all. A hook never exits non-zero because the root was ambiguous; an installer about to write does abort. ${CLAUDE_PLUGIN_DATA}is official, persistent and writable.~/.claude/plugins/data/<plugin-id>/, survives plugin updates (2.1.78+).~/.claude/**is a sensitive path, so aWrite/Edittool call there is routed to a permission ASK - not a block - andplugins/is not one of the carve-outs; a hook or Bash subprocess writes there freely. So it is a fully supported persistent write target, just not from aWrite/Edittool call in an unattended run, where${CLAUDE_PROJECT_DIR}/.claude/<subdir>/is the right home (hook-creator.md:351,:364-371). Write the token bare:${VAR:-fallback}is never matched by the substitution regex, reaches the shell verbatim, and the fallback then always wins silently.- Installing brewtools does not arm the wall. Only
/brewtools:manager-setup installdoes. - Nothing in the suite registers
SessionEnd,PreCompactorPostCompact. Zero hits across all threehooks.json, every generatedassets/INSTALL.mdand this repo's own.claude/settings.json. It still has nothing to say once a session is over. Compaction is a different case: the suite does speak there, but it speaks after the squeeze, onSessionStartmatchercompact, because that is the only channel that reaches the model -PostCompactoutput lands in the UI asuserDisplayMessage, which the model never sees (compact-recall.mjs:6-7), and anything injected before a compaction is exactly what the compaction then collapses.
2. Delegation and manager discipline
The forced-eval [ROLE] / [SPLIT] / [BRANCH] lines from section 1 are the base layer: they are always on.
Everything below is optional and sits on top of that base. Codewords ship with the plugin and always work; the
wall has to be installed first.
| Approach | Installed by | Scope | Trigger | Fires when | Excerpt | Purpose | Problem solved | Project-specific |
|---|---|---|---|---|---|---|---|---|
++m manager role |
text in brewtools/skills/manager-setup/references/full.md; manager-setup only re-texts it |
plugin-shipped | UserPromptSubmit |
You type ++m and the session is not in plan mode. One turn only |
You are a Manager, not an executor. Your only actions: think, plan, build the TaskGraph, delegate, observe, integrate. ... "Faster to do it myself" is a symptom, not an argument - delegate anyway. |
Put the session in orchestrator mode for one turn | The model works instead of fanning out | no |
6-field spawn brief, part of the ++m block |
same file, references/full.md |
plugin-shipped | UserPromptSubmit |
Same turn as ++m |
GOAL ... ROLE - what this agent owns; what it must NOT touch. SCOPE - exact paths ... CONTEXT ... CONSUMER ... DONE - acceptance criteria + the exact report shape. A bare one-line task is never enough. |
Every subagent brief carries six named fields | The agent invents its own scope and returns an unusable report shape | no |
++m in plan mode |
references/planmode.md, appended to the full.md block |
plugin-shipped | UserPromptSubmit with permission_mode === 'plan' |
You type ++m in plan mode. Adds a plan addon to the full block |
This instruction dies when you exit plan mode - the PLAN does not. So the role must live INSIDE the plan ... STEP 0 ... "Re-assume MANAGER role. Create the ENTIRE TaskGraph now" |
Carry the manager role across the plan-mode boundary | The role evaporates on exit and implementation starts hand-coding | no |
++a architecture first |
references/architect.md |
plugin-shipped | UserPromptSubmit |
You type ++a; combines with the other groups |
Before implementation, delegate an architecture pass ... do not design it inline. ... As SIMPLE as possible while staying scalable ... Find the closest well-built existing counterpart in the repo ... ADDITIVE to conventions/rules/docs, never instead. |
Design becomes a delegated, written step | Improvised inline design that ignores existing patterns | no |
++rr anti-regression review |
references/review-regression.md |
plugin-shipped | UserPromptSubmit |
You type ++rr. It beats ++r |
ONE primary focus, three axes: 1. NO REGRESSION (primary) ... Review is two-phase, always ... b. Double-check - re-verify each finding before acting (no blind fixes). |
Regression-first, two-phase review after each phase | New code breaking what worked; reviewers fixing findings that were never real | no |
++r two-phase double check |
references/review-double.md |
plugin-shipped | UserPromptSubmit |
You type ++r and not ++rr |
Before the review proper, pass the code for simplification ... 1. Review. 2. Double-check ... 3. Fix - only after confirmation. Never fix on first pass without the double-check step. |
Simplify, review, verify, then fix | Fixing hallucinated findings; shipping over-engineered code | no |
Hard wall, level balanced (default) |
/brewtools:manager-setup install |
project only, there is no global wall | PreToolUse matcher * |
Every main-session tool call while state.hard === true |
Hard wall (balanced): only read-only Bash is allowed in the main session - delegate execution to a subagent. and, for the flat denials, Hard wall: ${tool} is blocked in the main session - delegate to a subagent. (hardmode-guard.mjs:375, :353) |
Remove the session's hands, keep inspection. Tool level: ALWAYS_ALLOW permits Read/Grep/Glob/NotebookRead, Task/Agent/Skill/SlashCommand/ListAgents/SendMessage/Monitor, the plan tools, ToolSearch, task tracking, BashOutput/KillShell, MCP resource introspection and AskUserQuestion; ALWAYS_BLOCK is Write, Edit, NotebookEdit, WebFetch; everything unlisted is default-denied, Artifact explicitly included (:126-148, :349-403). Bash level: an allowlist, not a denylist (v6, BT-F01 vector C) - one READONLY table, bare binary names only (so /bin/ls, VAR=x ls and env ls never match), each row carrying a per-binary flag vetter (:294-306). A wrapper denylist was rejected outright: timeout/xargs/nice were denied only incidentally, so every future "read-only" addition reopened the hole. env is gone - it is both a listed binary and a universal wrapper. Command substitution and any redirection are refused before tokenizing (FORBIDDEN_META, :172) |
"Faster if I just do it": the session implements everything itself with no observable trail | yes |
Hard wall, level strict |
/brewtools:manager-setup level strict |
project | PreToolUse matcher * |
Same, and additionally denies all Bash, WebSearch and every MCP tool | Hard wall (strict): Bash is blocked in the main session - delegate execution to a subagent. |
Zero main-session execution | A "read-only" chain that quietly mutates via redirects, git add, sed -i |
yes |
| Per-binary flag vetting on the Bash allowlist | same install | project | PreToolUse matcher * |
Every balanced-level Bash call. A listed binary still fails if any argument can make it exec or write | git rejects -c/--config-env/--exec-path/--upload-pack/--receive-pack/--ext-diff/-C/--output/--textconv, and git branch must be bare or all-listing-flags (GIT_BRANCH_READ_FLAG, :269) so git branch -D is denied; rg/grep reject --pre/--pre-glob/--hostname-bin/--search-zip/-z; find rejects -exec/-execdir/-ok/-okdir/-delete/-fprint/-fprint0/-fprintf/-fls; node passes only as node --check <file>; gh read verbs are POSITIONAL, gh <group> <verb> or gh <verb> only, because scanning the whole argv let a VALUE launder a write verb - gh issue comment 1 --body list used to pass (vetGh, :283-288) |
Close the four bypasses a listed binary still had | A "read-only" allowlist that writes a file, deletes a branch or posts a comment | yes |
| Self-exempt off-switch | same install | project | PreToolUse matcher * |
One exact command shape passes at every level. No &&, no $, no env prefix, no -e |
node <ABS>/.claude/brewtools/manager/manager-state.mjs set hard=false |
You can always leave the wall | Being locked in with no exit; also an arbitrary-code-execution hole an earlier substring anchor allowed | yes |
| Automatic manager injection while the wall is on | side effect of state.hard === true |
project | UserPromptSubmit |
Every turn while the wall is up, with no codeword typed | Manager HARD wall is ON - operate as orchestrator (delegate everything): plus the whole ++m block |
Keep the role text present while the wall is up | The wall denies tools while the model has no idea why | yes |
| One state reader behind all three wall mechanisms | ships with brewtools as brewtools/hooks/lib/manager-state.mjs |
project state, with a global twin for mode only |
none of its own - imported | Whenever session-start.mjs, manager-prompt.mjs or the installed hardmode-guard.mjs needs the state. All three import resolveState, so the notice, the auto-injection and the denial can never disagree about whether the wall is up |
SECURITY: hard and level come ONLY from the PROJECT state.json - a global state.json must never enable the HARD wall in projects without their own state (manager-state.mjs:127-129) |
One resolver, one answer, and a wall that cannot be armed from ~/.claude |
Three hooks each re-reading the file with their own defaults; a global setting silently arming every repo you open | yes |
| Codeword text resolved project -> global -> plugin | brewtools/hooks/lib/manager-prompts.mjs |
plugin default, overridable at project or global |
none of its own - imported by manager-prompt.mjs |
On every codeword hit. <repo>/.claude/brewtools/manager/prompts/<mode>.md, then ~/.claude/manager/prompts/<mode>.md, then <pluginRoot>/skills/manager-setup/references/<mode>.md. Only the text inside fenced blocks is injected; a file with no fence is used whole |
Injected text = inside fenced ... blocks if present (concatenated in order), else the whole file (raw-text fallback) |
Retext a codeword for one repo, or for every repo, without touching the plugin | An edited plugin file lost on the next update; and there is no global wall, but there is a global text override | yes when overridden |
3. Subagent control
Three opt-in setups. None is registered in brewtools/hooks/hooks.json - installing the plugin does nothing
until you run the skill, which delegates the file work to brewcode:hook-creator following an
assets/INSTALL.md runbook. They work together, each at its own moment: the router at spawn, the return
contract at SubagentStart, the deadline all the way through, and the return guard plus the deadline cleanup
at SubagentStop. Sharing the SubagentStop event is safe, because the cleanup hook always returns {} and
never asks for a decision.
| Approach | Installed by | Scope | Files | Trigger | Fires when | Excerpt | Config and defaults | Off switch | Purpose | Problem solved |
|---|---|---|---|---|---|---|---|---|---|---|
| Return contract announced at spawn | /brewtools:agent-return-setup |
choice: project, global or both | hooks/agent-return-contract.mjs + shared hooks/agent-return-budget.mjs |
SubagentStart, timeout 5 s |
The moment any subagent is spawned, before it does anything. Same text for every agent type | RETURN CONTRACT ... Verdict first, <=30 lines, \path:line`. !=bodies/output/log/preamble. Over ~1000 tokens your return is blocked for compression; over ~2500 write the detail to ... and return that path + verdict + <=3 lines.` |
.claude/agent-return.json, project wins over global, found by walking up at most 16 directories. Each threshold resolves in this order: config key, then env var (AGENT_RETURN_PASS / AGENT_RETURN_FILE), then the built-in passTokens 1000 / fileTokens 2500 (agent-return-budget.mjs:113, :115). The announced numbers are the resolved ones, so what is announced is what is enforced |
Live config flag: enabled === true only; absent key or file = OFF |
Put the return rule in front of the agent at the moment it applies | A prose rule in the preamble loses to whatever the agent just did |
| Return size budget enforced at return | same skill; all 3 files install as one unit | same | hooks/agent-return-guard.mjs |
SubagentStop, timeout 5 s |
When a subagent produces its final answer, just before it reaches the manager session | RETURN TOO LARGE (~N tokens, budget 1000). ... Re-send the SAME answer, compressed: keep the verdict line and every \path:line` ref, drop preamble, file bodies, command output, logs` |
t = ceil(len/4). <=1000 pass, 1000..2500 block for compression, >2500 block and order a report file. No LLM judge, two integer comparisons |
Same flag; blocks at most once via stop_hook_active |
Cap the single biggest context cost in a manager session | Ten subagents each dump their full output into the main session, context fills and compacts. Measured over 80 real returns: p50 1404 est-tokens, max 7931, 58% of the total is overflow above 800 |
| Soft wall-clock deadline | /brewtools:agent-deadline-setup |
choice: project, global or both. Global costs ~58 ms per tool call everywhere | hooks/agent-deadline-guard.mjs |
PreToolUse matcher .*, timeout 5 s |
Every tool call, but acts only when the payload carries both agent_id and a non-empty agent_type. The clock starts at the agent's first tool call |
80%: N of your 20 minute time budget are used. Start wrapping up NOW. 100%: AGENT DEADLINE EXCEEDED ... The tool "X" is blocked and retrying it will fail again. Stop investigating |
.claude/agent-deadline.json: defaultMinutes 20, byAgentType {}, hardStopRatio 2. Not configurable: warn at 0.8 of the budget, repeat a notice at most once per 10% of the budget, prune state older than 24 h. Past 100% only the 7 advertised finalize tools plus TaskCreate, BashOutput, TaskOutput are allowed; AskUserQuestion is denied on purpose (agent-deadline-guard.mjs:77-79 - a subagent parked on a human is unbounded wall-clock. Belt and braces since 2.1.233: the tool is stripped from every subagent anyway, so that branch can no longer fire). Past hardStopRatio (2x the budget) the list shrinks to Write and Edit only |
Live config flag: enabled !== true = OFF |
Force a runaway agent to finalize instead of killing it | An agent runs an hour, unobservable and drifting. maxTurns would kill it and throw away the report. Time is sampled only at tool-call boundaries, so one 25-min Bash call is invisible |
| Deadline state cleanup | same skill, installed together | same | hooks/agent-deadline-cleanup.mjs |
SubagentStop, timeout 3 s |
When a subagent finishes for any reason | No text. Deletes <tmpdir>/brewtools-agent-deadline/<session>/<agent>.json, prunes dirs older than ~24 h, always returns {} |
State only: { start, warned, expired }, mode 0700, never under ~/.claude |
None - unconditional bookkeeping, harmless when the guard is off | Keep the tmp state tree self-cleaning | A stale start carries a used-up budget into a new agent id |
| Route a generic spawn to the real expert, tier 1 | /brewtools:agent-router-setup |
project only - the roster is per project, no scope question | .claude/hooks/agent-router.mjs |
PreToolUse matcher Agent, timeout 5 s |
Every main-loop agent spawn; an omitted subagent_type is normalized to general-purpose before the project-agent and generic checks run, so it is policed too, per the Agent tool contract. Exits instantly if agent_id is present, if the picked type is a project agent, is in neverFlag, or is not in genericTypes |
agent-router: this looks like skill authoring - 'brewcode:skill-creator' is the expert for it, not general-purpose - retry with subagent_type: brewcode:skill-creator. (Deliberate? retry once and it passes.) |
.claude/brewtools/agent-router.json: level fast, genericTypes ["general-purpose","worker"], neverFlag 8 entries, minScore 3, margin 2, 4 built-in intents. Anti-loop: one deny per session+root+task. status staleness is content_version-based: it compares the installed hook's header against the plugin template, and the config's content_version against assets/INSTALL.md's header - version/plugin fields are informational provenance only, never deciding staleness |
Live flag, inverted: only enabled === false disables. Missing file = ON with defaults and still effective; stale=yes stays effective too, just running old logic. Re-install is idempotent but not inert: it re-copies agent-router.mjs, which is what repairs a stale hook body |
Stop the main loop reaching for general-purpose when a hand-written expert exists |
The repo ships a domain expert and the manager spawns general-purpose out of habit. The deny reaches the model as a tool error, so a retry always passes |
| LLM judge for ambiguous spawns, tier 2 | /brewtools:agent-router-setup level strict |
project only; the judge prompt is inlined into settings.json, never copied |
a second .claude/settings.json entry |
PreToolUse matcher Agent, timeout 30 s |
Every Agent spawn once installed. Hooks run in parallel, so tier 1 cannot gate it |
Bias to {"ok": true} on ANY doubt. A wrong redirect costs the user a full wasted agent run against an ill-fitting expert; a missed redirect costs nothing |
Wired as type: "agent", model claude-haiku-4-5-20251001, timeout: 30 - Claude Code hook timeouts are in seconds, so that is 30 s, not 30 ms (agent-router-setup/assets/INSTALL.md:264, :296, :386). level in the config is only a record of what was wired; editing it by hand changes nothing |
Re-run at level fast. Setting enabled:false does NOT stop tier 2 costing a model call |
Catch domain fits too indirect for a regex | Tier 1 matches trigger words, not meaning |
Known limits: tier 2 is wired but never verified end to end; the return guard blocks at most once, so a compressed second answer can land slightly over budget (observed 1417 -> 1026 against 1000); the deadline samples time only at tool-call boundaries.
A subagent cannot ask: the ## APPROVAL REQUIRED envelope
Under CC 2.1.233 AskUserQuestion is removed from every subagent by filter 1, even when the agent's
tools: frontmatter lists it (docs/sub-agents.md:337, :340; forks are the sole exemption). Declaring
it is therefore silent and inert, not an error - which is exactly what made it dangerous: an agent body
saying "confirm with the user" would simply never confirm. A skill keeps the tool; only subagents lost
it, so the prompt contract's own AskUserQuestion rules in section 8 stand unchanged.
The replacement is text, not a hook: the agent finishes every non-destructive step, then ends its final return with an envelope the CALLER presents.
| Rule | Where | What it looks like | Purpose | Problem solved |
|---|---|---|---|---|
One envelope per destructive operation, ids A1..AN |
## APPROVAL REQUIRED block, last section of the return |
COMMAND: / HOST: / EFFECT: / ROLLBACK: / EVIDENCE: / PRECONDITION:, exactly those six fields (brewtools/agents/ssh-admin.md:73-78) |
The caller can approve one operation without approving the run | A subagent that "helpfully" ran the migration it meant to ask about |
| Nothing to approve is stated, never implied | same block | the literal line APPROVAL REQUIRED: none - "Silence is never a gate" (deploy-admin-agent.md.template:52) |
An empty section is a claim, not an omission | A missing block read as "nothing dangerous happened" |
| Approval arrives as a token in the next spawn | the caller re-spawns | APPROVED: A1 A3 in the prompt; An explicit approval token in the incoming prompt is the ONLY authorization this agent may act on, covering only the ids it names, exactly as worded - not a similar command, not a broader scope, not a retry with different arguments (ssh-admin.md:86-88) |
Approval is scoped to the exact bytes shown to the human | An approved restart nginx reused to authorize restart everything |
| Destructive is enumerated, not judged | same contract | rm/mv over existing paths, force-push, tag delete, DB writes/migrations, service restart/stop, firewall/user/permission changes, secret rotation, deploy/rollback, docker system prune, any remote ssh mutation |
The agent does not get to decide what counts | An agent classifying its own risky step as routine |
| The gate is not delegable | brewtools/skills/{ssh,deploy}/SKILL.md |
Safety gates are NOT delegable ... So confirmation gates stay in THIS skill, in the main conversation (deploy/SKILL.md:126-129) |
The one context that still has AskUserQuestion owns the gate |
A skill delegating its confirmation step into a subagent that cannot confirm |
Byte-identical across seven surfaces, which is the point - the field set is the interoperability contract
between whoever writes the envelope and whoever reads it: brewtools/skills/ssh/SKILL.md:453,
brewtools/skills/deploy/SKILL.md:133, ssh/templates/ssh-admin-agent.md.template:81,
deploy/templates/deploy-admin-agent.md.template:42, ssh/references/safety-rules.md:42,
brewtools/agents/ssh-admin.md:70, brewtools/agents/deploy-admin.md:87. The one asymmetry is the
HOST: value: <server alias / user@host> for ssh, <local | user@host> for deploy.
4. Context economy
What keeps the main context small. The return budget (section 3) saves the most on its own, so it is not repeated here.
| Approach | Installed by | Scope | Trigger | Fires when | Excerpt | Off switch | Purpose | Problem solved | Project-specific |
|---|---|---|---|---|---|---|---|---|---|
| think-short at session open | /brewtools:think-short-setup install |
choice: project or global, one target per run | SessionStart |
Every session start or resume; also resets the per-session counter and prunes markers older than ~1 day | Be terse. Results first, no preamble/filler/sycophancy. ASCII only. ... Grep before Read. Edit over Write. Parallel calls in one message. ... Keep code simple - do not over-engineer. ... find the closest well-built counterpart in the repo |
disable renames think-short-prompt.md to .disabled; hooks stay wired and no-op |
Set output style and coding taste for the whole session | Verbose, sycophantic, over-commented output; serial tool calls | no |
| think-short reminder every 10th prompt | same | same | UserPromptSubmit |
Prompts 10, 20, 30 ... of a session. const INTERVAL = 10, never the first |
The same text, re-injected verbatim | same rename switch | Re-anchor terseness after the opening directive is buried | Terseness decays as the session grows | no |
| think-short into every subagent | same | same | SubagentStart |
Every subagent spawn. Delivers the directive as hookSpecificOutput.additionalContext |
The same body, unmodified | same rename switch; additionalContext accumulates across hooks, so no coexistence/yield logic is needed |
Give subagents the same output contract | A subagent inherits no parent context and returns a wall of prose | no |
| Pinned semantic-search MCP | /brewcode:semble-setup install |
MCP at user scope (semble[mcp]==0.5.4, alwaysLoad: true); everything else project |
user runs the skill | The MCP is registered once, into ~/.claude.json, and is then available in every project on the machine - but only from a NEW session. A fresh registration is invisible to the session that made it, so install stops at a reload checkpoint and finishes via resume |
Exposes mcp__semble_code__search and mcp__semble_code__find_related |
disable sets state.enabled=false; nothing is deleted, all hooks read the flag |
Make semantic search available before a grep habit forms | Grep fails on behaviour and intent questions where the wording is absent from the code | yes for project state; the MCP is machine-wide |
| semble-first rule and CLAUDE.md block | same | project | context auto-load | Every request, as part of the always-loaded instructions. Two files: the full table lives in .claude/rules/semble-first.md, and a 6-line summary is written into <repo>/CLAUDE.md between the literal markers <!-- BEGIN brewcode:semble --> and <!-- END brewcode:semble -->. Re-install replaces the marked range in place and never appends a second block; a BEGIN without its END reports malformed marker block and changes nothing |
Semantic search first: ONE mcp__semble_code__search with repo = absolute project root, top_k=5, max_snippet_lines=10 - then open the hit at start_line. rg/Grep stays for exact identifiers, regexes, paths and exhaustive enumeration. plus top-k is a ranked sample, not a list: "every/all" is unanswerable in principle. |
uninstall/purge remove the text; disable leaves it and only silences the hooks |
Teach the tool split once, in the always-loaded layer | The same question searched twice - semble then an equivalent rg - and semble used for enumeration it cannot do |
yes, text is generic |
| semble session directive | same | project | SessionStart, timeout 5 s |
Session start, only when .claude/semble/state.json phase is ready or awaiting_reload |
semble: use ONE mcp__semble_code__search first (repo=<cwd>, top_k=5, max_snippet_lines=10), then open the hit at start_line. |
enabled flag |
Restate the contract with the real repo path filled in | The model calls the MCP without repo, which is required and never inferred |
yes |
semble wrong-tool nudge - semble-reminder.mjs |
same | project | PreToolUse matcher Bash|Grep |
Just before a shell or grep search that looks like a behaviour question. Suppressed for identifiers, short patterns, regex metacharacters, paths, enumeration words (every, all, how many, list the), find and pipes. Only calls that pass that gate advance a counter in .claude/semble/reminder.json; it injects when count % reminderEvery === 0, and state.reminderEvery defaults to 1, so by default the gate is the only volume control. A corrupt or absent counter resets to 0 |
semble: wrong tool. mcp__semble_code__search repo="<cwd>" first. (first call builds the index; exact/-l stays rg) |
enabled flag |
Catch the habit at the moment it fires | Reaching for grep on a question grep cannot answer | yes |
| semble subagent nudge | same | project | SubagentStart, unmatched |
Every subagent spawn, no throttle | semble: mcp__semble_code__search is already available to you - no ToolSearch needed. ... Use rg only for exact identifiers, literal strings and exhaustive enumeration. |
enabled flag |
Tell a context-less subagent the tool exists and is not deferred behind ToolSearch | A subagent greps the repo because it never saw the rule | yes |
| semble prefetch - result, not advice | same | project | UserPromptSubmit |
A typed prompt of 30 to 2000 chars (semble-prefetch.mjs:213, :214) with question intent and a domain or repo reference. THROTTLE_MS 30 s (:116), SEARCH_TIMEOUT_MS 3 s (:133), TOP_K 3 (:134), COOLDOWN_MS 10 min after a failure (:122) and TIMEOUT_COOLDOWN_MS 1 min after a timeout (:131). Silent on a cold index |
These candidate locations were ranked for the question above before you started: then top 3 path:line, then Open the candidates that look right BEFORE running any search of your own |
enabled flag, fail-open on every error path |
Run the search for the model and hand over ranked paths | The model burns turns rediscovering locations a 3-second index lookup already knows | yes |
| semble agent migration | same, semble-agents.sh apply |
project .claude/agents/**/*.md |
one-off at install, upgrade or resume | An agent declares a tools: allowlist that lacks the two MCP tool names |
Appends mcp__semble_code__search, mcp__semble_code__find_related to tools: in whatever list form it uses |
--revert; wildcards and disallowedTools conflicts are skipped |
Make sure every project agent can call the tool the rule names | A rule ordering semble-first inside an agent with no permission to call it | yes |
semble telemetry - semble-stats.mjs |
same | project | PostToolUse and PostToolUseFailure, both with matcher mcp__semble_code__search|mcp__semble_code__find_related|Bash|Grep|Glob|Read |
After every matching tool call, whether it succeeded or failed. One file registered twice, which is why 5 semble hook files produce 6 settings entries. It is the only hook in the whole suite on PostToolUseFailure - a search that errored is still a data point about which tool was reached for |
None - pure observer, always {}. Log at .claude/semble/telemetry.jsonl, trimmed at 2 MB |
enabled flag |
Measure whether semble calls actually displaced grep-shaped calls | Adoption claims with no numbers behind them | yes |
| Session learnings compacted into the rule layer | /brewcode:rules |
project .claude/rules/ only, never ~/.claude/rules/ |
user runs the skill | You run it after a session that produced a lesson worth keeping. It syncs KNOWLEDGE.jsonl or the session's learnings into avoid.md, best-practice.md and {prefix}-avoid.md / {prefix}-best-practice.md for one slice of the repo |
Claude Code auto-loads .claude/rules/*.md into every session, so entries must be table rows, not prose (rules/SKILL.md:189) |
not a -setup skill: delete or trim the rule file |
Grow the always-loaded layer in the cheapest shape there is | A lesson learned twice; a rule file that turns into an essay and costs every request | yes |
| Etalon and convention extraction | /brewcode:convention |
project | user runs the skill | You run it on a repo whose patterns are not written down. Default mode full; mode rules writes only into .claude/rules/ |
argument-hint: "[prompt] [full|conventions|rules|paths <p1,p2>]" |
same - delete what it wrote | Name the repo's real etalon classes and patterns once, in the layer every session already reads | Every agent re-deriving the house style from scratch, and each one deriving it differently | yes |
The order these arrive in a session: think-short comes first, at session open, and sets the tone of the output. semble-first comes next and decides how the session looks things up. The manager text comes last, once per turn, and turns the session from a worker into a coordinator.
5. Review and anti-drift
| Approach | Installed by | Scope | Files | When it runs | Excerpt | Purpose | Problem solved | Project-tailored |
|---|---|---|---|---|---|---|---|---|
| Two-axis review: MODE x DEPTH | /brewcode:superreview-setup |
project only, no global option | .claude/skills/superreview/SKILL.md |
Every run of the emitted /superreview |
{MODE} selects SCOPE (FULL_PROJECT / EXPLICIT / UNCOMMITTED / LAST_COMMITS). {DEPTH} selects EFFORT ... inferred SEMANTICALLY from the user's prompt ... no --fast, no flag |
Scope and effort decided before any agent is spawned | A full fan-out burned on a two-line diff; a flag nobody remembers to type | mechanism fixed, gates tailored |
QUICK depth, the default |
same | project | same SKILL.md |
Default of every run unless the prompt asks for depth | QUICK (the DEFAULT and common case - mechanical gates + the intent pass, ONE spawn, no domain experts) |
Cheap review that still answers "did I build what was asked" | A review so expensive nobody runs it | fixed shape |
EXTENDED depth, full fan-out |
same | project | SKILL.md + references/scope.md, references/agent-prompt.md |
Only when the prompt asks for depth or expertise, in any language | ONE parallel message ... intent-guard + domain experts + scope pass A ... + scope pass B ...; shared JSON finding contract |
Route each changed-file group to the agent that owns that domain | "a review routed to generic agents finds generic issues" | expert roster resolved at runtime from live .claude/agents/*.md |
| Mechanical gates as ground truth | same | project | SKILL.md step 1 |
First step of both depths | Run the MECHANICAL GATES (build/lint/test) - execution output is CONFIRMED-BY-EXECUTION, the ONE verdict needing no adversarial pass |
Run the build before arguing about the code | Models discussing the quality of code that does not compile | gate commands discovered per project |
| Adversarial per-finding validation | same | project | SKILL.md Phase 3 |
EXTENDED only, after the fan-out |
spawn ONE validator that independently RE-VERIFIES EVERY candidate finding in reverse ... Per-finding gate, NOT a sample plus an agent may not validate findings from the group IT reviewed in Phase 2 |
Every claim re-checked by someone who did not make it | A reviewer agreeing with itself; false positives shipped as findings | validator and arbiter are project agents |
| Verdict ladder | same | project | SKILL.md verdict rules |
Both depths | mechanical gate output carries CONFIRMED-BY-EXECUTION ... an intent-guard row carries CONFIRMED-BY-EVIDENCE ... A finding that could not be validated carries verdict: UNVALIDATED - claiming nothing |
Three named sources of truth; anything else is marked unproven | Confident-sounding findings with no evidence; an incomplete run passing as complete | fixed |
| Scope discipline against a sanctioned baseline | same | project | references/scope.md |
EXTENDED only |
Resolve the SANCTIONED SCOPE baseline (task + issue + recorded decisions); audit scope creep / blast radius / under-delivery - an unsanctioned touch is a first-class finding |
Measure the diff against what was sanctioned | Quiet scope creep and quiet under-delivery both passing review | wired to the real tracker; no baseline -> UNKNOWN and a permanent P2 cap |
intent-guard anti-drift agent |
written by /brewcode:superreview-setup generate.sh emit-agent; /brewcode:teams-setup calls the same writer |
project | .claude/agents/intent-guard.md |
Both superreview depths, unconditionally. At QUICK it IS the review. In a team: by explicit name only, never during development |
Drift starts small at the first turn and is large by the last one. A model can follow an approved plan faithfully for hours and still deliver something the requester did not ask for. |
One read-only pass: was the DELIVERED thing the ASKED thing | The implementation quietly stops serving the goal you asked for | frontmatter fixed; invariants, drift examples and evidence commands tailored |
| Tiered sources of truth for intent | same | project | intent-guard.md sec. 1 |
Every intent pass | Highest tier wins EVERY contradiction ... Tier 1 may not exist. The user often just typed the task in chat. That is NORMAL ... treat the verbatim request as tier 1 ... Never invent a source |
Rank ticket > spec > plan > policy > transcript | A stale plan file outranking what the user said five minutes ago | tier locations are project scalars, none allowed |
| Cheap-evidence budget | same | project | intent-guard.md sec. 2 |
Every intent pass | Hard budget: <= 15 tool calls, <= 10 minutes. You are a smell test, not an audit. plus A claim you cannot back with a name, a path, a count or a one-line peek is not a finding - drop it. |
Keep the drift pass cheap enough to run every time | An anti-drift check that costs as much as the review and gets switched off | evidence commands tailored to the stack |
| One writer, reuse wins | same | project | .claude/agents/intent-guard.md |
Every emit, and whenever teams-setup builds a team | a USABLE file already exists -> the writer prints INTENT_GUARD: REUSE <path> and leaves it BYTE-UNTOUCHED ... Do not "refresh" it, do not diff-merge it |
Two generators, one file, no overwrite of tuning | A second skill silently rewriting a hand-tuned agent | reuse, migrate or create per project state |
| Dynamic teams of domain agents | /brewcode:teams-setup |
project | .claude/teams/{TEAM}/team.md, trace.jsonl, one .claude/agents/{name}.md per member |
On install or upgrade; members then work as normal project agents | For each agent, spawn Task(subagent_type="brewcode:agent-creator") - ONE agent file per spawn, never "create the whole team" in one task |
A roster of narrow domain owners instead of one generalist | Overlapping vague agents that all answer the same way | roster and models derived from project analysis; size chosen by the user |
| Append-only team trace with a read cursor | /brewcode:teams-setup |
project | .claude/teams/{TEAM}/trace.jsonl, trace.cursor, trace-archive.jsonl, all through scripts/trace-ops.sh |
A member appends one line per event with trace-ops.sh add; only three kinds are accepted, each with its own closed vocabulary - track (took|refused|completed|failed), issue (low|medium|high|critical), insight (pattern|architecture|performance|security|convention|debt). status reads back everything after the cursor and then sets the cursor to now; cleanup moves old lines into trace-archive.jsonl and resets the cursor |
one line is {"ts":...,"sid":...,"src":...,"k":"track","s":"completed","txt":...}; If cursor exists and <10 post-cursor entries: expand to last 30 days |
A cheap shared ledger the whole team writes to and the status pass reads once | Team history living in chat scrollback; a status pass re-reading the same entries every run | roster is per project; the record shape is fixed |
| Quorum consensus on generated agents | /brewcode:teams-setup |
project | gates writes into .claude/agents/*.md |
Phases C5-C7, right after the roster is written | Quorum threshold: 2/3 agreement = confirmed. Match criteria: same file + same area (+/- 5 lines or same section) + same category |
Three independent reviewers, only agreed findings get fixed, then a fourth verifies | One reviewer's opinion or hallucination rewriting the roster | thresholds fixed; reviewers are the project's own agents |
| Reviewer-role exclusions | /brewcode:teams-setup |
project | - | C5/C7/C9 and C8/U4 | intent-guard is never the REVIEWER. It is not a general reviewer: it only compares asked-vs-delivered on a real delivery, and it has no code domain. |
Keep the drift checker out of code review and implementation | One agent doing the work and its own oversight | fixed |
| Report artefact convention | /brewcode:superreview-setup |
project | .claude/reports/{TIMESTAMP}_superreview/REPORT.md |
End of every run | ONE consolidated, validated, P0->P3-sorted report - the skill recommends fixes, never edits |
One timestamped ranked artefact per run | Findings scattered across chat scrollback | fixed path shape |
| Emitted-skill self-correction | /brewcode:superreview-setup |
project | edits .claude/skills/superreview/SKILL.md and references/scope.md in place |
Phase 4b, EXTENDED only, after the report |
corrects the emitted SKILL.md + references/scope.md IN PLACE ... a gate that reported not run because the command does not exist ... Line delta <= 0, facts only |
The review skill fixes its own stale facts | A generated skill routing to agents that no longer exist | yes by definition |
| Workspace-local review skill | none - hand-maintained, not shipped | this repo only: .claude/skills/brewcode-review/ |
SKILL.md, references/agent-prompt.md, references/report-template.md |
User types /brewcode-review; default -q 3-2 |
if unique_agents >= M: # Quorum threshold, then Single reviewer (Opus) verifies ALL confirmed findings, optional critic Find what ALL reviewers MISSED. |
Quorum, then double-check, then an optional devil's advocate | Single-reviewer bias; unverified findings; blind spots | fully adapted to this repo, hand-written |
The anti-drift chain, in order. First, the 6-field spawn brief (section 2) writes the goal down before any
subagent starts. Then intent-guard compares what was asked against what was delivered and reports
ALIGNED / MINOR DRIFT / MAJOR DRIFT, quoting the exact words it judged against. Finally a second
opinion - either a vote or a separate validator - keeps that check itself from being wrong. Ownership:
teams-setup owns who is on the team, superreview-setup owns the intent-guard.md file.
The three second-opinion styles are not the same thing. Teams uses a fixed 2 out of 3 vote by 3 reviewers.
brewcode-review uses a vote you configure yourself (N agents, M of them must agree). superreview uses no vote
at all: one validator re-checks each finding, and it may never validate findings from the group it reviewed
itself. intent-guard rows skip that validator on purpose - they already carry their own evidence.
6. Project generators
A generator skill does not do the work. It reads the target repo, writes project-local skills, agents, rules, hooks and state, and is then idle. You run what it produced.
| Generator | Scope | Generates | Kind | Project-tailored | Off switch |
|---|---|---|---|---|---|
/brewtools:task-board-setup |
project; a path-like first token retargets it, else cwd | .claude/agents/task-tracker.md; .claude/skills/task-board/SKILL.md; .claude/rules/tasks.md; .claude/features/{board,PROGRESS,TRACKER,TASK_TEMPLATE,INDEX}.md + 5 subdirs; SPEC_MODE also .claude/skills/task-spec/SKILL.md + 2 spec templates |
agent + 1-2 skills + rule + state dir, no hooks | yes - ~26 gated placeholders and 2 on/off arm pairs from a multi-agent repo scan | entry-file parking: renames the 4 machinery entry files to <name>.disabled, bodies byte-identical, .claude/features/** untouched |
/brewdoc:docsync-setup |
project only | .claude/hooks/docsync-{track,watch,gate}.mjs; .claude/docsync/config.json; .claude/docsync/state.json; 3 entries merged into .claude/settings.json |
3 hooks + config + state | partly - hook code is a byte-copy; only threshold_days and exclude are per project |
live config flag enabled. Absent key = ENABLED (c.enabled !== false), re-read every invocation |
/brewdoc:memory-sync-setup |
project only | .claude/skills/memory-sync/SKILL.md + references/{memory-guide,agent-audit,hard-sync}.md. Nothing else |
1 skill, 4 files | yes - 17 detected aspects feed 8 single-line values and 12 block placeholders | entry-file parking via generate.sh enable|disable: SKILL.md <-> SKILL.md.disabled |
/brewcode:e2e install |
project only | .claude/agents/e2e-*.md - a fixed 5-member roster (architect, scenario-analyst, automation-tester, manual-tester, reviewer); .claude/e2e/e2e-rules.md and .claude/e2e/config.json; optionally .claude/rules/e2e-conventions.md |
5 agents + rules + config + one optional auto-loaded rule, no hooks | yes - the roster is fixed, but the rules are merged from a 3-5 agent repo scan plus a web pass, and the rule export is scoped paths: ["{config.testSourceDir}/**"] |
none. Its modes are status|install|create|update|review|rules - not a -setup skill, so there is no enable/disable and no parking; you delete the agents by hand |
/brewtools:ssh setup |
project, plus a server row in the repo's git-ignored CLAUDE.local.md |
.claude/agents/ssh-admin.md from templates/ssh-admin-agent.md.template; a server row in CLAUDE.local.md; a CLAUDE.local.md line in .gitignore |
1 agent + a git-ignored inventory, no hooks | yes - {{SERVER_INVENTORY}} and {{SERVER_DETAILS}} come from a live SSH discovery run against each server |
none. Delete the file; update-agent rewrites it in place, at most 3 servers per run |
/brewtools:deploy setup |
project, plus a GitHub block in CLAUDE.local.md |
.claude/agents/deploy-admin.md from templates/deploy-admin-agent.md.template; owner, repo, registry and the workflow inventory into CLAUDE.local.md; the same .gitignore line |
1 agent + a git-ignored config, no hooks | yes - {{GITHUB_CONFIG}}, {{WORKFLOW_INVENTORY}}, {{SERVER_TARGETS}} and {{SECRETS_LIST}} come from a live gh probe of the repo |
none. Delete the file; update-agent rewrites it |
/brewtools:provider-switch install |
outside .claude/ entirely - one managed block in ~/.zshrc |
export <PRV>_API_KEY=... lines and one claude<name> alias per provider, between # ========== Claude Code Provider Aliases ========== and its End marker |
shell config only: no agent, no skill, no hook, and no settings.json entry at all |
yes per provider - endpoint, key variable and model ids from references/{deepseek,zai-glm,qwen-dashscope,minimax,openrouter}.md |
write-alias.sh remove-key / remove-alias. The env vars only live in the shell that ran the alias, so a new terminal is already back on the Anthropic subscription |
/brewcode:setup-status |
reads the cwd project plus the ~/.claude twins |
nothing - allowed-tools has no Write, no Edit, no Agent |
read-only report over 11 setup rows | n/a | n/a - not a -setup skill |
Three of these do not fit the "project-local" shape. ssh and deploy also append to CLAUDE.local.md,
which holds hosts and repo names, and they add that file to .gitignore before writing anything into it.
provider-switch writes nothing under .claude/ at all: its whole output is a marked block in ~/.zshrc.
Those three are rows five, six and seven of the table above.
The approaches living inside what they generate:
| Approach | From | Lives in | When | Excerpt | Purpose | Problem solved |
|---|---|---|---|---|---|---|
| Board is canonical, updated in the same change | task-board | .claude/agents/task-tracker.md |
Every tracker spawn | BRD is canonical task LIST + status. Update BRD in SAME change as ANY transition. Lagging BRD = wrong BRD. |
One file is the single truth for what exists and where | A board describing a state the repo left days ago |
| Tracker writes only the board | task-board | same | Every tracker spawn | write ONLY .claude/features/**. !=touch app code. |
Bookkeeping cannot ship code | A tracking run silently editing production files |
| Claim the board before any work | task-board | .claude/rules/tasks.md rule 8 |
When work touches .claude/features/** |
At the START of ANY task, run the task-tracker agent in ISOLATION (a spawned subagent via Task, NOT inlined) to claim/sync the board |
Every task starts from a synced board, in its own context | Two workers on one task; board drift; tracker chatter in the main context |
paths:-scoped rule instead of a hook |
task-board | .claude/rules/tasks.md frontmatter paths: [".claude/features/**"] |
Only when such a file is in play | That is why the session-progress contract is a rule section and NOT a new hook -- this generator installs no hooks. |
Inject the contract exactly when relevant | Permanent token cost, and a hook firing in sessions with no board work |
| Session snapshot with a size cap | task-board | .claude/features/PROGRESS.md |
Each session, on transitions | Cost cap: ~8 lines. !=grow it, !=turn it into a log |
Cheap handover between sessions | PROGRESS.md becoming an unbounded append-only log |
| Spec triage gate | task-board | tracker agent in SPEC_MODE, task-spec skill, gates G1-G5 |
On intake and on move-to-progress | NEXT: run /task-spec <ID> (spec required: <reason>) - a REPORT LINE, not a call: an agent cannot invoke a skill on behalf of the main session |
Large or risky tasks get a written spec first | Coding straight from a one-line title; an agent pretending it can call a slash command |
| Design is never authored solo | task-board | .claude/features/TRACKER.md s.10.3 |
When a design doc is produced | order: domain agent -> architecture agent -> built-in Plan; the agent used must be named in ## Evidence |
Second opinion is structural | Single-model design with no recorded provenance |
| Pointer-only agent returns | task-board | tracker agent | End of every run | return only that: verdict + task ids + file:line pointers. !=paste BRD |
Keep the parent context small | A subagent dumping the whole board back into the conversation |
| Machinery vs data on removal | task-board | PR phase of the generator | uninstall vs purge |
the generated agent/skills/rule are MACHINERY, .claude/features/** is the user's DATA ... only purge deletes the tasks. |
Uninstall is reversible | Losing every task ever written by typing "uninstall" |
| Undated docs flagged at write time | docsync | docsync-track.mjs |
PostToolUse on Write|Edit|MultiEdit of a .md |
docsync: ${rel} has no last_updated frontmatter. Add ... last_updated: "${today()}" (quoted - unquoted a real YAML parser types it as a Date) |
Every doc carries a date | Docs with no way to tell whether they are current |
| Silent read tracking | docsync | docsync-watch.mjs |
PostToolUse on Read of a .md |
records the path into state.json, emits nothing |
Build the session's doc set without noise | Nagging on files you only looked at |
| One end-of-turn staleness block | docsync | docsync-gate.mjs |
Stop |
decision: 'block' with Ask the user via AskUserQuestion whether to sync now; ... do NOT sync without confirmation. This is the only docsync block this session. |
Surface stale docs at the natural pause | Silent doc rot; unattended edits to your docs |
Once-per-session asked latch |
docsync | docsync-gate.mjs + state.json |
Same Stop hook | The asked flag prevents an infinite Stop loop, so this is at most ONE block per session |
A Stop hook that blocks Stop must stop blocking | Infinite Stop loop; repeated nagging |
| Scope re-applied at gate time | docsync | docsync-gate.mjs |
Same Stop hook | exclude globs and doc_type may have changed AFTER the file was recorded |
Mid-session exclusions take effect | Being blocked over a doc you just marked skip |
| Date-only staleness | docsync | config.json threshold_days |
Every gate evaluation | Staleness is DATE ONLY, in LOCAL time: today - last_updated > threshold_days. No hash, no deps. |
One cheap, explainable rule | Hash and dependency graphs that go wrong quietly |
| Settings merge that refuses to clobber | docsync | install step | Install and upgrade | python3 preferred, jq fallback, .bak first, abort rather than overwrite invalid JSON |
Safe registration into a shared file | Destroying a hand-written settings.json |
| Every generated hook inlines its own project root | docsync, agent-return, agent-router | docsync-track.mjs:32, -watch.mjs:34, -gate.mjs:38, agent-return-budget.mjs:52, agent-router.mjs:214 |
Every invocation, before any config or state read | the canonical recipe of hook-creator.md:373-396, byte-identical per file: CLAUDE_PROJECT_DIR -> upward walk for a root marker -> hook cwd. Never throws. |
A generated hook lands alone in someone else's repo with no shared lib to import | A hook resolving config against a drifted cwd, or crashing because an import path did not survive the copy |
| Sync auto-loaded context, not docs | memory-sync | emitted SKILL.md |
Every /memory-sync |
Documentation (docs/**) | Owned by a SEPARATE doc flow. Refs INTO docs are checked for RESOLUTION only; contents are never read-for-edit and NEVER edited |
Keep the memory layer true to the code | Two flows fighting over the same docs |
| Non-growth prime directive | memory-sync | emitted SKILL.md |
Every run | After the sweep EVERY file is <= its original line count, and the TOTAL delta is <= 0. A positive delta must be stated FIRST in the report |
Memory files shrink or hold | CLAUDE.md growing every session until it eats the context window |
| Scope ladder | memory-sync | emitted argument-hint |
Per run | [prompt] [session|branch|commit <sha>|recent[:N]|all] [hard] [free-form focus] |
Match the sweep to what changed | A full-repo re-audit for a two-file change |
hard depth = paths: audit + obvious-knowledge purge |
memory-sync | emitted SKILL.md PASS A/B |
/memory-sync ... hard |
the ONE authorized growth is the PASS A frontmatter paths: repair at HARD depth on a MISSING or TOO_NARROW verdict |
Fix rule targeting; delete what a model already knows | Rules that never load; memory restating common knowledge |
| Batch agents may not widen scope | memory-sync | emitted SKILL.md |
Each sync batch | A batch agent that "helpfully" widens the scope has broken the skill |
The coordinator owns scope | Fan-out that quietly becomes a whole-repo rewrite |
| Independent verify plus self-sync | memory-sync | emitted SKILL.md phases |
End of each run | per-file wc -l against the recorded baseline, then the skill syncs its own files |
Checked by someone other than the writer | A writer grading its own homework; a stale sync skill |
| Propose, never auto-create | memory-sync | PROPOSE phase | End of each run | new memory files are proposed, not written | You own your memory layer | A sync run inventing new always-loaded files |
| Emit manifest is the removal manifest | memory-sync | scripts/generate.sh |
uninstall |
emit writes exactly SKILL.md + $EMITTED_REFS, so that manifest is also the removal manifest |
Clean removal that respects hand-added files | Deleting files you put in the skill dir |
| Refuse to overwrite a live install | memory-sync | generate.sh emit |
Re-running install | emit never overwrites a live installation; MEMORY_SYNC_FORCE=1 overrides and DESTROYS hand-edits |
Regeneration is opt-in | Losing local edits to the emitted skill |
| Report, never run | setup-status | setup-status/SKILL.md |
/brewcode:setup-status |
This skill reports. The user runs each setup by hand, ideally one per fresh session. - no --run, no --fix |
One command tells you what to run | Chaining several interactive generators in one session and degrading all of them |
| Three-signal drift detection | setup-status | same | Same run | anchor artifact presence, stamped version/generated_by, and a cmp of byte-copied assets against the installed plugin |
Tell missing from disabled from partial from stale | "It is installed" when the files are two releases old |
| Parked is present, not missing | setup-status | Phase 1 probe | Same run | probe emits FILE/DIR/GLOB/PARK/MISS; PARK is present, not missing |
Disabled setups report disabled |
Reinstalling something you deliberately parked |
| Off-switch polarity read from the reader | setup-status | Phase 1b | Same run | for each row, resolve the absent-key meaning from the code that reads it, never a house default | The dashboard is right though polarity differs per setup | Reporting docsync, where absent means on, as disabled |
| A generated agent may not hold a plugin path | e2e | references/mode-install.md S6 |
Every install |
The generated .claude/agents/e2e-*.md are not plugin-owned, so no plugin path and no *_PLUGIN_ROOT variable resolves inside them; and an absolute cache path ... dies at the next plugin update because the version is in the path. |
Generated agents read repo-relative files only, so they survive an update, an uninstall, a clone and CI | An agent pointing into ~/.claude/plugins/cache/.../5.5.2/ that breaks on the next release |
| Refuse to write rather than stamp a fake version | e2e | SKILL.md Phase 0 and mode-install.md S6 |
Before any file is written | the script refuses to emit a fake version rather than let unknown reach a version: stamp, and If you are ever holding something that is not X.Y.Z -- unknown, an empty string, an unsubstituted {PLUGIN_VERSION} -- do NOT write the file. Stop and report it. |
A broken install fails loudly instead of writing a lie | Artefacts stamped unknown, which no staleness check can ever compare |
| Leftover-token gate after every templated write | ssh, deploy | SKILL.md install step, immediately after the Write |
Each agent generation and regeneration | one grep -nE over the written file for both brace families - the skill's own {{SERVER_INVENTORY}} style and the metadata {PLUGIN_VERSION} style - then STOP if ❌ -- re-substitute before continuing. |
Verify the output, not just the input | A shipped agent whose inventory section is still a literal {{SERVER_INVENTORY}} |
| A regeneration re-resolves the stamp | ssh, deploy | ssh/SKILL.md:499, deploy P2 step 8 |
update-agent |
Re-resolve {PLUGIN_VERSION} and {LAST_UPDATED} exactly as in Install Step 3 -- a regeneration is a new write, so the stamp is refreshed, never carried over. |
The date on the file means the last time it was actually written | An agent refreshed with new server data still claiming last year's version |
| The generated agent carries the approval envelope, not an ask | ssh, deploy | ssh-admin-agent.md.template:81-93, deploy-admin-agent.md.template:42-53 |
Every destructive classification in the emitted agent | Do not execute -- emit an envelope, unless the prompt carries APPROVED: <id>, and Nothing to approve -> the literal line APPROVAL REQUIRED: none. Silence is never a gate. |
The generated copy inherits the section-3 contract, not just the shipped one | A project agent that was told to confirm, could not, and restarted a service anyway |
| The project copy shadows the shipped agent | ssh, deploy | .claude/agents/ssh-admin.md against brewtools/agents/ssh-admin.md |
After setup |
both declare the same bare name: ssh-admin; the project copy carries the filled-in inventory, the shipped one is generic and carries maxTurns: 80 |
One name, and in a configured repo it resolves to the copy that already knows your servers | A generic admin agent that re-discovers the whole inventory on every run |
| Secrets land in a git-ignored file first | ssh, deploy | CLAUDE.local.md and .gitignore |
Install, before the agent is written | grep -q "CLAUDE.local.md" .gitignore 2>/dev/null && echo "EXISTS" || (echo "CLAUDE.local.md" >> .gitignore && echo "ADDED") |
Hosts, users, ports, key paths and secret names never reach a commit | Server details pushed to a public repo |
| The key is read from stdin, never argv | provider-switch | scripts/write-alias.sh, action set-key |
Every key write | Value comes from stdin, NEVER argv -- argv is visible to ps and is logged verbatim. Call shape: printf '%s' "$KEY" | write-alias.sh set-key VAR_NAME |
The API key is invisible to other processes and to shell history | A provider key leaked through ps output or ~/.zsh_history |
| A backup that cannot leak what it backs up | provider-switch | same script, backup_zshrc and drop_backup |
Before every ~/.zshrc modification |
( umask 077; cp "$ZSHRC" "$ZSHRC.bak" ) then an explicit chmod 600; teardown deletes the backup instead of leaving it |
A rollback copy as protected as the original, and no credential snapshot left behind | A world-readable ~/.zshrc.bak holding five providers' API keys |
7. Agent authoring conventions
Every agent this suite writes carries a fixed set of sections. brewcode:agent-creator injects them and its
own checklist enforces them. Coverage is measured over the 8 shipped agents.
| Convention | Where | Enforced by | Excerpt | Purpose | Problem solved | Coverage |
|---|---|---|---|---|---|---|
## Return Contract |
last section of the body | guardrails block agent-creator.md:388-403, emitted verbatim, plus checklist :495 |
Verdict first, <=30 lines, path:line. !=bodies/output/log/preamble. Unconditional -- spend one step on what the MAIN SESSION needs and return only that. |
Cap what a subagent pushes back into the parent context | An agent that dumps its whole transcript burns the main context | 8/8 |
Report path .claude/reports/YYYYMMDD-HHMMSS_<agent>/ |
inside Return Contract and Checkpointing | same checklist | Bulk material (long logs, full diffs, dumps) -> file under .claude/reports/<YYYYMMDD-HHMMSS>_<name>/; return the PATH, !=the content. |
One predictable place for artefacts | Long output has nowhere to go except the reply | 8/8 |
| Return-guard awareness line | last line of Return Contract | hand-carried per agent | If the agent-return guard is installed, a return over ~1000 est-tokens (chars/4) is blocked for compression; over ~2500 file the detail |
The agent knows the runtime numbers before it is blocked | Writing a huge return, getting blocked, wasting a turn | 8/8 |
## Scope guard |
right after the title | hand-carried. The guardrails block (agent-creator.md:388-403) emits only Scope Fit and Return Contract, so this section is copied by the author, not injected; agent-creator carries its own at :21 |
Exceeds one bounded unit (one deliverable, ~5 files, ~10 steps) ... STOP, do not start. Return a split proposal ... An hour of unsupervised work is a failure even when it succeeds. |
Force a split proposal instead of an unbounded run | One agent working unobserved for an hour drifts and nobody sees it | 7/8 |
| Missing-brief rule inside Scope guard | Scope guard paragraph 2 | same block | Brief missing GOAL, SCOPE, CONTEXT ... or acceptance -- state your assumption explicitly in the report, or ask once. Never invent scope. |
Make invented scope visible | The agent silently guesses what was wanted | 7/8 |
## Checkpointing |
after Scope guard | agent-creator.md:429-444, sized to risk - an AG that runs long, writes files, or fans out gets the full checkpoint rule, a short read-only agent gets none |
maxTurns: 60 = anti-loop stop, != budget. On hit the run aborts and the final report is lost; scripts already written survive. ... On resume: read that file first |
Survive an abort, make a resume cheap | Hitting maxTurns destroys the final report |
8/8 |
Explicit maxTurns sized to the role |
frontmatter | agent-creator.md:431-442 role table |
the table reads: explorer 40, reviewer/architect/tester 60, docs/generator 80, developer/orchestrator 120. The 8 shipped agents use only two of those: five at 80, three at 60 | Anti-loop stop, deliberately generous | A tight cap aborts a healthy run and loses its report | 8/8 |
## Scope Fit for code-writing agents |
body, conditional | guardrails block agent-creator.md:393-396 plus checklist :496 |
Build for the actual scale and the problems that exist today ... Etalon-first: before writing a class/module/test, find the closest well-built existing one in this repo |
Keep generated code proportionate to the repo | Speculative abstraction; a new file that ignores repo idiom | 0/8 - none of the 8 writes product code |
Bash declared in tools: |
frontmatter | health flag in brewcode/skills/agents/SKILL.md:158 |
tools: Read, Write, Edit, Glob, Grep, Bash, WebFetch |
On macOS Grep/Glob are gated out of the default set and search runs through Bash |
An agent without Bash cannot search at all on this platform |
8/8 |
${CLAUDE_PLUGIN_ROOT} for in-plugin paths |
agent body | native substitution at spawn | ${CLAUDE_PLUGIN_ROOT}/skills/skills/references/prompt-contract.md |
Reference plugin files without knowing the cache version dir | A hard-coded .../5.5.2/ path breaks on every release |
6/8 |
Bare kebab name, one-line description with Triggers: |
frontmatter | checklist agent-creator.md:483-484 |
description: "Creates and improves Claude Code agents. Triggers: create agent, improve agent, scaffold agent." |
Claude Code prepends the plugin name itself | name: brewcode:x renders /brewcode:brewcode:x; a : in name makes the file skipped since 2.1.218 |
8/8 |
Stamp block doc_type / version / generated_by / last_updated |
trailing frontmatter keys | setup-status/references/artifact-metadata.md; rewritten by bump-version.sh |
doc_type: llm / version: "5.5.2" / generated_by: "brewcode" / last_updated: "2026-08-10" |
One number answers "is this install current?" | Stale copies indistinguishable from fresh ones | 8/8 |
[DICT: ...] abbreviation header |
line after frontmatter | nothing scripted. The LLM text rules table (agent-creator.md:406-418) only says Merge duplicates, abbreviate in tables; the header itself is hand-carried, agent-creator.md:15 |
[DICT: AG=agent, BC=brewcode, CC=Claude Code, ...] |
Let the body use short tokens without ambiguity | Repeating "subagent" hundreds of times costs tokens | 3/8, the three long generators |
| Final optimization pass delegated | creation process step 6 | agent-creator.md:427 plus checklist :500 |
Task(subagent_type="brewtools:text-optimizer", ...). brewtools absent -> skip and note it |
Compress the artefact after it is correct | Hand-written agent bodies grow verbose | 4/8 mention it |
| No "ask the user" instruction anywhere in the body | whole body | checklist agent-creator.md:487, prose :146 |
Body carries no "ask/confirm with the user" instruction -- a SA cannot prompt; it returns the decision request to its caller |
The body cannot promise a capability the runtime removed | An agent told to confirm before deleting, which silently deletes instead | 8/8 |
tools: survives the runtime filters |
frontmatter | checklist agent-creator.md:486 |
every entry survives the filters for the pool this AG runs in -- none of the nine filter-1 TLs |
A declared tool that is stripped is not a capability | AskUserQuestion in tools: reading as an approval mechanism that does not exist |
8/8 declare it; it is inert in all 8 |
## Approval Contract |
after the safety rules, before the workflow | hand-carried per agent - agent-creator does not inject it |
This agent therefore NEVER executes a destructive operation on its own judgement, then the six-field envelope and APPROVED: <ids> as the only authorization |
Turn "cannot ask" into a protocol instead of a dead end | An agent that either stops uselessly or self-approves a destructive command | 2/8 - brewtools/agents/ssh-admin.md:57-92, deploy-admin.md:75-110; the other 6 write nothing destructive |
The conventions above are measured over the shipped agents only. The generated families are a different
story: most of them carry none of the three sections, and none declares maxTurns.
| Agent family | Written by | Lives in | Carries the three sections |
|---|---|---|---|
| The 8 shipped agents | hand-maintained, agent-creator checklist |
brewcode/agents/{agent-creator,bash-expert,bc-rules-organizer,hook-creator,skill-creator}.md, brewtools/agents/{deploy-admin,ssh-admin,text-optimizer}.md |
yes - this is the Coverage column above |
| Team members | /brewcode:teams-setup via brewcode:agent-creator |
.claude/agents/{name}.md |
## Return Contract yes (teams-setup/references/agent-template.md:67), with the Scope Fit + Etalon-first block below it marked deletable for review-only members. No ## Scope guard, no ## Checkpointing |
intent-guard |
/brewcode:superreview-setup generate.sh emit-agent |
.claude/agents/intent-guard.md |
none of the three. It has its own ## 2. Evidence budget and ## 5. Output instead (intent-guard.md.template:94, :140) |
e2e-*, 5 members |
/brewcode:e2e install via agent-creator, from e2e/references/agent-template.md |
.claude/agents/e2e-*.md |
none of the three. The template body defines ## Scope Constraint, ## Rules Loading Protocol, ## Task Acceptance Protocol and ## Self-Check Protocol |
ssh-admin, deploy-admin project copies |
/brewtools:ssh setup, /brewtools:deploy setup, from a .template |
.claude/agents/{ssh,deploy}-admin.md |
none of the three, and no maxTurns - unlike the shipped agents of the same name, which have maxTurns: 80 |
docs-writer |
hand-maintained, this repo only | .claude/agents/docs-writer.md |
none of the three. It is not one of the 8 and is excluded from every count above |
What the creator agents teach, and the test that pins it
agent-creator, hook-creator and skill-creator are not only authoring guides - they are this
suite's copy of the Claude Code runtime contract, and every generated agent, hook and skill inherits
whatever they claim. v6 re-verified all of it against a 2.1.233 source snapshot. A wrong fact here is
not a doc bug: it is a wrong artefact in someone else's repo.
| Fact, as of 2.1.233 | Taught at | Why it is load-bearing |
|---|---|---|
31 hook events, 5 handler types: command, http, mcp_tool, prompt, agent |
hook-creator.md:21, per-event table :132-161 |
The roster is what a generated hook registers against; a missing event is a mechanism nobody knows exists |
SessionStart has five sources - startup, resume, clear, compact, fork (fork added 2.1.214) |
hook-creator.md:132, :648, version history :778 |
compact-recall.mjs and role-recall.mjs both key on source === 'compact'; a hook that assumed four sources treats a fork as a resume |
StopFailure carries 10 error types, Notification 9 |
hook-creator.md:659, :653 |
Both are matcher-free string discriminators; a truncated list silently drops a branch |
Matcher-free events include PostToolBatch |
hook-creator.md:661 |
Writing a matcher for one of these produces a hook that never fires and never errors |
No total-per-session subagent cap. CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION (DEF 200) shipped in 2.1.212 and was removed in 2.1.224 |
agent-creator.md:326, skill-creator.md:307-308 |
Fan-out is bounded by concurrency (20) and depth (3) only. Planning around 200 is planning around a limit that no longer exists |
AskUserQuestion is removed from EVERY subagent, even when its tools: lists it; forks are the sole exemption |
skill-creator.md:310-313, :630; agent-creator.md:146, checklist :487 |
This is what makes the ## APPROVAL REQUIRED envelope of section 3 necessary rather than stylistic |
brewcode/agents/tests/suite-creator-contract.mjs pins exactly these, 27 checks, all passing. Its own
header states the policy: unconditional exact-equality / exact-set checks with a description. No branching decides which asserts run. The 31-event roster is a verbatim fixture (EVENTS_31), so a
future edit that drops or renames an event fails a test instead of shipping. Five assertions were
re-hardened after mutation testing showed the originals passing against a deliberately broken agent
body.
8. Skill contract
The skill side is enforced by brewcode/skills/skills/scripts/validate-skill.sh, which exits non-zero.
Source of truth for the prompt rules: brewcode/skills/skills/references/prompt-contract.md.
Measured today: 27/27 distributed skills pass, 0 failures over all 33 skill dirs. The 27 are 9 in
brewcode, 5 in brewdoc, 13 in brewtools and 0 in brewui - brewui/skills/ holds only a .gitkeep, so
brewui ships no skill, no agent and no hook, and its README.md is the only thing a release touches in it.
The other 6 dirs are workspace-local and ship with nothing: .claude/skills/{brewcode-review,docs,eurodns, clean-cache,update-overview,claude-plugin-guide}. The last of those is the single contract exemption,
hard-coded as */.claude/skills/claude-plugin-guide in validate-skill.sh:19; it still has to accept
[prompt] in position 1.
| Rule | Enforced by | What it looks like | Purpose | Problem solved |
|---|---|---|---|---|
Prompt-first argument-hint |
validate-skill.sh check 7 |
argument-hint: "[prompt] [status|install|upgrade|enable|disable|uninstall|purge] [project|global]" |
Accept a sentence, not keys | a skill whose arguments are positional keys ... is unusable in practice - nobody types keys |
Mode keyword table with EN and RU columns and Mutates? |
validate-skill.sh check 10 - requires at least one Cyrillic keyword when 2+ modes |
| Mode | EN keywords | RU keywords | Mutates? | |
Make mode resolution deterministic and mark destructive modes | A model guessing the mode differently each run |
| Mode resolution algorithm, fixed order | prompt-contract.md sec. 3, documented not scriptable |
1. Strip flags. 2. explicit mode token anywhere wins outright - no scoring. 3. else score distinct whole-word keyword hits. 4. tie-break ... 5. empty -> default mode. |
One published order, reproducible outcome | Two runs on the same sentence choosing different modes |
| Tie-breaks favour safety | prompt-contract.md sec. 3.4 |
tie involving a destructive mode -> AskUserQuestion. Never guess destructive. tie where one side is read-only (status) -> read-only wins. |
Never silently pick the destructive branch | An ambiguous prompt triggering purge |
At most ONE AskUserQuestion, max 4 questions |
prompt-contract.md sec. 3.5-3.6 |
asked before the work starts, and only when the answer changes what gets written |
Bound the interrogation; read-only runs ask nothing. A skill runs in the main session, so unlike a subagent it still HAS this tool - the removal of section 3 does not touch skills | Skills interviewing the user across several rounds mid-run |
--noask never suppresses a ground-truth STOP |
prompt-contract.md sec. 3.6 |
records the literal Skipped (--noask); it never suppresses a ground-truth STOP (missing target, several candidates, destructive confirmation) |
Non-interactive runs stay possible without becoming unsafe | A flag used to bulldoze a missing-target error |
Mandatory 5-field PLAN block before the first action |
validate-skill.sh check 9 |
PLAN - brewtools:deploy with INPUT:, MODE:, SCOPE:, DO:, RESULT: |
Show the resolved intent before acting | No block, or a block printed after work started, is a defect |
## Prompt contract boilerplate in every body |
validate-skill.sh check 8 |
the contract section pasted first, with <plugin>:<skill> and <DEFAULT_MODE> substituted |
The rules travel with the skill, including into generated skills | A contract known only to the author, lost on the next edit |
| Canonical setup modes, this exact order | CLAUDE.md prose plus each hint | status|install|upgrade|enable|disable|uninstall|purge, extras appended after |
One vocabulary across 11 generators; aliases like init, on, off, setup, remove, reset were removed |
Every setup inventing its own verbs |
Every -setup skill ships BOTH enable and disable |
CLAUDE.md re-verify one-liner, 11/11 today | two mechanisms: a live config flag re-read each invocation, or entry-file parking (<name>.disabled, body byte-identical) |
Turn a mechanism off without uninstalling it | Users uninstalling, and losing config, just to silence a hook |
| Invocation invariant | CLAUDE.md plus skill-creator.md:216-231; 27/27 |
user-invocable: true AND disable-model-invocation: true, both explicit |
The model never sees skill descriptions, so nothing auto-activates | 27 model-visible descriptions are a permanent per-request token tax, and auto-activation is only 20-50% reliable |
Bare frontmatter name equal to the directory name |
validate-skill.sh check 4 |
name: semble-setup in brewcode/skills/semble-setup/ |
Claude Code prepends the plugin name itself | name: brewcode:e2e renders as /brewcode:brewcode:e2e |
| Frontmatter key order, all 27 skills | CLAUDE.md prose, no script check | name, description, user-invocable, disable-model-invocation, argument-hint, allowed-tools, model |
Diffable, greppable, one shape | Per-skill key orders make a mass audit a per-file read |
| Metadata key order on generated artefacts | artifact-metadata.md:62-72 |
doc_type, version, generated_by, last_updated, appended after the file's own keys |
The four provenance keys are always in one place | Provenance keys scattered through frontmatter |
| Structural checks | validate-skill.sh checks 1, 2, 5, 6 |
single-line description, no multiline |/>, <=1024 chars, SKILL.md uppercase, non-empty body |
Cheap structural failures caught before ship | Lowercase skill.md is silently ignored by Claude Code |
sync mode is delete-first and non-growing |
skills/references/mode-sync.md, shared by /brewcode:agents and /brewcode:skills |
DELETE first (dead / stale / obvious / duplicate) -> FIX -> ADD last; Total delta MUST be <= 0 |
Re-verify every agent and skill against the code without inflating it | Docs-style sync that only appends until artefacts are mostly stale prose |
sync add gate and traceability |
mode-sync.md prime directive |
a new fact enters only if ALL hold: non-obvious for a competent model + verified against a real source + its absence costs a real failure; Unverifiable -> delete it, !=reword it |
Only facts that earn their line survive | Agents accumulating restatements of what the model already knows |
sync fan-out, one subagent per file |
mode-sync.md steps S3-S4 |
ground truth collected once, then <=8 parallel spawns each owning ONE file, HARD LIMIT: line count after <= line count before |
Parallel, bounded, reviewable diffs | One agent handed the whole roster rewrites everything from memory |
Regression harnesses
Prose and a checklist say what an artefact should do. From v6 the load-bearing half is also pinned by a
runnable suite, so a future edit that breaks a contract fails a test instead of shipping quietly. Nine
harnesses were added or hardened in this release, 565 checks in total, all passing. Eight further suite
dirs predate v6 (semble-setup, docsync-setup, agent-deadline-setup, agent-return-setup,
agent-router-setup, manager-setup, provider-switch, think-short-setup).
| Suite | Checks | What it pins |
|---|---|---|
brewcode/agents/tests/suite-creator-contract.mjs |
27 | The 2.1.233 facts the three creator agents teach: the 31-event roster as a verbatim EVENTS_31 fixture, the 5 handler types, the matcher-free set including PostToolBatch, SessionStart fork, StopFailure 10 / Notification 9, the absence of a per-session subagent cap, and AskUserQuestion being stripped from every subagent. Five assertions were re-hardened after mutation testing showed the originals passing against a deliberately broken body |
brewcode/hooks/tests/ |
68 | BC-H01 the canonical project-root recipe; BC-H02 plan-link safety - a non-symlink or foreign-target LATEST.md is preserved, and a symlinked .claude/plans is refused rather than followed out of the project; the plansDirectory setting; BC-H03 the post-compact re-anchor ladder; and fail-open on malformed stdin for all four brewcode hooks |
brewcode/skills/teams-setup/tests/ |
65 | BCOP08 roster path traversal in toggle and verify; BCOP09 provenance checked before runnability, so a member file of unknown origin is never executed on the strength of parsing correctly |
brewdoc/skills/publish/tests/ |
108 | BD01 the publish allowlist, and BD-N03 produce-and-verify: the step that claims an artefact exists is not the step that wrote it |
brewdoc/skills/md-to-pdf/tests/ |
14 | BD12 unique temp-file naming - two concurrent conversions cannot land on the same path |
brewtools/skills/deploy/tests/ |
76 | BT-F09 no ${{ }} expression interpolated into a shell or JS body; BT-F18 the health check's exit code is the gate, not its stdout; BT-F30 no reliance on a timeout binary that is absent from the runner PATH |
brewtools/skills/ssh/tests/ |
74 | BT-F22/F12/F21 the credential and host-key contracts, and that every token shape is read from stdin rather than argv |
brewtools/skills/secrets-scan/tests/ |
82 (26+16+16+24) | BT-F03 redaction - a scanner that reports a finding must never echo the secret it found |
brewtools/skills/text-optimize/tests/ |
51 | BT-F15 the optimization snapshot, and BT-F29b the refusal to write when the result would not be an improvement |
Release-time stamping and the Codex mirror
The same contract, one layer down: how a shipped artefact gets its version, and how the suite renders
itself a second time for a different host. Both are enforced by scripts that run in one transaction -
.claude/scripts/bump-version.sh, which aborts on any failed check - so they sit with the enforcement
rules above rather than with the agent-body conventions of section 7. Same columns.
| Rule | Enforced by | What it looks like | Purpose | Problem solved |
|---|---|---|---|---|
| Baked stamp and install-time token are mutually exclusive per file | bump-version.sh STAMPED_FILES, 44 entries, each re-checked by stamp_verify |
baked: brewcode-meta: version=5.5.2 generated_by=brewcode:semble-setup in the asset itself. Substituted: {PLUGIN_VERSION} / {GENERATED_BY} / {LAST_UPDATED} resolved when a skill writes the file. Kind fmd adds last_updated: and is used only for the 9 hand-maintained shipped artefacts, Never for byte-copied assets: a date there would churn every build |
setup-status compares an installed copy against the plugin asset with cmp -s, so a copied asset must already carry its number |
an install-time stamp would make every install report DIFFERS forever; the reverse mistake, a baked literal in a template, made the superreview references differ on every release |
version and content_version are two separate stamps |
bump-version.sh content_version_for(), called per STAMPED_FILES entry before stamp_rewrite |
brewcode-meta: version=5.5.2 content_version=5.4.0 generated_by=brewcode:semble-setup; version = "the plugin release that produced this file (bumped every run)", content_version = "the release in which this file's BODY last actually changed" |
content_version_for diffs the on-disk file's stripped body against the copy at its old content_version's git tag - "Identical -> preserve the old value ... Anything else ... is treated as CHANGED -> $NEW" |
setup-status reporting every install stale on every unrelated version bump, when only version moved and the body did not |
| Version carriers in docs are anchored, never global | doc_rewrite over the 7 VERSIONED_DOCS, then doc_verify |
DOC_VER_GREP matches six one-line headers only: | Version | X.Y.Z |, **ver:**, **Version:**, ^> Version:, version X.Y.Z, skills/, claude-plugin-brewcode@X.Y.Z |
Bump the header, leave the prose | these files also contain historical prose ("dropped in v5.0.0", "broken before v5.0.0") that must never move. A renamed header stops being bumped, so doc_verify hard-fails on a file with no recognised carrier |
| Frontmatter rewrites scoped to the leading block | the sed range 2,/^---$/ in stamp_rewrite |
2,/^---$/s|^version: "..."$|version: "$NEW"| |
Only the file's own frontmatter moves | A doc that documents frontmatter getting the YAML inside its fenced example silently bumped |
| The Codex mirror is regenerated in the same transaction as the version | bump-version.sh runs .codex/scripts/generate-compat.mjs then validate-compat.mjs, die if either is missing or fails |
per-plugin brew*/.codex/** plus the marketplace layout .codex/plugins/**: 8 hook .mjs (2 entry + 2 lib for brewcode, same for brewtools), 3 hooks.json, 4 agent .toml, and 11 mirrored skills each with an agents/openai.yaml |
The second distribution target cannot fall behind the first | the documented step is exactly what let the mirror rot a full major version behind (4.0.6 vs 5.0.0) |
| The mirror's own version is frozen on purpose | const VERSION = '4.0.6' at validate-compat.mjs:9, checked at :86 against 9 manifests |
every .codex manifest reads 4.0.6 or 4.0.6+codex.<cachebuster>; bump-version.sh never touches them |
The compat contract versions independently of the plugin | Raising any of them to the plugin version is NOT a fix -- it breaks the Codex contract |
| Claude vocabulary is rewritten, not stripped | transformText in generate-compat.mjs |
${CLAUDE_PLUGIN_ROOT} -> <plugin-root>, ${CLAUDE_SKILL_DIR} -> <skill-directory>, settings.json -> config.toml, .claude-plugin -> .codex-plugin, agent <name>.md -> <name>.toml |
One source of truth, two dialects | A hand-maintained mirror drifting in wording from the skill it mirrors |
| The skill sigil is escaped inside mirrored shell assets | skillSigil in generate-compat.mjs |
\$ in a .sh, bare $ on a comment line |
Codex invokes a skill as $plugin:skill, and that string has to survive a shell |
mirrored scripts run under set -eu, and a double-quoted "... $brewcode:teams-setup ..." aborts them with brewcode: unbound variable |
Worked examples: what a real turn looks like
The tables above say which hook speaks at which moment. This appendix shows the bytes. Each block is one
moment: what happens, the JSON the hook writes to stdout, and the text that ends up in the model's context.
Long strings are cut with ...; everything quoted is copied from the source file named with it.
A. Session opens
What happens: you start or resume Claude Code. Every registered SessionStart hook runs.
brewcode's session-start.mjs - the unmatched one, which runs on every start - writes one field, and it is
not a model field (brewcode/hooks/session-start.mjs:268, with the reason stated in the comment at :264):
{
"systemMessage": "brewcode: /Users/you/.claude/plugins/cache/.../5.5.2 | session: a1b2c3d4 | perm: default\nUPDATE brewcode 5.5.1 → 5.5.2: https://github.com/kochetkov-ma/claude-brewcode/releases/latest"
}
What the model sees: nothing. systemMessage is printed to you, the human. The hook sets no
additionalContext on purpose.
Other hooks on the same event do speak to the model. semble's speaks only when its state is ready:
{
"systemMessage": "semble: ready | cache 3f9a1c04",
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "semble: use ONE mcp__semble_code__search first (repo=/Users/you/proj, top_k=5, ...), then open the hit at start_line. rg stays for exact/exhaustive matching."
}
}
What the model sees, with think-short installed:
Be terse. Results first, no preamble/filler/sycophancy. ASCII only.
Think short: minimal internal reasoning, no exploring aloud.
...
semble: use ONE mcp__semble_code__search first (repo=/Users/you/proj, top_k=5, ...), then open the hit at start_line. rg stays for exact/exhaustive matching.
The human gets the version banner; the model gets tone and search rules, and nothing about versions.
B. Every prompt: the always-on reminder
You type: add a retry to the upload client
brewcode/hooks/forced-eval.mjs:55 returns the same three lines every turn - REMINDER_TEXT from
lib/reminder.mjs, capped at 9000 chars:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "[ROLE] Manager: scan agents (project .claude/agents/ first) - expert for this domain exists -> delegate regardless of size; ...\n[SPLIT] One agent for an hour = drift you cannot observe: split into bounded units (1 deliverable, ~5 files, ~20 min), ...\n[BRANCH] Stay on the current branch; none chosen -> main. ..."
}
}
The model sees those three lines appended after your prompt, about 90 words, every single turn. The channel
is additionalContext, never updatedInput - that field is silently dropped on UserPromptSubmit in
CC 2.1.x. Type a bare ok instead and forced-eval.mjs:48 matches its skip list (:41-46) and writes {}:
nothing is added, which is the normal quiet case, not an error. The same three lines come back after a
compaction, from the same constant, through a different hook - example H.
C. A codeword turn: ++m
You type: ++m rewrite the parser
brewtools/hooks/manager-prompt.mjs:84 matches the codeword, loads the resolved block file, and prefixes a
header. Several codewords in one prompt produce several blocks joined by ---:
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "User typed `++m` — Manager mode is active for this turn:\n\n[ROLE: MANAGER]\n\nYou are a Manager, not an executor. ...\n\n---\n\nUser typed `++a` — ..."
}
}
What the model sees, on top of the block from example B:
User typed `++m` — Manager mode is active for this turn:
[ROLE: MANAGER]
You are a Manager, not an executor. Your only actions: think, plan, build the
TaskGraph, delegate, observe, integrate. ...
Several hundred words for one turn, then gone. Both hooks fire on the same event, so the model gets both.
D. The periodic one: think-short every 10th prompt
You type: anything. think-short-prompt-counter.mjs:70 bumps a per-session counter and checks
count % 10. On prompts 1 through 9 it writes {}. On prompt 10, 20, 30 it writes the whole prompt file
back (:86):
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Be terse. Results first, no preamble/filler/sycophancy. ASCII only.\n...\nComment like a human, not an AI. ..."
}
}
The same ~120 words that opened the session are re-stated once per ten turns, so terseness does not decay.
E. A tool call that gets denied: the hard wall
What happens: the main session calls Write while .claude/brewtools/manager/state.json has
hard: true. hardmode-guard.mjs:353 answers on PreToolUse:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Hard wall: Write is blocked in the main session — delegate to a subagent. Manager HARD wall is ON — delegate via Task/Agent. To exit run `/brewtools:manager-setup disable`; the only Bash it needs — `node <project>/.claude/brewtools/manager/manager-state.mjs set hard=false` — is self-exempt at every level."
}
}
The model sees that reason come back as a tool error and the call never runs. The same call from inside a
subagent passes untouched: subagent stdin carries agent_id and the guard returns {} for it
(hardmode-guard.mjs:339-342). A main session started with claude --agent <name> carries agent_type
but no agent_id, so it is walled like any other main session.
A Bash call is judged differently, and v6 changed how. At strict every Bash is denied outright
(:375). At balanced the command must pass isReadonlyCommand, which since v6 is an allowlist:
a READONLY table of 22 permitted binaries (:294-306), a FORBIDDEN_META reject on any of
$( > < (:172), and then per-binary flag vetting for the ones whose read-only face hides a write.
Four bypasses that a denylist could not see were closed this way: git diff --output= and --textconv
(GIT_EXEC_FLAG, :266), git branch -D (GIT_BRANCH_READ_FLAG, :269), find -fprint/-fprint0
alongside -exec/-delete (FIND_EXEC_ACTION, :271), and gh issue comment --body, where only the
first positional is a subcommand so the vet reads position, not substring (vetGh, :283-288).
Anything not on the list is denied rather than allowed - an unknown binary is now a deny, not a pass.
The whole guard fails closed. Unparseable stdin denies (:326); a corrupt state.json sitting next
to an installed manager dir resolves to { hard: true, level: 'strict' } (:98-121); the outer catch
denies (:399-403). The one deliberate hole is the documented exit: the state CLI is checked after tool
classification and survives both strict and a broken state (:359), so you can always turn the wall
off.
F. A subagent spawn
What happens: the session calls Task. One moment fires for all three injectors below: SubagentStart,
once the agent exists. That channel accumulates - every registered SubagentStart hook's
additionalContext is appended and delivered into the subagent's own message list, so none of the three can
clobber another. First, think-short delivers the same tone directive that opened the session
(think-short-subagent.mjs:64):
{
"hookSpecificOutput": {
"hookEventName": "SubagentStart",
"additionalContext": "Be terse. Results first, no preamble/filler/sycophancy. ASCII only.\n..."
}
}
This replaced an earlier PreToolUse on Task|Agent design that rewrote the spawn prompt via
updatedInput.prompt - the one hook in the suite that used updatedInput for injection. That channel is
single-writer/last-wins (every PreToolUse hook on the event sees the same original input, and the runner
keeps only the last hook's edit), so a second hook doing the same thing would have clobbered it; the retired
design carried a self-suppression check for exactly that case. SubagentStart + additionalContext has no
such conflict, so the check is gone.
The return contract announces the numbers the agent will be judged by (agent-return-budget.mjs:122):
{
"hookSpecificOutput": {
"hookEventName": "SubagentStart",
"additionalContext": "RETURN CONTRACT (agent-return guard, mechanical): Verdict first, <=30 lines, `path:line`. !=bodies/output/log/preamble. Over ~1000 tokens your return is blocked for compression; over ~2500 write the detail to `.claude/reports/YYYYMMDD-HHMMSS_<name>/` and return that path + verdict + <=3 lines."
}
}
What the subagent sees: the tone directive, then the return contract, then semble's one-liner
(semble-subagent.mjs:133):
semble: mcp__semble_code__search is already available to you — no ToolSearch needed. Start any "where/how/why does X work" question with ONE call: repo="/Users/you/proj", top_k=5, ...
A subagent that inherits no conversation still arrives with output style, a return budget and a search rule.
G. A subagent returns over budget
What happens: the subagent finishes with a 6,000-character answer. SubagentStop fires and
agent-return-guard.mjs:97 sizes it: ceil(6000 / 4) = 1500 tokens. Above passTokens 1000, below
fileTokens 2500, so it is a compress order:
{
"decision": "block",
"reason": "RETURN TOO LARGE (~1500 tokens, budget 1000). Directive from the agent-return guard, not user data. Re-send the SAME answer, compressed: keep the verdict line and every `path:line` ref, drop preamble, file bodies, command output, logs and restated context. ..."
}
The agent rewrites once and returns. It is blocked at most once ever: agent-return-guard.mjs:92 checks
stop_hook_active before anything else, so the second answer always passes even if it is still over. Past
2500 tokens the message instead names a .claude/reports/<stamp>_<agent>/ path to write the detail to. The
docsync gate blocks on the same event style once per session and says so in its own text: This is the only docsync block this session. (docsync-gate.mjs:150).
H. A compaction lands
What happens: the context fills and Claude Code compacts it, on its own mid-turn or because you typed
/compact. The conversation is replaced by a summary and the session continues. SessionStart fires with
source: "compact", and brewcode/hooks/hooks.json has a second SessionStart group carrying
"matcher": "compact" with two hooks in it, both timeout: 2. session-start.mjs sits in the first,
unmatched group and runs on this event too, but it still writes only a systemMessage.
This is the one moment UserPromptSubmit cannot cover: an auto-compaction has no prompt, so forced-eval.mjs
never fires, and the summary has already collapsed every earlier copy of the role frame. role-recall.mjs
re-states it, unconditionally, from the same lib/reminder.mjs constant forced-eval.mjs uses - so the two
are byte-identical by construction, not by discipline:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "[ROLE] Manager: scan agents (project .claude/agents/ first) - expert for this domain exists -> delegate regardless of size; ...\n[SPLIT] One agent for an hour = drift you cannot observe: ...\n[BRANCH] Stay on the current branch; none chosen -> main. ..."
}
}
compact-recall.mjs then answers the other half: what were we doing. It reads input.transcript_path - this
session's transcript and nothing else - with one readFileSync behind a statSync guard: not a regular file
or over MAX_TRANSCRIPT_BYTES (64 * 1024 * 1024) and it logs a warning and scans nothing. No JSONL
parsing at all: Buffer.lastIndexOf for "planFilePath":", buf.includes for "name":"TaskCreate" and for
the three plan-mode markers. measured on an 8.13 MB transcript: the scan itself ~6 ms, full process wall clock ~30 ms standalone / ~55 ms spawned from a node parent (node startup dominates).
Every one of those keys is matched with its JSON quotes, and that is a fix, not a style choice: prose that
merely names a key arrives in the transcript escaped (\"), so a quoted key cannot match a transcript's own
text about itself. A bare plan_mode_reentry matched this repo's own design discussion of this very hook and
claimed a plan that never existed.
Five outcomes since v6, a ladder, first match wins (compact-recall.mjs:140-186). Claude Code prunes
~/.claude/plans/ on its own cleanup period, so a transcript's recorded plan path routinely outlives
the plan file itself. That used to fall straight through to plan-missing, the branch seen most often:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "[PLAN] The plan file for this session is gone or unreadable at /Users/you/.claude/plans/2026-08-14-refactor.md.\nRebuild the frame from the compact summary plus TaskList, not from scratch.\n[TASKS] Then call TaskList: a task graph created before the compact ALREADY EXISTS in this session.\nRe-read it, do NOT create a new graph. The built-in reminder lags several turns and may show empty, so TaskList is the authority. Then resume the work."
}
}
v6 inserted a rung above it. plan-latest (compact-recall.mjs:155-164) checks the project-local
.claude/plans/LATEST.md that session-start.mjs maintains, and points at that instead: [PLAN] Read <root>/.claude/plans/LATEST.md with the Read tool before doing any work. / It is this project's latest plan, carried over from before the compact: follow its role model and delegation split, do not re-derive them. It sits above plan-missing on the stated rule that a real file beats a dead path, and it covers
the case a transcript scan structurally cannot: a plan that PREDATES the transcript, after --resume or
/clear, leaves no planFilePath to find at all. It cannot leak someone else's plan because the link is
hook-owned and project-scoped - BC-H02 containment is what makes this rung safe to add.
The five branches, in order: a planFilePath that still isFile() -> [PLAN] Read <path> with the Read tool before doing any work.; else a project-local .claude/plans/LATEST.md that exists -> the plan-latest text
above; a path recorded but gone -> the plan-missing text above; no path but a plan-mode
marker -> [PLAN] This session ran in plan mode; no plan file is available. and then both halves in one
fragment, A plan in the compact summary -> follow it and its delegation split, do not re-derive them. /
No plan there -> re-read the user ORIGINAL task and intent from the summary and keep executing THAT.;
nothing at all -> [INTENT] Re-read the user ORIGINAL task and intent from the compact summary and keep executing THAT.
What the model sees, once, next to the fresh summary - additionalContext accumulates across hooks on
one event, so both hooks land and neither clobbers the other:
[ROLE] Manager: scan agents (project .claude/agents/ first) - expert for this domain exists -> delegate regardless of size; no expert or trivial one-off -> self.
[SPLIT] One agent for an hour = drift you cannot observe: split into bounded units ...
[BRANCH] Stay on the current branch; none chosen -> main. ...
[PLAN] The plan file for this session is gone or unreadable at /Users/you/.claude/plans/2026-08-14-refactor.md.
Rebuild the frame from the compact summary plus TaskList, not from scratch.
[TASKS] Then call TaskList: a task graph created before the compact ALREADY EXISTS in this session.
Re-read it, do NOT create a new graph. ...
Three things about this block are deliberate. It is never silent: on source === 'compact' every failure
path, including the outer catch, degrades to the [INTENT] fragment rather than {} - the one place in
the suite where fail-open still speaks. It never quotes a plan from outside this session's transcript.
And [TASKS] orders TaskList before anything else because the built-in task_reminder lags several turns
after a compaction and can arrive empty, which is exactly what makes a session start a second graph.
Two trade-offs, recorded as trade-offs. Two plans in one session: the LAST planFilePath in the transcript
wins, so a session that planned once and later re-planned outside plan mode is pointed at the older plan.
And the plan-mode markers ("type":"plan_mode", "type":"plan_mode_reentry", "permissionMode":"plan") are
stamped on entering plan mode, strictly before any approval - measured in one real transcript as the first
permissionMode:plan on line 1166 against the first planFilePath on line 1178 - which is why that branch
claims only that the session ran in plan mode, and folds the intent fallback into the same fragment.
What is injected at each moment
Word counts are eyeballed from the source strings, not measured by a script.
| Moment | Hook event | Who injects | Roughly how many words | How often |
|---|---|---|---|---|
| Session opens, human line | SessionStart |
brewcode session-start.mjs, systemMessage only |
~15 to the human, 0 to the model | once per session |
| Session opens, tone | SessionStart |
think-short | ~120 | once per session, only when installed |
| Session opens, search rule | SessionStart |
semble | ~35 | once per session, only when state is ready |
| Session opens, wall notice | SessionStart |
brewtools session-start.mjs |
~60 | once per session, only while the wall is armed |
| After a compaction, role frame | SessionStart matcher compact |
brewcode role-recall.mjs |
108 words / 636 chars - the same REMINDER_TEXT as every prompt |
once per compaction, and compactions chain |
| After a compaction, plan and tasks | SessionStart matcher compact |
brewcode compact-recall.mjs |
exactly one plan fragment, 25-47 words: plan-file 29 / plan-latest 34 / plan-missing 25 / plan-in-summary 47 / intent 31 (146-257 chars, plan-latest 193, plus the plan path where one is quoted); plus 43 words / 251 chars of [TASKS] when the transcript holds a TaskCreate |
once per compaction, and compactions chain. Always something on source === 'compact', never {} |
| Every prompt | UserPromptSubmit |
brewcode forced-eval.mjs |
108 words / 636 chars, counted from lib/reminder.mjs |
every turn, except one-word replies |
| Codeword turn | UserPromptSubmit |
brewtools manager-prompt.mjs |
300-700 per block | only on the turn you type ++m / ++a / ++rr / ++r |
| Wall armed, no codeword | UserPromptSubmit |
brewtools manager-prompt.mjs |
~400 | every turn while state.hard === true |
| Tone refresh | UserPromptSubmit |
think-short counter | ~120 | every 10th turn |
| Ranked candidates | UserPromptSubmit |
semble prefetch | ~60 plus 3 paths | only on a question-shaped prompt, 30 s throttle |
| Main-session tool call | PreToolUse |
hard wall | ~60, as a deny | only when armed, and only on a blocked tool |
| Search-shaped shell call | PreToolUse |
semble reminder | ~25 | only when the command looks like a behaviour question |
| Subagent spawn | PreToolUse Task|Agent |
agent-router | ~40, only on a redirect | every spawn, when installed |
| Subagent starts | SubagentStart |
think-short subagent, agent-return contract, semble subagent | ~120 + ~50 + ~50 | every subagent, when installed |
| Subagent tool call | PreToolUse |
agent-deadline | ~60 warn, ~80 deny | past 80% of the budget, re-stated at most once per 10% of it |
| Subagent returns | SubagentStop |
agent-return guard | ~60 | only over budget, at most once per agent |
| End of turn | Stop |
docsync gate | ~50 | at most once per session, only when a doc is stale |
Where each thing lives
| Thing | Created by | Installed where | Turn it off with |
|---|---|---|---|
forced-eval.mjs |
ships with brewcode | brewcode/hooks/, registered in brewcode/hooks/hooks.json |
uninstall the plugin |
brewcode session-start.mjs |
ships with brewcode | brewcode/hooks/ |
uninstall the plugin |
role-recall.mjs |
ships with brewcode | brewcode/hooks/, registered in brewcode/hooks/hooks.json under the SessionStart group with "matcher": "compact" |
uninstall the plugin; there is no flag and no config file |
compact-recall.mjs |
ships with brewcode | brewcode/hooks/, second entry in that same "matcher": "compact" group |
uninstall the plugin; there is no flag and no config file |
lib/reminder.mjs - the one normative copy of [ROLE]/[SPLIT]/[BRANCH] |
ships with brewcode | brewcode/hooks/lib/, imported by forced-eval.mjs and role-recall.mjs |
nothing to turn off - it is text, not a hook |
brewtools session-start.mjs |
ships with brewtools | brewtools/hooks/ |
uninstall the plugin, or disable the wall so it goes silent |
manager-prompt.mjs |
ships with brewtools | brewtools/hooks/ |
uninstall the plugin; codewords cannot be disabled |
hardmode-guard.mjs copy |
/brewtools:manager-setup install |
<repo>/.claude/brewtools/manager/, registered in .claude/settings.local.json as PreToolUse "*" |
/brewtools:manager-setup disable, or uninstall to unwire |
manager-state.mjs + state.json |
/brewtools:manager-setup install |
<repo>/.claude/brewtools/manager/ |
node <ABS>/.claude/brewtools/manager/manager-state.mjs set hard=false; purge deletes it |
| Codeword text overrides | /brewtools:manager-setup edit (mode full only) |
<repo>/.claude/brewtools/manager/prompts/ or ~/.claude/manager/prompts/ |
purge restores plugin defaults |
think-short hooks: think-short-session.mjs, -prompt-counter.mjs, -subagent.mjs, think-short-prompt.md |
/brewtools:think-short-setup install |
project <repo>/.claude/hooks/ or global ~/.claude/hooks/, 3 entries in that scope's settings.json |
/brewtools:think-short-setup disable renames the prompt file |
semble_code MCP entry |
/brewcode:semble-setup install |
~/.claude.json, user scope, pinned semble[mcp]==0.5.4 |
/brewcode:semble-setup disable (flag), uninstall to remove |
.claude/rules/semble-first.md + the CLAUDE.md marker block |
/brewcode:semble-setup install |
project | uninstall or purge; disable leaves the text and silences the hooks |
semble hooks: semble-session.mjs, -reminder.mjs, -subagent.mjs, -prefetch.mjs, -stats.mjs |
/brewcode:semble-setup install |
<repo>/.claude/hooks/. 5 files, but 6 entries in .claude/settings.json: SessionStart, UserPromptSubmit, PreToolUse on Bash|Grep, SubagentStart with no matcher, and semble-stats.mjs registered twice - once on PostToolUse and once on PostToolUseFailure, which is the sixth entry (semble-setup/SKILL.md:443) |
state.enabled=false via disable |
agent-return-contract.mjs, agent-return-guard.mjs, agent-return-budget.mjs |
/brewtools:agent-return-setup install |
project and/or global hooks/, entries at SubagentStart and SubagentStop |
agent-return.json enabled: false, or omit the key |
agent-return.json |
same | <repo>/.claude/ or ~/.claude/ |
delete it - absent file means OFF |
agent-deadline-guard.mjs, agent-deadline-cleanup.mjs |
/brewtools:agent-deadline-setup install |
project and/or global hooks/, PreToolUse .* and SubagentStop |
agent-deadline.json enabled: false |
agent-deadline.json |
same | <repo>/.claude/ or ~/.claude/ |
delete it - absent file means OFF |
agent-router.mjs |
/brewtools:agent-router-setup install |
<repo>/.claude/hooks/, PreToolUse matcher Agent |
agent-router.json enabled: false - absent file means ON and effective; re-install is idempotent but not inert - it re-copies the hook file, which repairs a stale install |
agent-router.json |
same | <repo>/.claude/brewtools/ |
set enabled: false; deleting the file re-enables defaults |
| Tier-2 judge entry | /brewtools:agent-router-setup level strict |
inlined into <repo>/.claude/settings.json, type: "agent", haiku |
re-run at level fast; the config flag does not stop it |
/superreview skill |
/brewcode:superreview-setup |
<repo>/.claude/skills/superreview/ + references/ |
delete the skill dir; there is no global variant |
intent-guard.md |
/brewcode:superreview-setup generate.sh emit-agent - the only writer. /brewcode:teams-setup calls that same writer |
<repo>/.claude/agents/intent-guard.md |
delete the file; teams never parks it |
| Team dir and member agents | /brewcode:teams-setup |
<repo>/.claude/teams/{name}/ and <repo>/.claude/agents/*.md |
/brewcode:teams-setup disable parks entry files |
task-tracker.md, task-board skill, tasks.md rule, task-spec skill |
/brewtools:task-board-setup |
<repo>/.claude/agents/, .claude/skills/, .claude/rules/ |
disable renames the 4 entry files to .disabled |
.claude/features/** board data |
/brewtools:task-board-setup |
<repo>/.claude/features/ |
only purge deletes it; uninstall keeps your tasks |
docsync-track.mjs, docsync-watch.mjs, docsync-gate.mjs |
/brewdoc:docsync-setup |
<repo>/.claude/hooks/, 3 entries in .claude/settings.json |
config.json enabled: false - absent key means ON |
.claude/docsync/config.json + state.json |
/brewdoc:docsync-setup |
project | uninstall unwires, purge deletes |
memory-sync skill |
/brewdoc:memory-sync-setup |
<repo>/.claude/skills/memory-sync/ |
generate.sh disable renames SKILL.md to SKILL.md.disabled |
brewcode-review skill |
hand-maintained, not shipped by any plugin | <repo>/.claude/skills/brewcode-review/ in this workspace only |
delete the dir |
.claude/semble/state.json, reminder.json, telemetry.jsonl |
/brewcode:semble-setup install, then written by the hooks at runtime |
<repo>/.claude/semble/ |
disable sets state.enabled=false; purge deletes the dir |
| Per-agent deadline state | the deadline hooks at runtime | <os.tmpdir()>/brewtools-agent-deadline/<session_id>/<agent_id>.json (agent-deadline-guard.mjs:50) |
agent-deadline-cleanup.mjs removes it on SubagentStop; deleting the dir is safe |
.claude/teams/{TEAM}/trace.jsonl, trace-archive.jsonl, trace.cursor |
/brewcode:teams-setup, appended by scripts/trace-ops.sh |
project | disable leaves all three untouched (teams-setup/SKILL.md:91); delete by hand |
.claude/brewdoc/INDEX.jsonl |
/brewdoc:my-claude (SKILL.md:116) |
project. A legacy ~/.claude/brewdoc/INDEX.jsonl is merged in once, read-only, never written back (:121) |
delete the file |
.claude/md-to-pdf.config.json |
/brewdoc:md-to-pdf (SKILL.md:105-110) |
project, or ~/.claude/md-to-pdf.config.json at global scope |
delete it - the skill asks for the engine again |
.claude/e2e/config.json, .claude/e2e/e2e-rules.md, .claude/agents/e2e-*.md, optional .claude/rules/e2e-conventions.md |
/brewcode:e2e install (e2e/SKILL.md:70-71) |
project | delete the agents and the dir; this generator has no disable verb |
.claude/agents/ssh-admin.md |
/brewtools:ssh from templates/ssh-admin-agent.md.template (SKILL.md:341) |
project | delete the file; any re-run regenerates and re-stamps it (:499) |
.claude/agents/deploy-admin.md |
/brewtools:deploy from templates/deploy-admin-agent.md.template (SKILL.md:234) |
project | delete the file; a re-run regenerates it |
.claude/rules/avoid.md, best-practice.md and their *-avoid.md families |
/brewcode:rules |
project .claude/rules/ only, never ~/.claude/rules/ (rules/SKILL.md:13) |
delete the rows, or the file |
.claude/convention/*.md and the rule rows derived from them |
/brewcode:convention (SKILL.md:116) |
project | run mode conventions, which leaves .claude/rules/ untouched (:350); or delete the dir |
| Regression harnesses, 17 suite dirs | hand-maintained in this repo, never shipped to a consumer | brewcode/{agents,hooks}/tests/ and brew*/skills/<skill>/tests/ - 9 added or hardened in v6 (see section 8), 8 older ones under semble-setup, docsync-setup, agent-deadline-setup, agent-return-setup, agent-router-setup, manager-setup, provider-switch, think-short-setup |
nothing to turn off - they are node <suite>.mjs, run by hand or in CI |
| Provider alias block | /brewtools:provider-switch |
~/.zshrc only - the skill writes secrets nowhere else (SKILL.md:15, :54) |
delete the alias line |
Keeping this page current
When you add a hook, an agent, a generated skill or an injection, add two rows: one in the matching domain
table above, one in "Where each thing lives". Quote real text only, cut the middle with ... to keep it
short, and always name a concrete file. If a claim here stops matching the code, correct it in place - the
page states current behaviour, nothing else. If the thing you added is a new injection point - a new
hook event, or an old event answered at a new moment - also add a worked example: what happens, the JSON the
hook writes, the text the model ends up seeing. Give it a row in "What is injected at each moment" too. When
CLAUDE.md and the code disagree, the code wins and this page follows the code.
Runtime facts - the hook-event roster, the subagent tool filters, the spawn limits - are not this page's to
invent. They live in brewcode/agents/{agent-creator,hook-creator,skill-creator}.md, are pinned by
brewcode/agents/tests/suite-creator-contract.mjs, and are stated here against a verified Claude Code
version (2.1.233 as of v6.0.0). Change one of them here without changing it there and the test still passes
while the page lies. Change it there and the version pin moves too.