Claude CodeLesson
Hook-added context
Some advice only makes sense at the moment it applies: right after a push is rejected, or the first time an agent runs git. A hook can add that advice beside the result of the call that needed it, and then get out of the way.
Know hooks that add context already? Skip to In Terrarium.
Outside the box · General
The concept
A Claude Code hook is a command that runs at a fixed point in a session, wired in the project's settings. A PostToolUse hook runs after a tool call succeeds, and a PostToolUseFailure hook runs after one fails. By then the tool has already run, so the hook can't stop it. What it can do is talk to Claude: if it prints JSON with an additionalContext string, Claude Code puts that string into the conversation beside the tool's result, as a system reminder Claude reads on its next step. A UserPromptSubmit hook can do the same beside the prompt a person just sent.
{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "This file is generated. Edit src/schema.ts instead."
}
}
How hooks are wired, and what their exit codes do: Claude Code hooks.
Full referenceClaude Code docs: Add context for Claude
In Terrarium
Besides the hooks that set up a session and land its log, Terrarium's hooks are guards and hints. A guard runs before a call and can deny it. A hint runs after a call, or after a prompt, and only adds context. The roster in docs/agents/guards.md lists every hint beside the guards, and .claude/settings.json wires each one in.
Each hint watches for one situation. For example:
- scripts/hooks/stale-push-hint.ts waits for a
git pushthat git rejects as "stale info", and adds that the remote branch was probably deleted when its PR merged, with the fix. - scripts/hooks/read-first-hint.ts waits for the first call of a kind that
CLAUDE.mdor a ties to a doc, such as a session's first GitHub call or its first git command other thanstatus,diff,show,ls-tree,cat-file,greporls-files. It names that doc, with a closing line that a late read which clears the call is not a . After the main session opens a PR, it also says to invoke theclose-session. - scripts/hooks/adr-read-hint.ts runs after an
Edit, aWriteor a Bashgit commitand points to each whose rule matches a file written, or that the change names, unless the agent has already read it in full.
The stale-push hint makes its decision in one function: a Bash git push, then git's own rejection line anywhere in the output.
32export function staleInfoHint(payload: unknown): string | null {
33 const command = field(field(payload, 'tool_input'), 'command')
34 if (field(payload, 'tool_name') !== 'Bash') return null
35 if (typeof command !== 'string' || !GIT_PUSH.test(command)) return null
36 // PostToolUse carries `tool_response`; PostToolUseFailure carries `error`.
37 const response = field(payload, 'tool_response')
38 const output = [field(response, 'stdout'), field(response, 'stderr'), field(payload, 'error')]
39 return output.some((s) => typeof s === 'string' && STALE_REJECTION.test(s)) ? HINT : null
40}
The hint is wired on both PostToolUse and PostToolUseFailure, and prints the JSON above with whichever event called it as hookEventName.
A guard almost always fails closed: if it crashes, the call is blocked. A hint fails open. Each one is wired as … || true, and runGuard in scripts/hooks/hook-io.ts returns silently when a hint crashes: a broken hint adds nothing and the session carries on.
A hint also says each thing once. The read-first hint stays silent once the session's transcript shows the doc was read or the pointer was already given, and the ADR hint skips an ADR the agent already read in full or was already pointed to.
Why it's built this way
- Advice where it applies. Agents read git's "stale info" rejection as someone else pushing at the same time and retried, when the remote branch had usually been deleted after its PR merged. The hint puts the real cause and the fix into that same tool result. The cost: it arrives after the rejected push, so it corrects the next step, not this one. scripts/hooks/stale-push-hint.ts.
- Backing loaded instructions. The read-first and ADR hints catch a doc or ADR read only after the call or write that needed it, for the reasons CLAUDE.md and Path-specific rules give. ADR-0029 (docs/adr/0029-instructions-load-with-the-files-they-govern.md).
- Fail open. A broken hint must never block a session, for the reason Path-specific rules gives, so
runGuardreturns silently when a hint crashes. The cost: a broken hint just goes quiet, and nothing reports that its advice stopped. scripts/hooks/hook-io.ts. - Say it once. A pointer added on every matching call would repeat the same text in the context again and again. Each hint that points to a doc checks the agent's transcript first and stays silent once it is read or pointed to. The cost: with no transcript to check, the read-first hint in the main session and the ADR hint add nothing. scripts/hooks/read-first-hint.ts.
What to watch for
- A hint comes after the call. It can't stop a mistake, only correct the next step. When the first wrong call is itself costly, a hint is the wrong tool; a guard, which runs before the call, is the right one.
- Every matching call pays for the hook. Each hint on
Bashsits behind a short shell filter, as Claude Code hooks explains. - A subagent is a separate reader. Hooks fire for a dispatched subagent's calls too, but its payload carries the parent's transcript path. The hints that dedupe find the subagent's own transcript from its
agent_id, so a pointer the parent already got still reaches the subagent once.
Try it in your project
Find an error message your agent keeps misreading. Add a PostToolUseFailure hook on Bash that checks the command's error for that message and prints JSON like the example above, with hookEventName set to PostToolUseFailure and one sentence that names the real cause and the fix. End the hook's command with || true, so a bug in it never blocks a call.
Where next
- LessonClaude Code hooks
- LessonBlocking hooks
- LessonCLAUDE.md
- LessonPath-specific rules
- JournalFriction Ledger
Last updated · Latest source on GitHub ↗