Deterministic automation that Claude cannot ignore — shell commands, HTTP calls, and LLM prompts that fire at lifecycle events to enforce formatting, block dangerous operations, and audit every action.
The enforcement gap
Every layer you have learned so far — CLAUDE.md, rules, skills, agents — works by giving Claude instructions and trusting it to follow them. Claude is very good at following instructions. But "very good" is not "guaranteed."
A CLAUDE.md instruction that says "always run the linter after editing TypeScript files" works most of the time. But in a long session, in the middle of a complex multi-file refactoring, Claude might skip it. Not maliciously — it simply did not prioritise that instruction over the immediate task. The linting instruction was one of 200 lines competing for attention.
Hooks close this gap. A hook is a deterministic automation that fires at a specific lifecycle event in Claude Code. It does not depend on Claude choosing to follow an instruction. It executes automatically, every time, at the exact moment it is configured to fire.
Hooks can:
Block actions — prevent Claude from executing a tool call entirely
Validate output — check that a file edit meets standards before it is committed to the conversation
Run side effects — trigger linting, formatting, notifications, logging
Inject context — add information to Claude's context based on what is happening
The mental model: if CLAUDE.md is the employee handbook and skills are the standard operating procedures, hooks are the automated compliance checks. They run whether or not anyone remembers to run them.
?
Has Claude ever done something in your project that it was 'supposed to' not do?
Configuration and structure
Hooks are configured in settings files, not in markdown files like skills and agents:
Location
Scope
.claude/settings.json
Project-level — version-controlled, shared with team
.claude/settings.local.json
Project-level — gitignored, personal
~/.claude/settings.json
User-level — applies to all projects
Managed policy settings
Organisation-level — cannot be overridden
Hooks can also be defined inside skill and agent frontmatter, scoped to that component's lifetime.
Event name — when to fire (PreToolUse, PostToolUse, SessionStart, etc.)
Matcher — which specific tool, file, or trigger to match
Hook handlers — what to execute (command, HTTP, prompt, or agent)
When hooks fire
Claude Code has dozens of hook events. Here are the ones that matter most for developer workflows.
Tool execution events — the workhorses:
Event
Fires when
Can block?
Use for
PreToolUse
Before any tool runs
Yes
Blocking dangerous commands, validating inputs
PostToolUse
After any tool completes
No
Linting after edits, logging, notifications
PermissionRequest
When Claude asks for tool permission
Yes
Auto-approving or auto-denying specific operations
Session events:
Event
Fires when
Use for
SessionStart
Session begins (startup, resume, clear)
Environment setup, context injection
UserPromptSubmit
You send a message
Prompt validation, context enrichment
Stop
Claude finishes a response
Final checks, summary generation
File events:
Event
Fires when
Use for
FileChanged
A file is created, modified, or deleted
Auto-formatting, validation, notifications
The matcher field on tool events filters by tool name. "matcher": "Edit" means the hook only fires on Edit tool calls. "matcher": "Bash" fires on Bash. You can also use regex patterns: "matcher": "Edit|Write" fires on either.
The critical behaviour of PreToolUse: A PreToolUse hook that returns exit code 2 (for command hooks) or a block decision prevents the tool from executing entirely. Claude receives feedback that the action was blocked and why, and it must adjust its approach. This is the mechanism for hard enforcement.
The endpoint receives the event payload and returns a JSON response with a decision. Useful for integrating with external systems — CI/CD, audit logging, notification services.
3. Prompt hooks — single-turn LLM evaluation:
{ "type": "prompt", "prompt": "Is this bash command safe to execute in a development environment? The command is: $ARGUMENTS", "model": "claude-haiku-4-5"}
A fast, lightweight LLM call that returns a yes/no decision with reasoning. Uses a small model by default for speed. Useful for nuanced evaluations that are hard to express as shell script logic.
4. Agent hooks — multi-turn subagent with tool access:
{ "type": "agent", "prompt": "Verify that this code change follows our security standards. $ARGUMENTS", "model": "claude-sonnet-4-6"}
The most powerful handler. A subagent can read files, run commands, and make a complex decision. Use sparingly — agent hooks are slower than command hooks.
?
You want to automatically format TypeScript files after every edit. Which hook configuration handles this?
Three hooks you should configure
Hook 1: Auto-lint after edits
The most common hook. Runs your linter after every file edit so you never commit unlinted code:
The || true ensures the hook does not block even if linting finds unfixable issues — it formats what it can and lets Claude continue.
Hook 2: Block dangerous bash commands
Prevent Claude from running destructive commands:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "prompt", "prompt": "Evaluate if this bash command is safe for a development environment. Block if it: deletes files outside the project, force-pushes to main/master, drops databases, or modifies system configuration. The command: $ARGUMENTS", "model": "claude-haiku-4-5" } ] } ] }}
This uses a prompt hook — a fast LLM call that evaluates the command semantically. It catches rm -rf /, git push --force origin main, DROP TABLE, and similar dangerous patterns even if they are disguised or combined with other commands.
Hook 3: Protected files
Prevent edits to specific files that should not be modified during normal development:
{ "hooks": { "PreToolUse": [ { "matcher": "Edit", "if": "Edit(.env*)", "hooks": [ { "type": "command", "command": "echo 'BLOCKED: Cannot edit .env files through Claude Code. Manage environment variables through your secrets manager.' >&2; exit 2" } ] } ] }}
Exit code 2 blocks the action. The stderr message is sent to Claude as feedback, so it knows why the edit was blocked and can adjust.
Component-scoped hooks
Hooks defined in skill and agent frontmatter are scoped to that component's lifetime. They activate when the skill loads or the agent starts, and deactivate when it completes.
This deploy skill logs every bash command it runs and records completion in a log file. These hooks only fire while the deploy skill is active — they do not affect normal development work.
For agents, a Stop hook in the frontmatter automatically converts to SubagentStop, firing when the agent completes. Use this for cleanup operations — removing temp files, posting results to a channel, updating a status dashboard.
✎
Module 7 — Final Assessment
1
Your CLAUDE.md says 'always run tests after modifying a source file.' In a long session, Claude occasionally skips this step. What is the correct fix?
2
A PreToolUse hook on Bash returns exit code 2 with the message 'Command blocked: force push to protected branch'. What happens?
3
You want to evaluate whether a bash command is safe, but the safety criteria are nuanced (context-dependent, not a simple pattern match). Which hook handler type fits best?
4
Hooks defined in a skill's frontmatter behave differently from hooks in settings.json. How?