Chapter 9 of 9. Eight chapters described the mechanisms one at a time. This one walks the estate they add up to, including the parts that were taken back out. Prerequisites: Chapter 2 and Chapter 6. You should already know that CLAUDE.md is context rather than configuration, that a hook is the escalation path, and that a subagent runs in its own window and returns only text.
Everything below is against Claude Code 2.1.209, read off disk on 2026-07-14. Counts are exact as of that read. They will not be exact when you read this. That is the chapter.
Search "build personal ai dev system claude code" and you get setup posts. You get a directory layout, a CLAUDE.md template, and a hook that formats on save. What none of them tell you is what the thing costs to keep, which layers stop working when nobody is watching, and which pieces you will eventually delete. This is that account, from a system that has been running long enough to have a graveyard.
What a personal AI dev system in Claude Code actually is
Here is the whole estate, as counted on 2026-07-14.
| Surface | Location | Count |
|---|---|---|
| Constitution | ~/.claude/CLAUDE.md | 430 lines |
| Subagents | ~/.claude/agents/*.md | 31 |
| Skills | ~/.claude/skills/*/SKILL.md | 29 dirs, 20 of them symlinks |
| Slash commands | ~/.claude/commands/*.md | 23 |
| Claude Code hooks | ~/.claude/hooks/ + settings.json | 11 scripts, 4 events, 15 registrations |
| Git hooks | ~/.git-hooks/ | 4 scripts, wired by core.hooksPath |
| Registries | port-registry.json, a bot registry, a roadmap per repo | 3 |
Two rows deserve a pause before we get to the layers.
Twenty of the 29 skills are symlinks into the repos that own them. The global skills folder works as a mount table: only nine of those directories physically live there, and the rest are pointers. Author a skill inside a project, link it into ~/.claude/skills/, and it becomes globally invocable while its knowledge stays next to the code it describes. Holding all of them costs almost nothing, because per the skills docs "a skill's body loads only when it's used, so long reference material costs almost nothing until you need it."
The last row lives outside Claude Code entirely. Four of the enforcement scripts are git hooks, activated machine-wide with git config --global core.hooksPath ~/.git-hooks. They fire when anyone runs git commit, whether or not a model is in the loop at all. A system that stops at the boundary of the agent process governs the agent and nothing else, and plenty of damage in a repo arrives via a cron, an IDE, or a human at 1am.
Layer by layer: what each one was added to fix
The constitution came first, because writing rules down is what everybody does first. It opens with a nine-step mandatory workflow and thirteen named rules, running from SPEC and TESTS through PORTS and SHARE to REFLECT. The memory docs are blunt about what that buys you: "Claude treats them as context, not enforced configuration."
Then the first failure, dated in the file itself:
# 2026-04-18: Claude Code defaulted to general-purpose instead of specialist
# agents for an entire auth implementation session. Quality chain not followed.
# Fix: estate cut to a small core after Claude was over-faced with choice.
An entire auth session ran without the specialists the constitution mandates. The remedy sits two lines further down in the same gotcha, and its second sentence is what the rest of this course is built on:
# SessionStart hook prints 9-step workflow. Stop hook prints quality checklist.
# Lesson: Agents are voluntary. Hooks are not. Make the non-negotiables hooks.
Layer two is the advisory hook: shell that prints. SessionStart echoes the nine steps and the port registry into context. Stop prints a quality checklist and runs typecheck if package.json declares one. Nothing blocks. The registration lives in settings.json, where a hook stops being a file and becomes an event:
"SessionStart": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "echo '=== CJ WORKFLOW — 9 STEPS ===' && ..." },
{ "type": "command", "command": "cat ~/.claude/port-registry.json | python3 -c \"...\"" },
{ "type": "command", "command": "python3 ~/.claude/hooks/bot-registry-sessionstart.py" },
{ "type": "command", "command": "python3 ~/.claude/hooks/roadmap-sessionstart.py" }
]
}
]
Text in a context window can be read and then ignored. Layer three answers that with the blocking hook, and PreToolUse is the event that refuses a tool call before it happens. In Claude Code 2.1.209 the hooks reference gives you two ways to write one and insists you pick exactly one per hook. Exit 2 blocks, and stderr goes back to the model as the error. That path carries the gotcha which eats most first attempts: "Claude Code treats exit code 1 as a non-blocking error and proceeds with the action, even though 1 is the conventional Unix failure code. If your hook is meant to enforce a policy, use exit 2." The other path is to exit 0 and print JSON, where hookSpecificOutput.permissionDecision: "deny" refuses the call and permissionDecisionReason tells the model why.
Both of the live blockers in this estate take the JSON route. One matches Bash and denies any command that injects synthetic keystrokes while the Mac's screen is locked, written the day such a command typed itself into the login password field. The other matches Write and denies a video-timeline file carrying an effect id the installed editor cannot resolve, because that import succeeds with zero warnings and the frame then renders black. Neither of them returns 2. Both exit 0 with a refusal in the payload.
Layer four sits below the agent. A published article on this site leaked a real username into a plist example, and the fix that mattered was a pre-commit hook that scans the staged diff for the identifier and exits 1. It fires for the model, for a cron, for an IDE, for a human in a hurry. The docstring on the roadmap sub-hook states the design calculus for that whole layer in one clause: "the cost of a missed tick is recoverable; the cost of an unbypassable hook isn't." Every gate at the git layer fails open and carries a named bypass token in a uniform shape: BBBRAIN_NAMELEAK_BYPASS, BBBRAIN_ROADMAP_BYPASS, BBBRAIN_DEVLOG_BYPASS, BBBRAIN_BBPARK_LINT_BYPASS. Two of them log the bypass to stderr, so the escape hatch leaves a trace in the terminal history.
That is four layers. The bottom two hold under pressure for one shared reason: neither asks the model to cooperate. A PreToolUse deny lands before the tool runs. A pre-commit exit 1 lands whether or not a model was ever in the room.
Controlling a long session without burning tokens: mark, then nudge
Here is the problem the constitution cannot solve. A session runs for two hours. It edits twenty files. Somewhere around file fourteen it does work that has to be written down, and by the time it stops, the rule that says so has been sitting 400 lines up in a context window for an hour, competing with everything since.
Put the rule in CLAUDE.md and you pay its tokens at every session start, on all the days it is irrelevant, and the memory docs still promise you nothing at the end of it. Run a second model to watch the first and you are spending tokens to buy attention.
The pattern that works costs nothing until the condition is real. Two hooks and a file in $TMPDIR.
The first is a PostToolUse hook on Write|Edit. PostToolUse cannot block, per the hooks reference, and this one does not try. It writes a marker.
#!/usr/bin/env bash
# PostToolUse(Write|Edit) — if the edited path is Remotion / video-engine work,
# drop a per-session marker. Silent, never blocks. The Stop hook reads the marker.
set -uo pipefail
input="$(cat)"
f="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_response.filePath // empty' 2>/dev/null)"
[ -n "$f" ] || exit 0
# Never self-trigger: complying with the reminder (writing memory, the devlog, or
# these hook scripts) must not re-arm the marker.
case "$f" in
"$HOME"/.claude/*|*/docs/devlog.md) exit 0 ;;
esac
The rest of the script appends the path to $TMPDIR/claude-remotion/<session_id>.marker and exits 0. Cost: one shell exec per file write, zero tokens, no model involvement.
The second hook fires on Stop, reads the marker, and injects the nudge exactly once. As of 2.1.209 the reference documents this shape as a top-level decision: "block" carrying a reason. The stop is prevented, the conversation continues, and your text is the next thing the model reads.
m="${TMPDIR:-/tmp}/claude-remotion/${sid}.marker"
[ -f "$m" ] || exit 0
n="$(wc -l < "$m" | tr -d ' ')"
files="$(sort -u "$m" | head -5 | sed 's|^| - |')"
rm -f "$m" # one-shot: clear before emitting so we never loop
jq -n --arg n "$n" --arg files "$files" '
{
systemMessage: ("Remotion/video work this session (\($n) edits) — devlog + memory reminder issued."),
decision: "block",
reason: ("REMOTION / VIDEO WORK DETECTED THIS SESSION. Before finishing:\n\n1. Append an entry to the devlog … \n\nFiles touched:\n\($files)\n\nIf both are already done this session, say so briefly and stop.")
}'
(The reason string is trimmed above; on disk it names the exact devlog path and memory file.)
The defensive details are what stop this becoming a loop. The marker excludes ~/.claude/* and the devlog itself, so complying with the reminder cannot re-arm it. The Stop hook deletes the marker before it emits. One shot, no second fire. And the reason ends by telling the model what to do if the work is already done: say so briefly and stop.
The sibling gate sharpens the pattern by asking for evidence. A Stop hook compares a render stamp against the push marker, and when the stamp is the older of the two it blocks with this in the reason:
A clean import is NOT proof. Both of these imported with ZERO warnings and were still broken:
• one composition — every clip transformed off-screen (viewer was pure black)
• a guessed effect uid — the transition rendered black
That gate has no bypass token. You clear it by rendering a frame. Its condition is an artefact that cannot exist unless the work was done, which is the one shape of gate a model cannot talk its way past.
The nudges that do cost tokens, and the caps that hold them
One hook in this estate spends real money. On Stop it fires a background thread, reads the session transcript, and shells out to a second Claude to propose backlog items. Its constants are its budget: Haiku, --max-turns 5, the last 50,000 characters of transcript, a 500-character output cap, ten runs per repository per day, and a 500-byte floor below which a transcript is not worth reviewing at all.
The guard that matters most is BBBRAIN_STOP_REVIEW_RECURSION=1, set in the child's environment before the review spawns. Leave it out and a token-spending hook on Stop triggers itself the moment the child session stops. Chapter 8 prices all of this properly. Carry the ordering into your own estate: a hook that spends tokens declares its ceiling on the first screen of the file, above the logic.
Model choice is the same dial one level up. The models overview spans a tenfold price band from Haiku to the top of the family, and the constitution spends one rule turning that band into routing:
# AGENTS: Each subagent declares its `model:` in frontmatter. Cheapest
# model that can do the job: Haiku for deterministic mechanical
# work, Sonnet for synthesis + code, Opus for genuine reasoning.
# No global override — frontmatter is source of truth. Reviewers
# should question Opus selections that don't justify the cost.
The graveyard
Additions are easy to write up. Everything below was taken out of the system, or should have been, and each removal changed how the next thing got built.
The agent roster, cut and then regrown. The 2026-04-18 gotcha ends with the estate "cut to a small core after Claude was over-faced with choice". It was later re-expanded to 31, and the update appended to the same gotcha carries the lesson learned in between:
# Update (2026-06-05): agents re-done and deliberately expanded — all active and
# verified well-formed (frontmatter, models, tools; no stubs). Specialists are
# opt-in by name. Live roster = whatever is in ~/.claude/agents/*.md; do not
# hard-code a count here — it drifts.
The instruction not to hard-code a count is itself only a comment. The 31 in the table above came from ls.
The whole-file scan in the identifier hook. The first version of the name-leak pre-commit scanned entire staged files. It fired constantly on edits to the constitution, the devlog and the roadmap, because those files legitimately discuss the patterns the hook is looking for. Bypass use climbed. The rewrite scans added lines only, via git diff --cached --unified=0, keeping lines that start with +. A false positive does not merely annoy. It trains the hand to reach for the bypass token, and habitual bypass is removal with extra steps.
A symlink. The git hooks originally lived under ~/.claude/ and were reached through a symlink at ~/.git-hooks. core.hooksPath resolved through that symlink, so every write to ~/.git-hooks/foo landed somewhere else on disk than the path suggested. The extraction into a standalone repo is justified in its README with a line worth stealing: "Indirection you can't see is a class of footgun."
A warning, promoted to a gate. A lint was written to catch subagent definitions that declare the Bash tool without documenting the caller convention the audit trail depends on. It shipped as a warning: it printed, it did not block. That decision and the three post-merge fixes behind it are the subject of every post-merge fix was the same fix. My own article still says the lint only flags. On disk it no longer does. A later commit to the git-hooks repo wired lint_agents.py into pre-commit, and the script now ends return 1 if warnings else 0. A commit that stages an agent definition and trips the lint dies with the escape hatch printed to stderr: BBBRAIN_BBPARK_LINT_BYPASS=1. I nearly shipped this chapter with the article's version of the truth in it. Reading the hook took under a minute. Trusting my own prose would have taken none. The distinction the lint enforces still holds: "it captures ideas" and "the trail is clean" are different tests, and only the first is visible without looking.
And the thing that should have been removed first. The constitution still carries about seventeen lines of video-tooling notes, paid for in full at every session start, on the day the task is a PHP backend. The memory docs put the budget at "under 200 lines per CLAUDE.md file. Longer files consume more context and reduce adherence." This one is 430 lines. Its version footer still claims 140. Nothing compiles a comment.
The loudest rules have no gate behind them
Three rules in the constitution are written in the strongest language in the file. Never force push. Never commit directly to main. Inspect every staged file over 1 MB, after a 60 MB build artefact once ended up in a commit.
There is no pre-push hook. There is no size gate. What exists is a PreToolUse echo:
{
"type": "command",
"command": "echo \"$CLAUDE_TOOL_INPUT\" | grep -qE 'gh pr create|git push.*main|npm publish|bun publish' && echo '⚠️ PR/PUBLISH DETECTED — ensure build-validator passed first' || true"
}
That prints a warning and blocks nothing. The || true guarantees exit 0, and in 2.1.209 the only things that stop a tool call are exit 2 or a JSON permissionDecision of deny. Loudness in the doc has no relationship to enforcement on the disk. You find that out by looking, or you never find it out.
The port registry marks the honest boundary of the whole thesis. A SessionStart print is the entirety of its enforcement. No commit-time artefact proves a port was checked before npm run dev ran, and the git layer can only govern what enters history. Some rules stay advisory because the layer that would enforce them does not exist yet.
Audit your own estate before you trust it
That is the failure mode this course has been circling since chapter 1, and I built this with Claude Code documents the same shape one level down: a system that looks correct because nothing about it is throwing an error. Fabricated stats typecheck. An unwired hook produces exactly the silence of a wired one that has never had to fire.
- Open the hooks browser
/hookslists every event with a count of what is configured against it, and selecting one shows the matcher, type, source file and command. In 2.1.209 the menu is read-only: to change anything you edit the settings JSON directly. - Diff the directory against the registrations
List
~/.claude/hooks/and grepsettings.jsonfor each filename. Any script with no hit is a guarantee you believe you have and do not. This is the check that found the two preflights. - Check the second layer separately
git config --global core.hooksPathtells you whether the git hooks are active at all. Setting it globally means git looks only there, so per-repo.git/hooks/*stop firing unless that directory delegates to them. A repo-local gitleaks hook, in that configuration, is dead. - Measure the constitution against its own claims
wc -l ~/.claude/CLAUDE.md, then read its version footer. If they disagree, every session since the drift has been paying for the difference.
Run those four on a schedule, not on a feeling. The estate has no linter, no CI, and no mechanism anywhere in it that notices a hook has quietly come unwired.
The rule to build your own system around
The best line in this system was written after an auth session ignored every specialist agent it was told to use: agents are voluntary, hooks are not, so make the non-negotiables hooks. Eight chapters have argued for that sentence. This chapter adds the complication the auth gotcha could not have known about.
A hook is not voluntary. Its registration is.
So every rule in your constitution should carry two things beyond the rule itself: the mechanism that enforces it, and the command that proves the mechanism is live today. Prose is advisory, and the docs say so. A hook is deterministic only while it is wired. Nothing warns you when it stops. Write the rule. Name the gate. Then write down the one-line check that would catch the gate having quietly disappeared, and run it on a day when you are not already hunting a bug.
Everything in a personal AI dev system decays toward advisory. Without an audit you have no reading of how far yours has gone, only the model's recollection of what the constitution said.
Map your estate by enforcement layer, then schedule the audit
The chapter's closing rule is that every constitution rule should carry the mechanism that enforces it and the one-line check that proves the mechanism is live today. Apply that to your own machine: build the estate table from commands rather than memory, tag every rule in your CLAUDE.md with its real enforcement layer, and turn the chapter's four audit steps into a script that runs on a schedule.
Expected behaviour
- An estate table for your machine, with each count produced by a command such as ls or wc -l, covering at least your constitution, agents, skills, hook scripts versus registrations, and git hooks
- Every rule in your CLAUDE.md carries a tag naming its layer, whether prose only, printing hook, blocking hook, or git hook, with the enforcing file named where one exists
- The four audit steps run as one script: the hooks-browser count check, the hooks-directory versus settings diff, the core.hooksPath check, and the constitution line count measured against its own claims
- The script is wired to a scheduled trigger rather than run on a feeling, and its first scheduled execution has left a log entry you can point at
PROVE IT Paste real output from a scheduled run and name one rule whose strong wording turned out to have no gate on disk, then either wire a gate for it or write down the decision to leave it advisory. A layer map that has never been diffed against disk is the version footer claiming 140 lines while the file holds 430.
In the mark-then-nudge pattern, why is the marker written from PostToolUse while the nudge is delivered from Stop?
Which environment variable, set in the child's environment, stops the token-spending Stop hook from spawning reviews of its own reviews?
The chapter ends by asking for two things beside every rule in a constitution. What are they, and why does prose alone decay?
Show answer
Each rule should name the mechanism that enforces it and the one-line check that proves that mechanism is live today. Prose is advisory by the docs' own statement, and a hook is deterministic only while its registration exists, yet nothing warns you when a gate comes unwired. Without a scheduled check the estate drifts back toward advisory and you are left with the model's recollection of what the constitution said.
↺ re-read: “The rule to build your own system around”