Visual overview

Use the workflow to follow the task, and the architecture map to separate responsibilities. These are conceptual maps; the guide below defines implementation details and verification limits.

01 / WorkflowFrom intent to a checked result
  1. Select a lifecycle event
  2. Inspect tool input safely
  3. Run bounded checks
  4. Return the documented exit status
02 / ArchitectureResponsibility boundaries
  1. Boundary 1Client event
  2. Boundary 2Hook script
  3. Boundary 3Allow or block result

Connected responsibilities, not a required class hierarchy or an execution trace.

Load this when: a rule must hold every time rather than usually, you are repeating the same correction to an agent, or you are deciding between a hook, a CI check, and a reviewer subagent.

A hook is a shell command that runs automatically at a fixed lifecycle point. It is not a suggestion the model weighs — it executes, and its exit code decides what happens next. That makes it the correct home for anything a script can decide.


1. Hook vs. CI vs. reviewer

Three enforcement layers. Use the cheapest one that can decide the question.

Layer Decides Cost Feedback speed
Hook Rules a script can evaluate ~free Immediate — model self-corrects mid-turn
CI Same, plus full build/test minutes After push
Reviewer subagent Judgment: is this correct, does it match intent tokens End of task

The ordering matters. Spending a reviewer subagent on "did you use DispatchQueue.main.async" is waste — a grep answers that for free, instantly, and cannot be argued with. Reserve model judgment for what rules cannot express.

Rule of thumb: if you can write the check as a grep or an exit code, it is a hook. If it needs to understand intent, it is a reviewer.


2. Events

Event Fires Typical use
PreToolUse Before a tool runs Block edits to generated or protected files
PostToolUse After a tool succeeds Format, lint, check the file just written
UserPromptSubmit On each user message Inject project context
Stop Before the turn ends Build/test verification
SubagentStop When a subagent finishes Validate a subagent's output
SessionStart Session begins Environment setup, load state
SessionEnd Session ends Cleanup
PreCompact Before context compaction Persist state that must survive

PreToolUse, PostToolUse, and Stop cover almost every practical need.


3. The exit-code contract

This is the whole interface:

Exit Meaning
0 Pass. stdout goes to the transcript.
2 Block. stderr is fed back to the model as the reason.
other Non-blocking error, surfaced to the user.

Exit 2 is what makes hooks valuable: the model reads the failure and fixes it without the user having to notice, let alone intervene.

# The message on stderr IS the instruction the model acts on.
# Say what is wrong AND what to do instead.
echo "BLOCKED: CLAUDE.md is generated from SKILL.md." >&2
echo "Edit SKILL.md, then run ./scripts/sync-mirrors.sh" >&2
exit 2

A blocking message that only says "not allowed" wastes the round trip. Every exit-2 message names the fix.

Hooks receive JSON on stdin — tool_name, tool_input, cwd, session_id, and for PostToolUse also tool_response. Parse it rather than guessing:

INPUT="$(cat)"
FILE_PATH="$(printf '%s' "$INPUT" | python3 -c '
import json, sys
print(json.load(sys.stdin).get("tool_input", {}).get("file_path", ""))
')"

4. Configuration

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write|MultiEdit",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/swift-format.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}
  • matcher is a regex over the tool name. Omit it to match every invocation of the event.
  • Always use $CLAUDE_PROJECT_DIR for paths — a relative path breaks the moment the working directory differs.
  • Set a timeout. A hook that hangs blocks the session.

5. This repository's hooks

Wired in .claude/settings.json, implemented in scripts/hooks/:

Hook Event Effect
guard-generated-files.sh PreToolUse Denies edits to the 24 generated mirror files, points at SKILL.md
sync-mirrors-on-edit.sh PostToolUse Regenerates all mirrors whenever SKILL.md changes
verify-repo.sh Stop Runs the CI checks — mirror sync, frontmatter, doc references, subagent frontmatter

Together these make an entire class of mistake impossible rather than merely discouraged: a hand-edited mirror is blocked, a forgotten sync is automatic, and a broken doc reference cannot survive the end of a turn.


6. Hooks for iOS projects

Drop-in templates live in templates/hooks/:

Hook Event Effect
swift-format.sh PostToolUse SwiftFormat + SwiftLint autocorrect on the edited file
forbid-antipatterns.sh PostToolUse Blocks SKILL.md's banned patterns with line numbers and the fix
build-check.sh Stop Builds (and tests) before the turn ends

forbid-antipatterns.sh catches, among others: DispatchQueue.main.async, Task.detached, @Observable without @MainActor, empty catch, try!, NavigationView, AnyView, fixed font sizes, live-implementation default arguments, print(), and a type named Task. Test and mock files are exempt from the app-code-only rules.

Installation is in templates/hooks/README.md.


7. Design rules

Fail fast, and say what to do. A hook that blocks without naming the fix costs a round trip and teaches nothing.

Be conservative about blocking. A false positive on exit 2 stops legitimate work. When a rule has real exceptions, either exempt them by path (as forbid-antipatterns.sh does for test and mock files) or downgrade to a warning on exit 0.

Keep them fast. PostToolUse runs on every edit. Check the single file that changed, never the whole tree.

Degrade gracefully. A missing formatter or absent toolchain is not a failure — exit 0 and say the check was skipped. build-check.sh prints UNVERIFIED rather than implying a build that never ran, which keeps it honest under the evidence contract in verification.md.

Never let a hook fabricate a pass. A check that cannot run reports that it could not run. Silence reads as success and is the one failure mode that makes the whole layer untrustworthy.


8. Anti-patterns

# 1. Using a reviewer subagent for what a grep decides.
-> Tokens spent on something free and deterministic.

# 2. Blocking without naming the fix.
echo "Not allowed" >&2; exit 2
-> The model does not know what to do next.

# 3. A PostToolUse hook that scans the whole repo.
-> Runs on every edit. Check only the changed file.

# 4. Treating a missing tool as a failure.
swiftformat ... || exit 2
-> The project may not use it. Exit 0 and skip.

# 5. A hook with no timeout.
-> One hang blocks the session.

# 6. Relative paths in the command.
"command": "./hooks/check.sh"
-> Breaks whenever cwd differs. Use $CLAUDE_PROJECT_DIR.

# 7. A check that silently passes when it could not run.
-> Reports success for work nobody verified.

# 8. Enforcing style opinions with exit 2.
-> Reserve blocking for correctness. Formatting is auto-fixed, not blocked.

Checklist for a new hook

  • [ ] The rule is genuinely deterministic — no judgment required.
  • [ ] It checks only what changed, not the whole tree.
  • [ ] Exit 2 messages name both the problem and the fix.
  • [ ] Legitimate exceptions are exempted by path, not by weakening the rule.
  • [ ] A missing tool or toolchain exits 0 and says the check was skipped.
  • [ ] A timeout is set.
  • [ ] The command uses $CLAUDE_PROJECT_DIR.
  • [ ] It has been run against both a passing and a failing input.