Claude Code Hooks Tutorial: Enforce Rules Programmatically
This claude code hooks tutorial shows architects how to intercept tool calls, normalise outputs, and enforce compliance rules that prompts alone cannot guarantee.
By Solomon Udoh · AI Architect & Certification Lead

Why do CLAUDE.md instructions get ignored?
If you have ever written a rule in CLAUDE.md in capital letters and watched Claude Code ignore it anyway, you are not alone. Instruction-following in large language models is probabilistic: a well-written system prompt might achieve 70 to 80 percent adherence on a given task, but that gap matters enormously in production. This claude code hooks tutorial explains how to close that gap by moving enforcement out of prose and into code.
Hooks are shell commands that Claude Code executes at defined points in its tool-use lifecycle. They run deterministically, every time, regardless of what the model decides. Where a CLAUDE.md rule says "please do X", a hook does X and returns a non-zero exit code if the condition is not met. The difference is the difference between a suggestion and a gate.
The CCAR-F exam (Domain 1: Agentic Architecture and Orchestration, weighted at 27%) tests exactly this judgement: knowing when to reach for prompt-based vs programmatic enforcement and why high-stakes scenarios demand the latter.
What are Claude Code hooks and how do they work?
Claude Code hooks are user-defined shell commands registered in your configuration that fire at specific lifecycle events. Per Anthropic's Claude Code documentation, four event types are available:
| Hook event | Fires when | Typical use |
|---|---|---|
PreToolUse | Before any tool call executes | Validate inputs, block forbidden operations |
PostToolUse | After a tool call returns | Normalise output, log results, trigger side effects |
Notification | When Claude Code emits a notification | Route alerts to Slack, PagerDuty, or a log file |
Stop | When the agent loop ends | Summarise session, run cleanup scripts |
Each hook receives a JSON payload on stdin describing the event. It can write to stdout, and its exit code signals Claude Code what to do next: exit 0 means proceed, exit 2 blocks the tool call and feeds your stderr message back to the model as context.
The hook pipeline architecture means you can chain multiple hooks on the same event, each responsible for a single concern. That composability is what makes hooks the right tool for compliance, not a monolithic system prompt.
How do you register a hook in Claude Code?
Hooks live in your Claude Code settings file, typically at ~/.claude/settings.json for user-level scope or .claude/settings.json at the project root for team-level scope. The three-level configuration hierarchy means project-level settings override user-level ones, which is exactly what you want for team-wide enforcement.
A minimal registration looks like this:
{"hooks": {"PreToolUse": [{"matcher": "Bash","hooks": [{"type": "command","command": "python3 /scripts/validate_bash.py"}]}],"PostToolUse": [{"matcher": ".*","hooks": [{"type": "command","command": "python3 /scripts/normalise_output.py"}]}]}}
The matcher field is a regular expression matched against the tool name. Use ".*" to catch every tool, or "Bash|Write" to target specific ones. The command string is executed in a shell, so environment variables, virtual environments, and absolute paths all work as expected.
How do you write a PreToolUse hook that blocks dangerous commands?
The most common use case is intercepting shell commands before they run. The hook receives a JSON payload on stdin; your script reads it, applies its logic, and exits with the appropriate code.
#!/usr/bin/env python3"""Block rm -rf on paths outside /tmp."""import json, sys, repayload = json.load(sys.stdin)tool_input = payload.get("tool_input", {})command = tool_input.get("command", "")DANGEROUS = re.compile(r"rm\s+-rf\s+(?!/tmp)")if DANGEROUS.search(command):print(f"Blocked: destructive rm outside /tmp: {command}", file=sys.stderr)sys.exit(2)sys.exit(0)
Exit code 2 causes Claude Code to surface your stderr message to the model as an error result, giving it the context to try a safer approach. Exit code 1 (or any non-zero code other than 2) aborts the session entirely, which is appropriate for truly unrecoverable violations.
This pattern is the practical implementation of the tool call interception hooks concept: the hook sits between the model's decision and the operating system, making enforcement unconditional.
How do you write a PostToolUse hook for data normalisation?
PostToolUse hooks for data normalisation are useful when downstream systems expect a consistent format but the model's output varies. A common scenario: a file-write tool produces JSON with inconsistent key casing, and your pipeline requires snake_case throughout.
#!/usr/bin/env python3"""Normalise JSON output keys to snake_case after any Write tool call."""import json, re, sysdef to_snake(s):s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s)s = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", s)return s.lower()def normalise_keys(obj):if isinstance(obj, dict):return {to_snake(k): normalise_keys(v) for k, v in obj.items()}if isinstance(obj, list):return [normalise_keys(i) for i in obj]return objpayload = json.load(sys.stdin)tool_result = payload.get("tool_result", {})# Only act on JSON contentcontent = tool_result.get("content", "")try:data = json.loads(content)normalised = normalise_keys(data)# Write normalised version back to the output path if presentoutput_path = payload.get("tool_input", {}).get("path")if output_path:with open(output_path, "w") as f:json.dump(normalised, f, indent=2)except (json.JSONDecodeError, TypeError):pass # Not JSON; nothing to normalisesys.exit(0)
Because this hook exits 0 unconditionally, it never blocks the agent loop. It silently corrects the output and moves on. That is the right posture for normalisation: fix the problem without interrupting the workflow.
When should you use hooks versus prompt instructions?
The hooks vs prompts decision framework comes down to two questions: how often must the rule hold, and what is the cost of a single failure?
| Criterion | Use a prompt instruction | Use a hook |
|---|---|---|
| Enforcement frequency needed | Occasional guidance | Every single invocation |
| Cost of one failure | Low (style preference) | High (security, compliance, data integrity) |
| Rule complexity | Nuanced, context-dependent | Binary, deterministic |
| Auditability required | No | Yes (hook logs are machine-readable) |
| Rule changes frequently | Yes | No (requires config update) |
Hooks provide a way to execute custom shell commands at key points in Claude's operation, giving you deterministic control that prompt instructions cannot guarantee.
The high-stakes enforcement decision rule on the CCAR-F exam is consistent: when a violation would cause irreversible harm (data deletion, credential exposure, compliance breach), reach for programmatic enforcement. Prompts are appropriate for preferences; hooks are appropriate for constraints.
How do you build a compliance hook pipeline for a team?
A production hook pipeline typically layers three concerns: input validation, output normalisation, and audit logging. Each concern lives in its own script, registered on the appropriate event.
{"hooks": {"PreToolUse": [{"matcher": "Bash","hooks": [{"type": "command", "command": "python3 /hooks/validate_bash.py"},{"type": "command", "command": "python3 /hooks/check_secrets.py"}]},{"matcher": "Write|Edit","hooks": [{"type": "command", "command": "python3 /hooks/validate_file_path.py"}]}],"PostToolUse": [{"matcher": ".*","hooks": [{"type": "command", "command": "python3 /hooks/audit_log.py"}]}],"Stop": [{"matcher": ".*","hooks": [{"type": "command", "command": "python3 /hooks/session_summary.py"}]}]}}
The check_secrets.py hook deserves special attention. It should scan the command string for patterns matching API keys, tokens, and passwords before the Bash tool executes. A regex against common secret formats (AWS keys, GitHub tokens, generic sk- prefixes) catches the most common accidental exposures.
#!/usr/bin/env python3"""Block commands that appear to contain secrets."""import json, re, sysSECRET_PATTERNS = [re.compile(r"AKIA[0-9A-Z]{16}"), # AWS access keyre.compile(r"ghp_[A-Za-z0-9]{36}"), # GitHub personal access tokenre.compile(r"sk-[A-Za-z0-9]{32,}"), # Generic API key prefixre.compile(r"(?i)password\s*=\s*\S+"), # Inline password assignment]payload = json.load(sys.stdin)command = payload.get("tool_input", {}).get("command", "")for pattern in SECRET_PATTERNS:if pattern.search(command):print("Blocked: potential secret detected in command.", file=sys.stderr)sys.exit(2)sys.exit(0)
Commit these scripts to version control alongside your .claude/settings.json. The version control implications of your hook configuration are significant: a hook that exists only on one developer's machine provides no team-wide guarantee.
How do hooks interact with agentic and multi-agent workflows?
In a multi-agent system, hooks become even more valuable because the surface area for unexpected behaviour grows with each subagent. A coordinator spawning subagents via the Task tool cannot rely on each subagent's system prompt to enforce the same rules. Hooks registered at the project level apply to every tool call in every session that uses that configuration.
This connects directly to the compliance hook scenario application pattern: define your enforcement rules once in hooks, then let the coordinator and all subagents inherit them automatically. The alternative, duplicating rules in every subagent's system prompt, is fragile and hard to audit.
For the CCAR-F exam, expect scenarios where a multi-agent pipeline has a compliance requirement (for example, all file writes must be to an approved directory). The correct answer will favour a hook over a prompt instruction because hooks are deterministic and auditable.
Hooks run independently of the model's reasoning, which means they enforce constraints even when the model is operating in an extended agentic loop with reduced human oversight.
What are the most common hook implementation mistakes?
The hook implementation patterns concept covers several failure modes worth knowing before the exam and before production deployment.
| Mistake | Consequence | Fix |
|---|---|---|
| Forgetting to handle stdin read errors | Hook crashes silently; Claude Code may proceed or abort unpredictably | Wrap json.load(sys.stdin) in a try/except |
| Using exit code 1 for recoverable errors | Aborts the entire session instead of feeding context back to the model | Use exit code 2 for recoverable blocks |
| Writing hooks that are slow (over 5 seconds) | Adds latency to every tool call in the session | Keep hooks fast; offload heavy work asynchronously |
| Registering hooks only at user scope | Team members without the hook config bypass enforcement | Commit hooks to project-level .claude/settings.json |
| Not logging hook decisions | No audit trail; impossible to debug enforcement gaps | Write structured JSON logs from every hook |
The exam consistently rewards proportionate fixes. A hook that exits 2 with a clear error message is proportionate; a hook that exits 1 and kills the session for a minor formatting issue is not.
How do you test hooks before deploying them?
Testing hooks locally before committing them is straightforward because they are just shell commands that read JSON from stdin.
# Simulate a PreToolUse event for a Bash tool callecho '{"tool_name":"Bash","tool_input":{"command":"rm -rf /important"}}' \| python3 /hooks/validate_bash.pyecho "Exit code: $?"
For PostToolUse hooks, construct a representative payload that mirrors what Claude Code actually sends:
echo '{"tool_name": "Write","tool_input": {"path": "/tmp/test.json"},"tool_result": {"content": "{\"camelCaseKey\": \"value\"}"}}' | python3 /hooks/normalise_output.py
Run these tests in CI alongside your application tests. Because hooks are plain Python or Bash scripts, they integrate naturally into any test framework. A hook that fails its unit tests should block the pull request, not the production agent loop.
The broader Claude Code Configuration and Workflows domain (weighted at 20% of the CCAR-F exam) treats testability as a first-class concern. Hooks that cannot be tested independently are a configuration smell.
Preparing for CCAR-F hook questions
Domain 1 (Agentic Architecture and Orchestration, 27%) and Domain 3 (Claude Code Configuration and Workflows, 20%) together account for nearly half the CCAR-F exam. Hook-related scenarios appear in both. The exam will present a situation, describe a compliance or reliability requirement, and ask you to choose between a prompt-based and a programmatic solution.
The pattern to internalise: if the requirement is binary, high-stakes, or must hold 100% of the time, the answer is a hook. If the requirement is nuanced, context-dependent, or a style preference, a prompt instruction is appropriate. Our concept library at /concepts maps all 174 atomic concepts to the five exam domains, including the full set of hook-related patterns tested in Domain 1.
AI Skill Certs is an independent prep platform and is not affiliated with or endorsed by Anthropic. Our CCAR-F practice exams mirror the real format: 60 scenario-based items, scored 100 to 1000, with 720 as the passing bar.
Frequently asked questions
What exit code should a Claude Code hook return to block a tool call?
Where do Claude Code hooks get registered?
Can Claude Code hooks run on every tool call, not just Bash?
Do Claude Code hooks apply inside multi-agent subagent sessions?
How do I test a Claude Code hook without running a full Claude Code session?
Which CCAR-F exam domains cover Claude Code hooks?
People also ask
What are Claude Code hooks used for?
How do Claude Code hooks differ from CLAUDE.md instructions?
Can Claude Code hooks block tool calls?
Are Claude Code hooks supported in CI/CD pipelines?
How many hook event types does Claude Code support?
About the author
AI Architect & Certification Lead
Solomon Udoh is an AI Architect who designs and ships production agent systems on the Claude API and Claude Code. He built AI Skill Certs' adaptive engine and authored its 174-concept knowledge graph, mapping every Claude Certified Architect - Foundations objective to hands-on, exam-aligned practice.
- Designs production multi-agent systems on the Claude API and Agent SDK
- Author of the AI Skill Certs knowledge graph (174 mapped exam concepts)
- Builds with MCP, Claude Code, structured outputs, and agentic loops daily
- Reviews every concept page against the official Anthropic exam guide
You might also like
Ready to put it into practice?
Study every exam concept with an adaptive tutor.