PRACTICE · 2 hours
How I use Claude Code hooks in my own SDLC harness to automate safety, observability, and parallel-agent coordination
When I started running Claude Code agents autonomously overnight, I kept discovering problems the morning after — a file written to the wrong place, a shell command that ran when it shouldn't have, a long session I had no way of knowing had finished. My SDLC harness runs multiple agents in parallel git worktrees, each implementing a different task from a dependency-ordered wave. Without visibility into what each agent was actually doing, I was flying blind.
Claude hooks changed that. They give me five specific points to intercept, observe, or block agent activity: before a tool runs, after it runs, when Claude needs attention, when a session ends, and when a sub-agent finishes. I use them every day in production. This module walks through exactly what I built and why, starting from the simplest safety check and building to a complete coordination layer.
What we're building: A hook configuration grounded in a real multi-agent SDLC harness running on a Mac Mini behind a Cloudflare Tunnel — not a toy example, but the actual structure I use when spawning parallel coding agents.
Think of hooks as the circuit breakers and observability layer of your agent pipeline. Without them, an agent is a black box — you see input and output, but nothing in between. With hooks, every tool call becomes an observable, interceptable event.
The fundamental design is simple: hooks run as shell commands. They read context from environment variables, can write to stdout for logging, and control execution by their exit code. A non-zero exit from a PreToolUse hook blocks the tool entirely. That single rule — exit code determines whether the tool runs — unlocks an enormous amount of control without modifying Claude Code itself.
Claude Code fires five hook events at specific lifecycle points. Each provides a different set of environment variables and a different kind of control.
PreToolUse fires before any tool runs. A non-zero exit code blocks the tool from executing. The hook receives TOOL_NAME (such as Bash or Edit) and TOOL_COMMAND for bash invocations. This is where safety logic lives.
The matcher field is a regex matched against the tool name. Use "Bash" for shell commands, "Edit|Write" for file operations, or ".*" to intercept everything.
PostToolUse fires after a tool completes. The exit code has no effect — the tool already ran. This is the right place for logging and observability.
Notification fires when Claude pauses mid-session waiting for the user. I wire this to macOS's built-in say command so I get an audio alert when I've stepped away from a long run.
Stop fires when Claude finishes its response. I use it to save the session transcript and run a final lint check to catch anything the agent missed.
SubagentStop fires when a sub-agent completes in a multi-agent session. In my SDLC harness, each parallel worktree runs as a sub-agent. When one finishes, SubagentStop fires immediately — no polling needed.
Pattern: Use SubagentStop to track parallel task progress. Each worktree completion fires the hook, letting you build a simple counter — "3 of 5 tasks done" — without any separate monitoring infrastructure.
Knowledge checkpoint: Which hook event blocks a tool from running when it exits non-zero — PreToolUse or PostToolUse? Which would you use to log files modified during a session without affecting agent execution?
Safety hooks share a common structure: read tool context from environment variables, evaluate against a rule, and exit non-zero to block if the rule fails.
The simplest safety check blocks specific dangerous patterns. This is where to start — easy to write, easy to audit, and catches the most common mistakes.
Two things to notice: the early exit for non-Bash tools (you don't want to block file reads), and the minimal stdout output (useful for debugging when you review logs).
A blocklist catches known-bad commands but misses novel ones. In my SDLC harness, where agents work in parallel worktrees, I want confidence that only recognized commands run unsupervised. An allowlist inverts the logic: only commands matching known-safe patterns are allowed through automatically.
Start with the blocklist: An overly strict allowlist will block legitimate agent work and cause the agent to retry or give up. Build up the allowlist gradually by reviewing blocked commands in your logs and adding patterns for the legitimate ones.
Safety controls protect you from bad outcomes. Observability helps you understand what actually happened in a session. In a multi-agent system, this matters even more — which agent wrote which file, when, and in what order?
A JSONL log (one JSON object per line) is easy to stream, grep, and parse. I attach this to every Edit and Write operation to build a complete audit trail of each session.
Using Path.home() / '.claude' / 'logs' keeps hook logs in a consistent location across machines. The mkdir(parents=True, exist_ok=True) call means the hook works even on a fresh machine.
For sessions with many tool calls, scanning raw log entries is tedious. I add a summarization step using claude-haiku-4-5 — inexpensive enough to run on every tool call and fast enough not to slow the agent down.
Cost at scale: Claude Haiku 4.5 costs $1 per million input tokens. A full day of active multi-agent work might generate 300-500 hook calls — well under a dollar — and the human-readable summaries make log review meaningfully faster than scanning raw command strings.
With safety and logging in place, hooks can handle coordination tasks across the whole agent system. Here is the pattern I use for tracking progress across parallel worktrees.
My SDLC harness runs 4-6 agents in parallel, each in an isolated git worktree, each implementing a different task from a dependency-ordered wave. When any agent finishes, I want an immediate notification and an updated count.
All five hooks wired together. I keep this in ~/.claude/settings.json so it applies globally across all projects on my Mac Mini.
uv run instead of python: Each hook script can declare inline dependencies in a # /// script block. uv run installs them on first use. No virtual environment management needed — hooks stay self-contained and portable across machines.
Reflective prompt: Think about your own agent workflows. What information would you need to answer "why did this agent take so long on task 3?" — and which hook events would give you those signals? What would you change about the configuration above if your worktrees needed to coordinate file access rather than just tracking completion?
Build a hook configuration for a project you are actively working on. Three concrete targets:
Safety: Write a PreToolUse hook that logs but does not block unrecognized commands by default. Add a --strict flag that switches it to blocking mode. This gives you "warn mode" for exploration and "enforce mode" for production runs.
Observability: Extend the file change logger to also record a size_bytes field — the file size after the write. This lets you see at a glance which files had large changes versus small ones.
Coordination: Write a Stop hook that appends a one-line session summary to a project-level agent-log.txt: session ID, timestamp, and the number of files written (read from the JSONL change log the PostToolUse hook maintains).
Extension challenge: Wire these three hooks together so the Stop hook reads the JSONL change log, computes the most-modified file for the session, and includes it in the summary line.
1. A PreToolUse hook script exits with code 0. What happens to the tool call?
Correct Answer: B
Exit code 0 means success — the tool runs. A non-zero exit code (like sys.exit(1)) blocks the tool. This is standard Unix convention: 0 = success/allow, non-zero = failure/block. PreToolUse hooks use this to gate tool execution.
2. You want to log every Bash command that runs during a session, including ones you are not blocking. Which hook is most appropriate?
Correct Answer: B
PostToolUse is right for logging what actually ran. If a PreToolUse safety hook blocks a command, you would log an event that never executed. PostToolUse fires only after successful execution, so the log accurately reflects completed actions.
3. Your allowlist safety hook is blocking commands the agent legitimately needs. What is the most principled fix?
Correct Answer: C
The right fix is to review your observability logs and add specific patterns. Switching to a blocklist trades safety for permissiveness — it only catches known-bad patterns. Disabling the hook removes all protection. Timeouts are irrelevant: the hook is blocking on policy evaluation, not elapsed time.
4. In a multi-agent session with 5 parallel tasks running as sub-agents, which hook fires when each individual task completes?
Correct Answer: B
SubagentStop fires specifically when a sub-agent completes in a multi-agent session. Stop fires only at the end of the main orchestrating session. For tracking progress across parallel tasks, SubagentStop gives you per-completion signals you can count and aggregate.
5. The code below raises an AttributeError at runtime. What is the correct replacement?\n\nclient = anthropic.Client()
Correct Answer: B
The Anthropic Python SDK uses anthropic.Anthropic() — not anthropic.Client(). The Client class does not exist in SDK v1.x. The correct pattern is: from anthropic import Anthropic; client = Anthropic(). The constructor reads ANTHROPIC_API_KEY from the environment by default.
6. A PostToolUse hook exits with code 1 after logging a file write. What is the effect on the file that was written?
Correct Answer: B
PostToolUse hooks run after the tool has already executed and cannot affect it. The exit code has no effect on the completed write. This is why PostToolUse is the right hook for logging and alerting — and why PreToolUse is required if you need to prevent an operation before it happens.