The model is a stateless function: tokens in, tokens out. It has no memory, no filesystem, no ability to do anything. Every capability you think of as "Claude Code" is the harness — the program that decides what tokens go in, what to do with the tokens that come out, and what runs in between. This chapter walks one turn end to end, using this actual host as the running example, so that Chapters 3–7 (the individual MCP servers and repos) and Chapter 8 (the author's self-description) have a shared vocabulary to point back to.
This chapter in one table
| Layer | Rebuilt every turn? | Persuasion or enforcement? | Idle cost | Covered in |
|---|---|---|---|---|
| System prompt | yes | harness-owned | fixed, small | 1.3 |
CLAUDE.md files |
yes, as plain text | persuasion only | short | 1.3 |
| Tool schemas (built-in + MCP) | yes, unless deferred | harness executes what's called, but the schema itself doesn't stop anything | tens of tokens per tool, unless deferred | 1.3 |
| Skill listings | yes (name + description only) | persuasion until invoked | bounded by a context-budget fraction | 1.3 |
| Hooks | only when their event fires | enforcement, but only for what they match | zero when idle | 1.4, 1.6 |
| Auto-memory / conversation history | yes | persuasion | grows every turn | 1.3, 1.7 |
| Subagents | separate context window | — | cost of the report only | 1.9 |
1.1 What this instance looks like right now
It helps to ground the abstract layers in one real, fully-inspected setup, split by where each piece actually lives.
User level (~/.claude/, this reader's account):
~/.claude/CLAUDE.md (none) ← no global instructions
~/.claude/settings.json theme + autoMode.environment only, no "hooks" key, no permissions
~/.claude/skills/ (none)
~/.claude/agents/ (none)
~/.claude.json mcpServers {} ← empty map, zero local MCP servers
There is also a flat-file auto-memory: a MEMORY.md index plus one file per memory (typed user/feedback/project/reference). The index is loaded into context at session start alongside the conversation — it is a seventh context layer, of the same "plain text, no enforcement" kind as CLAUDE.md.
Project level (/mnt/BlockVolume/Claude/.claude/, this specific working directory — note the directory itself is not a git repository, so "checked in" doesn't literally apply here the way it would in a repo):
/mnt/BlockVolume/Claude/.claude/settings.json 1 PreToolUse hook, matcher "Bash"
/mnt/BlockVolume/Claude/.claude/settings.local.json permissions.allow: 26 accumulated rules
/mnt/BlockVolume/Claude/CLAUDE.md the "never change anything without asking" rule
So this instance is not uniformly empty — it's empty at the user level, and already has one real hook, a sizeable allow-list, and a project rule at the project level. Step 1.6 covers exactly what that one hook does and doesn't do, because it turns out to matter a lot for how you should read the "hooks are enforcement" claim later in this chapter.
The MCP servers visible at session start in the Claude Code session that produced this guide (Gmail, Docs, Spotify, …) were claude.ai connectors: remote servers attached to the claude.ai account, not processes running on this machine. Nothing MCP-shaped runs locally here.
1.2 Step 1 — You press Enter
The harness now builds a single prompt. It is rebuilt from scratch every turn (with prefix caching under the hood for cost, but conceptually: from scratch). Nothing persists in the model between turns except what the harness re-sends.
1.3 Step 2 — Context assembly
The pieces below are assembled into the context the model sees. Treat this as a set of layers rather than a rigidly specified order — no source available for this chapter pins down the harness's exact byte-for-byte sequencing, and in the session that produced this guide, project instructions and memory content in fact arrived as reminder blocks attached to the conversation, not inside a literal "system prompt" slot. What is solid is which layers exist and what each one costs.
a) System prompt. Harness-owned: identity, tool-use conventions, safety framing, the environment block (cwd, platform, git status, today's date). You mostly don't write this directly.
b) CLAUDE.md files, merged by precedence:
~/.claude/CLAUDE.md ← user-global, every project (empty here)
<project>/CLAUDE.md ← project-level (yours lives here)
<project>/sub/dir/CLAUDE.md ← pulled in only when you touch files there
These land as plain text. They are pure prompt — there is no enforcement behind them. Your "never apply changes without asking" rule works because the model reads it and (usually) complies, not because anything in the harness blocks a violation by itself. Section 1.6 covers the one respect in which that's no longer quite true on this host.
CLAUDE.md also supports @path/to/file.md imports, functionally identical to pasting the file inline — the split is for human maintainability, not for the model. According to the author's own self-description (not verified in any of the eight public repos — see Chapter 8), Raindancer118's personal global CLAUDE.md uses this to pull in several rules/*.md files, including one on security. Treat that specific detail as reported, not sourced.
c) Tool definitions. Every built-in tool (Read, Edit, Bash, Grep, …) plus every tool from every enabled MCP server, normally sent as a JSON schema with name, description, and parameter docs. This is the expensive part, and it's why the token-discipline obsession you'll see in these repos exists.
Counting the actual @mcp.tool()/@admin_mcp.tool() decorators in the repos: diary-mcp's main server registers 73 tools (diary_project_tools.py 37, memory_service.py 19, diary_link.py 8, search_engine.py 4, sync_manager.py 3, graph_core.py 2), plus 7 more on the separate diary-admin-mcp entry point (graph_admin.py), for 80 in total — see Chapter 3. cachyos-mcp registers 111: 21 of its own (arch.py) plus 31 from the network-mcp package it bundles (tools.py 20 + advanced.py 11) plus 59 from the sysprobe-mcp package it bundles (diag.py 17 + services.py 12 + desktop.py 30). That bundling is a plain Python import wrapped in try/except Exception: pass in cachyosmcp/__init__.py, so if the netmcp/sysprobe packages aren't importable, cachyos-mcp silently falls back to just its own 21 — see Chapter 5.
How much does that cost in tokens? This is a rough estimate, not a measurement of what the harness actually sends: an AST pass over the real tool definitions (docstring length plus parameter count, since FastMCP derives the schema from the function signature and docstring — see Chapter 2) puts diary-mcp's 73 tools at roughly 7–8k tokens (~100 tokens/tool on average) and cachyos-mcp's 111 at roughly 6k tokens (~50–90 tokens/tool, shorter docstrings). Enable both and you're in the neighborhood of 15–20k tokens of schema, before you've typed a word — if the harness sends full schemas for every enabled tool on every turn.
It often doesn't. Two mitigations show up both in these repos and in the session that produced this guide:
- Splitting servers. The author moved the 7 audit-only tools onto a separate diary-admin-mcp specifically so normal sessions don't pay for them (diary_admin_server.py's module docstring states this rationale directly). According to the author's self-description, a similar logic applies at the server level: roughly 8 of 21 configured MCP servers are disabled per-project by default (see Chapter 8; this count is not independently verified in the repos).
- Deferred tools. In the Claude Code session that produced this guide, most tools were listed by name only, with full schemas loaded on demand via a ToolSearch-style tool. That is the harness doing the same optimization automatically: idle cost drops to roughly a name per tool, and the "every single turn" cost above only applies once a schema is actually pulled in.
d) Skill descriptions. This is one of the cleverest of these layers. A skill is a directory:
~/.claude/skills/minecraft-dev/SKILL.md
---
name: minecraft-dev
description: Use when working on Spigot/Paper plugins — build setup, event API, …
---
<the actual instructions — can be thousands of words, plus reference files>
Only name + description are listed in context; the description can run up to 1536 characters by default (skillListingMaxDescChars), and the whole listing is capped at roughly 1% of the context window by default (skillListingBudgetFraction) — so "one line each" understates how much a description can actually say, though the total listing is still small relative to a full context window. Nothing in the harness "matches" a description against your task automatically: the model reads the listing and decides to invoke a skill (or you invoke one by name), and only then does the body load. That's progressive disclosure — dozens of skills cost a bounded, small slice of context idle, and a matched one delivers a full playbook. In the session that produced this guide, skills such as dataviz, claude-api and code-review were listed as one-line descriptions; their bodies were not loaded unless invoked.
Skills are how instructions scale past what a CLAUDE.md can carry without bloating every single turn.
e) Hook output. Covered in 1.4 and 1.6 — this is the layer with actual enforcement teeth.
f) The auto-memory index, conversation history, then your prompt.
1.4 Step 3 — UserPromptSubmit fires
Before the model sees anything, the harness runs any UserPromptSubmit hooks. A hook receives JSON on stdin:
{ "session_id": "…", "transcript_path": "/home/opc/.claude/projects/…/xyz.jsonl",
"cwd": "/mnt/BlockVolume/Claude", "hook_event_name": "UserPromptSubmit",
"prompt": "the text you just typed" }
and prints JSON on stdout:
{ "hookSpecificOutput": { "hookEventName": "UserPromptSubmit",
"additionalContext": "…text spliced into the prompt…" } }
That additionalContext is appended to your message. The model never decided to call it and cannot skip it.
According to diary-mcp's own README, a UserPromptSubmit hook (~/.claude/hooks/diary_prompt_retrieval.py) runs your prompt text through Postgres full-text search against curated memories and injects the top hits — you ask "what was the deal with the Ficus?" and the relevant memory is already in the window before the model starts, no tool call, no round trip. This is worth a flag: that script is not actually in the public diary-mcp repo. git log --all on the repo shows the filename only ever entering as a README reference, never as a committed file; the repo's shipped scripts/ directory contains session_start_hook.py and session_end_hook.py, not a prompt-retrieval script. So the mechanism above is documented behavior, per the README, not verified code — treat it accordingly (see Chapter 3).
The README is explicit about one design choice for that hook: it's FTS-only, never semantic, because — per the README — loading the embedding model fresh on every single prompt would reintroduce a cold-start stall previously documented in a "v0.8.1 postmortem" (a document not included in the public repo, so that history isn't independently checkable either). The model in question is paraphrase-multilingual-MiniLM-L12-v2 via fastembed, and the ONNX weights are noted in code comments as "~470MB" (diary_embed.py:28, diary_embed_ipc.py:6) — an approximate figure from the source, not a measured one. The inference worth drawing, and it is an inference: a hook that sits on the critical path of every prompt has to be cheap, and the shipped session_start_hook.py backs this up directly — its own docstring states "fails silent. A hook must never block a session," and its code wraps the stdin read in a bare try/except.
1.4.1 The event catalogue (a selection)
The table below covers the events this guide actually uses — it is not the full list this Claude Code installation's schema defines.
| Event | Fires | Typical use |
|---|---|---|
SessionStart |
session begins / resumes / /clear / after compaction (field source: startup|resume|clear|compact) |
inject memory, project state |
UserPromptSubmit |
before each of your messages reaches the model | retrieval, prompt rewriting |
PreToolUse |
before a tool runs | can gate the call — permissionDecision: allow/deny/ask |
PostToolUse |
after a tool runs | auto-format, lint, run tests on edit |
Notification |
permission prompts / idle | desktop notifications |
Stop |
model finishes its turn | can refuse to let the turn end |
SubagentStop |
a subagent finishes | |
PreCompact |
before context compaction | save what's about to be lost |
SessionEnd |
session closes | persist state, extract memories |
This installation's actual schema has considerably more: PostToolUseFailure, PostToolBatch, UserPromptExpansion, StopFailure, SubagentStart, PostCompact, PermissionRequest, PermissionDenied, ConfigChange, CwdChanged, FileChanged, InstructionsLoaded, WorktreeCreate, WorktreeRemove, and others not enumerated here. It's also not true that "a hook is just a command": besides the command type used throughout this chapter, the schema defines prompt, agent, http, and mcp_tool hook types, and a command hook accepts options beyond the bare shell string — timeout, statusMessage, async, asyncRewake, once, if.
PreToolUse and Stop are the two this chapter leans on most, but they're not the only events with "teeth" — PermissionRequest/PermissionDenied exist specifically to react to permission decisions, and any hook can return a blocking exit code (below), so the "which events can actually stop something" set is broader than this table.
Exit codes matter: 0 = fine, 2 = blocking error and stderr is fed back to the model (so the model sees why it was blocked and can adapt), anything else = non-blocking error — which, notably, means a hook that errors out for an unrelated reason (a missing dependency, say) does not automatically block anything unless it exits with code 2 specifically. Keep that in mind for 1.6.
Hooks are registered in settings.json. Here is the actual registration on this project, in full — not a hypothetical:
{ "hooks": {
"PreToolUse": [ { "matcher": "Bash", "hooks": [
{ "type": "command",
"command": "bash /mnt/BlockVolume/Claude/.claude/hooks/guard-destructive-bash.sh",
"timeout": 10,
"statusMessage": "Checking for destructive/state-changing command..." } ] } ] } }
This is from /mnt/BlockVolume/Claude/.claude/settings.json. The user-level ~/.claude/settings.json genuinely has no hooks key — but the project level, one directory down, already does. Step 1.6 walks through what that one hook actually does.
1.5 Step 4 — The model produces tokens
Plain prediction. If it emits a tool-call block, the harness intercepts it.
The animation below plays steps 4 to 6 as one loop, as plain text, the way it passes through the model. Text that is typed out is generated by the model. Text that appears at once is put there by the harness: the model writes a tool call and stops, the harness runs the tool, appends the result, and calls the model again. Between the two model calls nothing is remembered except what is in this text.
system: [system prompt, CLAUDE.md files, tool schemas]
user: How much space is left on the root disk?
assistant: I'll check the root filesystem.
{"type": "tool_use", "id": "toolu_01A…", "name": "Bash", "input": {"command": "df -h /", "description": "Show free space on /"}}
running Bash: df -h /
user: {"type": "tool_result", "tool_use_id": "toolu_01A…", "content": "Filesystem Size Used Avail Use% Mounted on\n/dev/mapper/ocivolume-root 36G 34G 1.5G 96% /"}
assistant: The root filesystem is 96% full: 1.5 GB of 36 GB left. That's tight, so anything large (a Docker image, a model cache) should go to /mnt/BlockVolume instead.The tool result comes back as a user message. That's how the API carries it: the model's context is a list of messages, and the harness adds the result as the next one. When the model answers without another tool call, the turn ends and the Stop event fires (1.7).
1.6 Step 5 — Permission check
Before executing a tool call, some combination of the following resolves it: the permission mode, the permissions.allow/deny/ask rules in settings, and any PreToolUse hook's own allow/deny/ask decision. This chapter will not assert a strict, linear "mode, then rules, then hook, then ask you" order — no source available here pins that down precisely, and the one hook installed on this project actually depends on being able to override an allow rule (see below), which is hard to square with a model where allow rules are checked and resolved before the hook ever runs. Treat the exact precedence as harness-internal and version-specific; what's certain is that all three inputs feed into the same decision, and if nothing resolves the call, you get a prompt.
Permission mode is one of six values (not four): default, acceptEdits, plan, auto, dontAsk, bypassPermissions. This host's user-level settings.json configures an autoMode.environment block, which is why auto mode matters here specifically — the other three chapters in this book that touch on autonomous operation (Chapter 6, on AIDEN, and Chapter 8) come back to what running unattended actually implies.
CLAUDE.mdis persuasion. Hooks and permission rules are enforced by the harness — but only for exactly what they match. A regex-based hook on one tool is a tripwire, not a sandbox.
That distinction is worth making concrete rather than leaving abstract, because this project already has a real example: /mnt/BlockVolume/Claude/.claude/hooks/guard-destructive-bash.sh, wired up as the PreToolUse hook shown in 1.4.1. Reading it directly:
- It only fires on the
Bashtool (the hook'smatcher).Edit,Write,NotebookEdit, and any MCP tool call are invisible to it entirely. - It reads
.tool_input.commandviajqand tests it, case-insensitively, against a fixed alternation of patterns:docker … (prune|rm|stop|kill|restart),docker compose … (down|restart),systemctl (start|stop|restart|enable|disable|mask),rm -rf/rm -fr-style flag combinations,mvinto/etc,/usr,/var,/boot, or/srv,kill/pkill/killall,shutdown,reboot,iptables,firewall-cmd,umount,mkfs,dd if=,fdisk,parted. - On a match it emits
hookSpecificOutput.permissionDecision: "ask"with a reason citingCLAUDE.md, and — per the script's own header comment — this is deliberate: it forces a fresh approval prompt even for a command wrapped insidessh ... "command", and even if an equivalent command, or an allow rule, already exists insettings.local.json(which on this project holds 26 accumulatedpermissions.allowentries, e.g.Bash(claude update *), several scopedcurl/sshcommands,WebSearch). That's the whole reason it has to be a hook and not just a stricter allow-list: an allow rule can't retroactively demand a fresh confirmation for a pattern that would otherwise auto-approve. - It always exits
0. That includes the case wherejqitself fails: the script setsset -euo pipefail, so ajqerror would in principle stop the script early — but because it never reaches an explicit non-zeroexit, and any such abnormal exit is a non-2code, that counts as a non-blocking error per the exit-code rule in 1.4.1, and the command would proceed unguarded. In other words, it fails open, not closed. - It's a plain regex over a command string, so it also has the ordinary blind spots of that approach: unlisted state-changing verbs (
systemctl reload,docker compose up -d,docker start,service … restart,sed -i,git pushto a repo that autodeploys,rm -rwithout-fwritten as separate flags in some orders), and anything indirected through a variable,base64 … | sh, or a script file written first and executed second.
Here is that hook playing out on one call. The left pane is what the model sees. The right pane is what the harness does while the model waits. For an ask, none of it reaches the model: Claude Code shows the hook's reason to you, not to Claude, so the model only gets the final tool_result and cannot tell that a hook caused the prompt.
system: [system prompt, CLAUDE.md files, tool schemas]
user: The new nginx config is in place, please apply it.
assistant: I'll restart nginx so it picks up the new config.
{"type": "tool_use", "id": "toolu_01B…", "name": "Bash", "input": {"command": "systemctl restart nginx", "description": "Restart nginx"}}
waiting for the tool resultPreToolUse hook (matcher: Bash)
$ .claude/hooks/guard-destructive-bash.sh
stdin:
{"session_id": "…", "transcript_path": "…/<session>.jsonl", "cwd": "/mnt/BlockVolume/Claude", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "systemctl restart nginx", "description": "Restart nginx"}}
running hook
grep -Eiq matched "systemctl restart" (pattern on line 10)
exit 0, stdout:
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "ask", "permissionDecisionReason": "Matches a destructive/state-changing command pattern (…). Per CLAUDE.md, …"}}
permission prompt: Bash command: systemctl restart nginx. Do you want to proceed?
waiting for you> No
command not run
user: {"type": "tool_result", "tool_use_id": "toolu_01B…", "is_error": true, "content": "The user doesn't want to proceed with this tool use. The tool use was rejected …"}
assistant: Okay, I won't restart it. Should I run nginx -t first to check the new config for errors?Had the command been systemctl reload nginx, no pattern would match: the hook would print nothing, exit 0, and the command would run without any prompt. A regex hook only stops what it lists.
What the model sees of a hook
Whether the model notices a hook depends on the outcome. Claude Code's hooks reference is explicit about the reason text: "For "allow" and "ask", shown to the user but not Claude. For "deny", shown to Claude."
| Hook outcome | What reaches the model |
|---|---|
PreToolUse ask, you approve |
Only the normal tool_result. Nothing says a hook ran. |
PreToolUse ask, you decline |
Only the rejection tool_result (the animation above). The hook's reason went to you. |
PreToolUse allow |
The normal tool_result. The reason text is shown to you only. |
PreToolUse deny, or exit code 2 |
The call is blocked, and Claude sees the reason (or the hook's stderr) as the denial message. |
additionalContext (PreToolUse, UserPromptSubmit, SessionStart, …) |
Added to the model's context, so it can see and act on it. |
A Stop hook that blocks |
Its reason is fed back and the model keeps working (1.7). |
You can always tell that a hook asked: Claude Code labels the prompt with where the hook came from, [settings] for a hook from a settings file like this project's. The model can't. In the session that produced this guide, the model tried it on purpose. It first ran a plain date, then a harmless echo whose text contains systemctl restart, which the guard matches:
Bash: date -u +%T.%3N
→ 05:46:32.388
Bash: date -u +%T.%3N; echo "harmless hook test: systemctl restart nginx"
→ 05:47:52.601
harmless hook test: systemctl restart nginx
The second result is just the command's output. Nothing in it says a hook ran, a prompt appeared, or you approved. The only trace is the clock: about 80 seconds between two calls that normally follow each other within seconds. The model can infer that something paused the call, but not that it was a hook. In the session that produced this guide, the model did see hook output of the last kind: the /goal command's Stop hook sent its verdicts back as "Stop hook feedback" messages whenever the goal wasn't met yet.
Accept once, or always accept
When a call needs your approval, Claude Code's prompt offers roughly three answers (the exact wording varies by version):
| Answer | What happens |
|---|---|
| Yes | This one call runs. Nothing is saved, so the next matching call asks again. |
| Yes, and don't ask again | The call runs, and a permissions.allow rule for that command (or a prefix of it) is saved in .claude/settings.local.json. Later matching calls run without a prompt. |
| No | The call is blocked, and the model receives the rejection as its tool_result (the last user: message in the animation above). |
Not every prompt offers the second answer. Claude Code only offers it when the prompt can show everything the saved rule would allow, so some prompts are one-time only (permissions docs).
This project's settings.local.json shows what the second answer leaves behind: 26 entries. 15 are long, exact one-off Bash commands (a single approved ssh … sed -i … line, for instance), 5 are prefix rules such as Bash(claude update *), and 4 are rules for other tools.
For anything the guard hook matches, "don't ask again" does not stick, and that is deliberate. The hook runs on every Bash call and decides afresh each time. Claude Code runs PreToolUse hooks before the permission prompt, and a hook's output "can deny the tool call, force a prompt, or skip the prompt" (permissions docs); a hook's ask even forces a prompt in auto mode. So when it prints "ask", you are asked again, whatever the allow list says. The script's header says it forces the prompt "regardless of prior approvals elsewhere in the session", and the reason it prints spells the rule out: "Per CLAUDE.md, every state-changing command needs its own explicit approval, even if an equivalent command was just approved on a different host or earlier this session." So every guarded command is accept-once. That is this project's own CLAUDE.md rule at work, not part of Raindancer118's setup. Consistent with it, none of the 26 saved rules matches one of the guard's patterns. If the guard seems to "forget" an approval you just gave, that's the design, not a bug.
So: the project's "never apply changes without asking" rule is partly structural now, not purely persuasion-only — but only for the specific list of Bash patterns above, only on the Bash tool, and only as an "ask," never a hard "deny." Everything else the rule covers (Edit/Write calls, MCP tool calls, any state-changing command outside that pattern list) is still enforced by nothing but the model reading CLAUDE.md and complying.
1.7 Step 6 — Execute, append result, loop
The tool result is appended to the context and the model runs again. Repeat until it emits no tool call. Then Stop fires.
Stop can refuse to let the turn end — the reason is fed back to the model, and the turn continues instead of ending. According to the author's own self-description (not sourced in any of the eight public repos — see Chapter 8 for the full account), Raindancer118 describes a Stop hook that "limits autonomous endless loops," which, read literally, is the inverse use of the mechanism: it would count forced continuations and stop blocking after some number of them, rather than being the source of the looping itself. The same self-description separately mentions a rate-limit mechanism where a session "waits in the background and continues on its own" after hitting a rate limit. Neither hook appears anywhere in the eight public repos, and how the rate-limit one is actually built is not stated. Both are flagged here as reported, not verified — Chapter 8 goes through the self-description in full and is the place to see what else falls into that same "described but not shipped" bucket.
1.8 Step 7 — Compaction, eventually
When the window fills, the harness summarizes older history and restarts from the summary. This is where state silently dies — and it's why diary-mcp's SessionStart hook distinguishes source == "compact" from other sources, re-injecting only the memories pinned with a compact trigger while a normal start re-injects the ones pinned with start (diary-mcp/scripts/session_start_hook.py). Read plainly, that's a design choice worth naming precisely: it treats compaction as a distinct event with its own retrieval rule, rather than treating "session just started" and "session just got summarized" as the same case. Whether that's rarer among memory systems generally is not something this chapter can support — no comparison data is available here — but it is a deliberate, verifiable design decision in this specific codebase.
One naming note for anyone trying to install this from the README: the README's registration example uses python3 ~/.claude/hooks/diary_session_start.py, but the file actually shipped in the repo is scripts/session_start_hook.py — the README's path reflects the author's own local copy name, not the repo's filename.
1.9 Subagents — the fifth thing
A subagent is a separate context window with its own system prompt, its own tool set, and its own model, whose final report is the only thing that returns to the parent. Defined in .claude/agents/*.md with frontmatter (name, description, tools, model).
A major benefit — not the only one, since subagents are also used for plain parallel fan-out — is context economy: an agent can read 40 files and hand back three paragraphs, and the parent only pays for the three paragraphs. Per the author's self-description, this is the shape of Raindancer118's own "orchestrator pattern" — a larger model plans, and subagents on a smaller model do the fan-out work (see Chapter 8); that specific pattern and its naming are not verified against any of the eight public repos, so treat it as reported rather than sourced.
1.10 What to take away
| If it must… | Use | Cost |
|---|---|---|
| always be true, and is short | CLAUDE.md |
every turn |
| be available conditionally, and is long | Skill | a bounded listing until triggered |
| let the agent do something external | MCP server | schema cost every turn, unless deferred |
| happen whether or not the model cooperates — but only within what it actually matches | Hook | zero until it fires |
| be investigated without polluting the window | Subagent | just the report |
- A turn is context assembled fresh (system prompt,
CLAUDE.md, tool schemas, skill listings, hook output, history, your prompt), sent to a stateless model, whose tool calls the harness then gates and executes. CLAUDE.mdand skill descriptions are pure prompt text with no enforcement behind them; hooks and permission rules are the only layers the harness itself enforces, and strictly only for what they're written to match.- This project already has one real
PreToolUsehook and a sizeable permission allow-list — it is not the blank slate a purely "empty instance" framing would suggest, even though the user-level config genuinely is close to empty. - Several vivid claims about this stack — the prompt-retrieval hook's exact behavior, the Stop-hook loop limiter, the rate-limit resume mechanism, the
rules/*.mdsplit, the orchestrator pattern's name — trace back to the author's own self-description rather than to code in the eight public repos; Chapter 8 is where all of those get checked against what's actually shipped. - Tool and skill counts are exact where they come from counting real decorators (
diary-mcp73 + 7 admin,cachyos-mcp111 bundled/21 standalone); token costs are rough estimates from docstring and parameter length, not measurements of what the harness actually transmits.
Checkpoint
Answer each question in your head first, then reveal and grade yourself. Your grades are stored only in this browser.
guard-destructive-bash.sh hook returns permissionDecision: "ask" for rm -rf and docker restart. Would it catch systemctl reload nginx or a command built as d=docker; $d rm mycontainer, and why or why not?No to both. The hook's pattern for systemctl only matches start|stop|restart|enable|disable|mask, not reload, so systemctl reload nginx passes silently. The $d rm mycontainer case defeats the regex entirely because the match is against the literal string docker … rm, and $d isn't textually docker until the shell expands it — the hook only sees the raw, unexpanded command text via jq. Both are real gaps in a regex-based, single-tool guard, not edge cases invented for this question.
diary-mcp's README documents a UserPromptSubmit hook that does Postgres full-text-search retrieval on every prompt. Why does this chapter call that "documented behavior, not verified code"?Because the script the README names (~/.claude/hooks/diary_prompt_retrieval.py) has never actually been committed to the public diary-mcp repo — git log --all shows no such file ever entering history, and the repo's shipped scripts/ directory contains only session_start_hook.py and session_end_hook.py. The README and a test-file comment reference it, but its real implementation isn't available to inspect.
SessionStart hook in diary-mcp treat source == "compact" differently from a normal session start, and what does that buy?Because compaction silently discards older conversation history, and a memory pinned only for "session start" would not be guaranteed to survive that event unless the hook explicitly re-injects it afterward too. session_start_hook.py checks the source field the harness sends and, on "compact", re-injects memories tagged with a compact pin trigger instead of (or alongside) the start-triggered ones — modeling compaction as its own retrieval case rather than folding it into "session just started."
CLAUDE.md. Given what guard-destructive-bash.sh actually covers, is that distinction absolute?No — it's enforcement only within what a given hook or rule actually matches. guard-destructive-bash.sh only inspects the Bash tool's command string against a fixed pattern list, fails open if jq errors, and never touches Edit, Write, or MCP tool calls at all. So "hooks are enforcement" is true in the sense that the harness runs them regardless of the model's cooperation, but a narrow hook still leaves most of a broad rule like "never change anything without asking" resting on the model reading CLAUDE.md and complying.