Method·9 min read·21 July 2026

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

Claude Code Hooks Tutorial: Enforce Rules Programmatically

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 eventFires whenTypical use
PreToolUseBefore any tool call executesValidate inputs, block forbidden operations
PostToolUseAfter a tool call returnsNormalise output, log results, trigger side effects
NotificationWhen Claude Code emits a notificationRoute alerts to Slack, PagerDuty, or a log file
StopWhen the agent loop endsSummarise 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:

json
{
"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.

python
#!/usr/bin/env python3
"""Block rm -rf on paths outside /tmp."""
import json, sys, re
payload = 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.

python
#!/usr/bin/env python3
"""Normalise JSON output keys to snake_case after any Write tool call."""
import json, re, sys
def 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 obj
payload = json.load(sys.stdin)
tool_result = payload.get("tool_result", {})
# Only act on JSON content
content = tool_result.get("content", "")
try:
data = json.loads(content)
normalised = normalise_keys(data)
# Write normalised version back to the output path if present
output_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 normalise
sys.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?

CriterionUse a prompt instructionUse a hook
Enforcement frequency neededOccasional guidanceEvery single invocation
Cost of one failureLow (style preference)High (security, compliance, data integrity)
Rule complexityNuanced, context-dependentBinary, deterministic
Auditability requiredNoYes (hook logs are machine-readable)
Rule changes frequentlyYesNo (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.

Anthropic , Claude Code Documentation

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.

json
{
"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.

python
#!/usr/bin/env python3
"""Block commands that appear to contain secrets."""
import json, re, sys
SECRET_PATTERNS = [
re.compile(r"AKIA[0-9A-Z]{16}"), # AWS access key
re.compile(r"ghp_[A-Za-z0-9]{36}"), # GitHub personal access token
re.compile(r"sk-[A-Za-z0-9]{32,}"), # Generic API key prefix
re.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.

Anthropic , Claude Code Documentation

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.

MistakeConsequenceFix
Forgetting to handle stdin read errorsHook crashes silently; Claude Code may proceed or abort unpredictablyWrap json.load(sys.stdin) in a try/except
Using exit code 1 for recoverable errorsAborts the entire session instead of feeding context back to the modelUse exit code 2 for recoverable blocks
Writing hooks that are slow (over 5 seconds)Adds latency to every tool call in the sessionKeep hooks fast; offload heavy work asynchronously
Registering hooks only at user scopeTeam members without the hook config bypass enforcementCommit hooks to project-level .claude/settings.json
Not logging hook decisionsNo audit trail; impossible to debug enforcement gapsWrite 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.

bash
# Simulate a PreToolUse event for a Bash tool call
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /important"}}' \
| python3 /hooks/validate_bash.py
echo "Exit code: $?"

For PostToolUse hooks, construct a representative payload that mirrors what Claude Code actually sends:

bash
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?
Return exit code 2 to block a tool call and feed your stderr message back to the model as context, allowing it to try a different approach. Exit code 1 aborts the entire session, which is appropriate only for unrecoverable violations. Exit code 0 always means proceed normally.
Where do Claude Code hooks get registered?
Hooks are registered in the Claude Code settings file: ~/.claude/settings.json for user-level scope, or .claude/settings.json at the project root for team-level scope. Project-level settings override user-level ones, so committing hooks to the project config ensures every team member runs the same enforcement rules.
Can Claude Code hooks run on every tool call, not just Bash?
Yes. Set the matcher field to ".*" (a regular expression that matches any string) and the hook fires on every tool call regardless of type. You can also target specific tools by name, such as "Bash|Write|Edit", to apply different hooks to different tool categories.
Do Claude Code hooks apply inside multi-agent subagent sessions?
Project-level hooks apply to every tool call in every session that uses that project configuration, including subagent sessions spawned by a coordinator. This makes hooks more reliable than duplicating rules in each subagent's system prompt, which is fragile and hard to audit across a large pipeline.
How do I test a Claude Code hook without running a full Claude Code session?
Pipe a representative JSON payload into your hook script directly from the command line using echo and a shell pipe. Because hooks are plain shell commands that read from stdin and write to stdout/stderr, they can be unit-tested with any standard test framework and integrated into CI pipelines independently of Claude Code itself.
Which CCAR-F exam domains cover Claude Code hooks?
Hook-related content appears primarily in Domain 1 (Agentic Architecture and Orchestration, 27%) and Domain 3 (Claude Code Configuration and Workflows, 20%), which together account for 47% of the exam. Expect scenario-based questions asking you to choose between prompt instructions and programmatic hook enforcement for compliance and reliability requirements.

People also ask

What are Claude Code hooks used for?
Claude Code hooks are shell commands that execute at defined lifecycle events (PreToolUse, PostToolUse, Notification, Stop) to enforce rules deterministically. Common uses include blocking dangerous shell commands, normalising tool output, logging audit trails, and enforcing compliance requirements that prompt instructions cannot guarantee with 100% reliability.
How do Claude Code hooks differ from CLAUDE.md instructions?
CLAUDE.md instructions are probabilistic: the model may follow them 70 to 80 percent of the time depending on context. Hooks are deterministic shell commands that run every time a matching tool event fires, regardless of model reasoning. For high-stakes or binary requirements, hooks are the correct enforcement mechanism.
Can Claude Code hooks block tool calls?
Yes. A PreToolUse hook that exits with code 2 blocks the tool call and returns your stderr message to the model as an error result, giving it context to try a safer approach. Exit code 0 allows the call to proceed. Exit code 1 aborts the entire session, reserved for unrecoverable violations.
Are Claude Code hooks supported in CI/CD pipelines?
Yes. Because hooks are plain shell scripts registered in a JSON config file, they work in any environment where Claude Code runs, including headless CI/CD sessions invoked with the -p flag. Committing hooks to project-level .claude/settings.json ensures they apply consistently across local development and automated pipelines.
How many hook event types does Claude Code support?
Claude Code supports four hook event types: PreToolUse (fires before a tool call executes), PostToolUse (fires after a tool call returns), Notification (fires when Claude Code emits a notification), and Stop (fires when the agent loop ends). Each event type accepts multiple hooks matched by tool-name regular expressions.

About the author

Solomon Udoh

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.

Start studying