Before we dive in: I share practical insights like this weekly. Join developers and founders getting my newsletter with real solutions to engineering and business challenges.
I've been spending the majority of my development time in the terminal lately, and Claude Code has become a core part of that workflow. I wrote about managing multiple Claude Code sessions previously, and while exploring the tool further I stumbled into a feature that completely changes how I think about AI-assisted development: hooks.
The problem hooks solve is simple but important. When you're using Claude Code in any kind of autonomous mode, you're trusting the agent to make good decisions about your codebase. CLAUDE.md files and prompt-based rules help guide that behaviour, but they're ultimately suggestions that the model can choose to ignore. I've had Claude Code cheerfully reformat files I didn't want touched, run commands I'd rather it hadn't, and decide it was "done" when it clearly wasn't. Hooks fix all of this by giving you hard, deterministic control over what happens at every stage of the agent's lifecycle.
What Hooks Actually Are
Think of hooks like git hooks, but for your AI agent. You define handlers - shell commands, LLM prompts, or even sub-agents - that fire automatically at specific points during Claude Code's execution. Before a tool runs, after a file gets edited, when the agent decides to stop, when a session starts. There are now 15 different lifecycle events you can hook into.
The critical difference from prompt-based rules is that hooks are guaranteed to execute. If you tell Claude Code in your CLAUDE.md not to modify .env files, it will probably listen. If you set up a PreToolUse hook that blocks writes to .env files, it will always block them. For anyone working on production codebases or in regulated environments, that distinction between "probably" and "always" is everything.
The Use Cases That Got Me Excited
Before getting into the technical details, I want to walk through the scenarios that made me realise how much value hooks can add to a typical development workflow.
Your code stays formatted without you thinking about it. Every time Claude Code writes or edits a file, a PostToolUse hook can run your formatter automatically. No more reviewing diffs full of formatting inconsistencies, no more manually running gofmt or prettier after every change. Boris Cherny, who created Claude Code, uses exactly this setup. One important gotcha though: if your formatter changes files, Claude receives a system reminder about those changes every time, which eats into your context window. The smarter approach is to format on commit via a Stop hook rather than after every individual edit.
Dangerous commands get blocked before they execute. I wrote about the risks of dangerously-skip-permissions previously, and hooks provide a much better solution to the underlying problem. Rather than giving Claude Code blanket permission and hoping for the best, you set up a PreToolUse hook that inspects every bash command before it runs. If it matches a dangerous pattern like rm -rf / or piping curl output directly into bash, it gets blocked and Claude receives the error message explaining why. You get the speed of autonomous operation with actual safety guardrails.
The agent can't claim it's done until the work is actually done. This one genuinely changed my workflow. The Stop event fires whenever Claude finishes responding, and if your hook returns a blocking signal, Claude is forced to keep working. Combine this with a prompt hook - where a separate, fast model evaluates whether all requested tasks were completed and tests were run - and you get an AI agent that verifies its own work before stopping. I've found the vibe coding paradox applies heavily here: the more you can automate the verification of AI-generated code, the better the output quality.
You get notified when Claude needs attention. If you're running a long task and switch to another window, a Stop or Notification hook can fire a desktop notification when Claude finishes or needs input. Nothing fancy - just osascript on macOS or notify-send on Linux - but it means you stop checking back every thirty seconds.
Project context gets injected automatically at session start. SessionStart hooks fire when Claude Code starts, resumes, or clears a session, and their stdout gets injected as context for Claude. You could pull in your current git branch, recent commit messages, open issues, environment details, or any project-specific context that helps Claude understand what you're working on. No more starting every session with a paragraph explaining where you left off.
Protected files and directories stay protected. If certain files should never be modified by an AI agent - configuration files, migration files, anything with secrets - a PreToolUse hook can enforce that absolutely. Claude gets a clear error message explaining why the edit was blocked, and you never have to worry about accidentally accepting a change to something sensitive.
Where Hooks Live
Configuration is stored in JSON settings files at four levels. ~/.claude/settings.json applies globally across all projects. .claude/settings.json is project-level and gets version controlled, which is great for sharing hooks with your team. .claude/settings.local.json handles local overrides that stay gitignored. Enterprise managed policy covers organisation-wide enforcement.
One security detail worth noting: Claude Code snapshots your hook configuration at startup and uses that snapshot for the entire session. Edits mid-session have no effect, which prevents any modification of hooks while the agent is running. You can also manage hooks interactively through the /hooks slash command.
The Technical Details
The basic configuration structure nests hook arrays under event names with optional matchers for filtering. A matcher accepts regex patterns against tool names: "Edit|Write" catches both tools, "Bash(npm test*)" matches specific command arguments, and "mcp__memory__.*" targets MCP server tools. Omitting the matcher matches everything.
A simple auto-formatting hook looks like this:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [
{ "type": "command", "command": "bun run format || true" }
]
}
]
}
}
For Go projects I'd swap that for gofmt -w against the edited file path, which you can extract from the JSON that gets passed to stdin.
Command hooks communicate through exit codes: exit 0 means success, exit 2 is a blocking error where stderr gets fed back to Claude as an error message, and any other exit code is a non-blocking warning. For more sophisticated control, you can return structured JSON on stdout:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Writing to .env files is prohibited"
}
}
PreToolUse hooks can also modify tool arguments through the updatedInput field before execution, which enables things like path correction or secret redaction without the model knowing anything changed.
A dangerous command blocker as a bash script:
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
if echo "$COMMAND" | grep -qE "rm -rf[[:space:]]+(/|~|\$HOME)|:(){ :|:& };:"; then
echo "Blocked: dangerous command pattern detected" >&2
exit 2
fi
exit 0
The TDD enforcement hook using a prompt handler, which delegates the decision to a fast model:
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Analyze this context: $ARGUMENTS. Are all tasks complete and were tests run and passing? Respond with {\"decision\": \"approve\"} or {\"decision\": \"block\", \"reason\": \"explanation\"}.",
"timeout": 30
}
]
}
]
}
}
The Three Handler Types
Claude Code uniquely offers three types of hook handlers, which is more than any competing tool.
Command hooks run shell commands as child processes. They receive JSON on stdin with the session ID, transcript path, working directory, tool name, input parameters, and tool response for post-execution hooks. These are what you'll use most of the time.
Prompt hooks send a text prompt to a fast Claude model (Haiku by default, configurable) for single-turn semantic evaluation. The $ARGUMENTS placeholder injects the hook's input JSON. This is how you get intelligent, context-aware decisions without writing custom scripts. The TDD enforcement example above uses this.
Agent hooks spawn a sub-agent with access to tools like Read, Grep, and Glob for multi-turn codebase verification. This is the heaviest handler type, suitable for deep validation like confirming that all modified files have corresponding test coverage. No other AI coding tool offers anything like this.
The 15 Lifecycle Events
The full list of events covers every stage of the agent's execution. PreToolUse and PostToolUse handle the before and after of tool execution. PostToolUseFailure fires on tool errors. PermissionRequest fires when the user would normally see a permission dialog.
SessionStart and SessionEnd handle session lifecycle, with SessionStart being particularly useful because its stdout becomes Claude's context. Stop and SubagentStop fire when the agent or sub-agent finishes, and can force continuation. SubagentStart fires when a sub-agent spawns.
UserPromptSubmit fires when you submit a prompt and can inject context or block it. PreCompact fires before conversation compaction. Notification fires on system notifications. TeammateIdle, TaskCompleted, and Setup round out the newer additions for multi-agent and initialisation workflows.
How This Compares to Other Tools
Cursor added hooks in v1.7 with 6 events and command-only handlers. GitHub Copilot supports 5 events. Cline uses file-based hook discovery with 4 events. Windsurf, Continue.dev, and Aider have no hook support at all, relying entirely on prompt-level rules.
Claude Code's 15 events and three handler types make it the most comprehensive system available. The prompt hook type is the standout: no other tool lets you delegate hook decisions to an LLM without writing custom code. The agent hook type, spawning sub-agents with tool access for deep verification, is similarly unique.
The Growing Ecosystem
The community around hooks is developing quickly. The disler/claude-code-hooks-mastery repo on GitHub has over 3,000 stars and includes hooks for all events, security validation, and observability. lasso-security/claude-hooks focuses on prompt injection defence, scanning outputs for instruction override attempts and encoded payloads. GitButler has one of the most sophisticated production deployments, using hooks to automatically manage Git branches and commits as Claude works. SDKs exist in Python, TypeScript, PHP, and Ruby for cleaner APIs than raw stdin/stdout handling.
Getting Started
The simplest approach is to add a few high-value hooks to your .claude/settings.json and commit them to your project. Start with auto-formatting on PostToolUse, dangerous command blocking on PreToolUse, and desktop notifications on Stop. Those three cover the most ground for the least effort. From there, prompt hooks for task verification and SessionStart hooks for injecting project context take things further.
The documentation lives at docs.claude.com/en/docs/claude-code/hooks for the reference and the hooks guide for practical examples. Both are worth reading through.
I'll be writing a follow-up once I've had more time to integrate hooks across my projects, particularly around using SessionStart to inject Cont3xt context and prompt hooks for automated quality checks on Go code. The combination of auto-formatting and command blocking has already made a noticeable difference, and the TDD enforcement pattern feels like it could fundamentally change how I use Claude Code for anything non-trivial.
