IDInternals Decoded
Claude Code, Mastered
PlaybooksIntermediate10 min readJun 2026

Hooks: Run Your Rules on Every Action

Lint on every edit, guard dangerous commands, and wire Claude Code into your standards.

Part 4 of 10Claude Code, MasteredView series →

Last time, we turned repeated instructions into reusable skills. Now we lock down what Claude Code is actually allowed to do. Hooks are deterministic event handlers that run your own code at fixed points in every conversation turn, tool call, or session boundary. They can block dangerous actions, auto-format files, inject context, or override tool inputs before the model even sees them.

A surprising truth: hooks are not suggestions to the model. They are enforced by the agent harness, outside the LLM (large language model) entirely. If the model hallucinates a harmful command, your PreToolUse hook can kill it dead, regardless of how confident the model sounds.

What exactly are hooks and why do I need them?

Hooks are external scripts that Claude Code runs automatically when specific lifecycle events fire. You define them in a JSON (JavaScript Object Notation) configuration file, and the harness delivers a structured JSON payload that describes what is happening. Your script can inspect that data, make a decision, and return an exit code plus optional JSON to allow, block, or modify the original action.

This is a fundamentally different control plane from CLAUDE.md instructions or permission rules. Instructions are probabilistic guidance the model might ignore under pressure. Permission rules are static pattern matches on tool names or paths. Hooks run arbitrary code at enforced boundaries, so you can check the actual contents of a file about to be written, call an external policy server, or check an environment variable, and then make a binding decision. They are what turns “LLM you have to babysit” into “agent you can safely delegate to.”

How do hooks differ from CLAUDE.md instructions or permission rules?

Instructions live inside prompts. They are the best-effort text you put in your CLAUDE.md or system prompt to steer behavior. The model can lose them when the context is compacted, or it can misgeneralize and still violate them. Permission rules add a declarative allow or deny on tool names and file globs, but they have no access to run-time data and no ability to modify a tool call before it goes through.

Hooks exist beside both. A PreToolUse hook sees the tool name, arguments, and sometimes the file contents. It can run a linter or a security scanner, and if those fail, the tool call never happens. A PostToolUse hook can rewrite the output, log it, or trigger an automatic reformat. A UserPromptSubmit hook can inject missing context before the model even starts reasoning. The key difference is computability: hooks are code, not static rules, and they run in a harness that can enforce the outcome.

An analogy: if instructions are like telling your intern to “please don’t touch production,” hooks are the locked door with an alarm system. The alarm doesn’t care what was said. It cares what is about to happen.

How does the lifecycle work and when do hooks fire?

Claude Code fires hook events at three cadences: once per session, once per turn, and on every tool call inside the agent loop.

Session-level events like SessionStart and SessionEnd fire when a conversation begins or terminates. They are ideal for loading project rules or cleaning up caches. Turn-level events fire at the start and end of each user turn: UserPromptSubmit captures the raw input, and Stop or StopFailure fire when the model tries to finish responding. Tool-level events surround every tool invocation: PreToolUse, PostToolUse, PostToolUseFailure, and PermissionRequest give you a chance to inspect, block, or alter every file write, shell command, or read operation.

Each event triggers any configured matcher groups. A group can contain one or more handlers that run in a defined order. The harness serializes the full event context into a JSON object and passes it to your handler via stdin (for command hooks) or an HTTP POST body. The handler’s response then determines what happens next.

What types of handlers can I write?

You have four main options to implement a hook handler.

A command hook is a local shell script that reads the JSON payload from stdin. It writes its decision as JSON on stdout and exits with a status code. This is the most common type for CI-like automation.

An HTTP hook sends an HTTP POST to a URL you control. The event payload is the request body, and the handler’s response follows the same JSON schema. This works well when you have a centralized policy service or want to integrate with existing webhooks.

A prompt-LLM hook sends the event to another LLM call to assess the situation. For a Stop event, you might ask a secondary model, “Is the task really complete?” and decide whether to let the agent stop.

An agent hook spawns a subagent with read-only tools (like Read, Grep, Glob) to investigate the state of the repository before making a decision. You’d use this when you need a deep code analysis that a simple script can’t do without building context.

How does a hook actually make a decision?

The communication protocol is simple. The handler receives a JSON blob with fields like tool_name, tool_input, tool_output, session_id, and prompt. It processes that data, then responds with a JSON object that can include a decision field.

Exit code 2 means “block this action.” The harness will abort the tool call and pass a denial message to the model. Exit code 0 means “allow,” but the harness also reads stdout. If stdout contains a JSON object with an augment or override field, the harness merges those changes into the tool’s input before execution. For a Write tool, you could override the file content to a formatted version. Any non-zero exit code other than 2 is treated as a hook error; the action may be blocked or allowed depending on configuration.

This flow is deterministic. The same payload will always produce the same result, which makes hooks testable and safe to compose.

Here is the decision flow for a PreToolUse hook:

The overrides at play are well documented in the Anthropic hooks reference. They allow a handler to rewrite arguments, inject extra context, or mark a tool as needing manual review.

What does a real hook look like in practice?

In our running web app side project, we want to stop Claude Code from ever touching .env files, and we want to auto-format any JavaScript it writes.

Start with the hook configuration in your project’s .claude/hooks.json (the path in Claude Code’s config model). The file can contain multiple matcher groups. Here is a pair of hooks that enforce both rules:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": { "toolName": "Write", "pathPattern": "**/.env*" },
        "handler": {
          "type": "command",
          "command": "echo '{\"decision\":\"block\"}' && exit 2"
        }
      }
    ],
    "PostToolUse": [
      {
        "matcher": { "toolName": "Write", "pathPattern": "src/**/*.js" },
        "handler": {
          "type": "command",
          "command": "prettier --write ${CLAUDE_TOOL_INPUT_FILE_PATH} && echo '{\"decision\":\"allow\"}'"
        }
      }
    ]
  }
}

For the block, we simply exit 2. No need to parse the input; we never want write access. For the formatter, we let the tool run first, then invoke Prettier on the written file. Because this is a PostToolUse hook, the tool output is already on disk. The handler exits 0, and the decision “allow” means nothing else changes. If we wanted to feed back a diff, we could output a JSON decision with an augment field, but for formatting after the fact, this works cleanly.

These are command hooks, but you could swap in an HTTP callback that hits your team’s CI status API (application programming interface), or a prompt-LLM that checks whether the commit message follows conventional commits. The structure stays the same: match an event, define a handler, and decide.

Where do hooks live and how do they compose across projects?

Hooks are configured in JSON files. Claude Code looks for them in several places, with a precedence that lets you layer global enterprise policy on top of project defaults. The default locations include a ~/.claude/hooks.json for personal global hooks and a .claude/hooks.json at the project root for per-repo hooks. Administrators can also enforce a system-wide configuration that cannot be overridden.

Each event can have multiple matcher groups, and within a group, you can list an array of handlers that run in order. This gives you composability: the global policy might block access to .env across all repos, while the local project adds a formatter that runs afterward. The merge is additive, not destructive, unless the administrator explicitly prevents per-project overrides.

Codex shares this same layered approach. Its hooks configuration is TOML or JSON, managed in user and enterprise config files. Codex also adds a trust model. Non-managed hooks must be hashed and pinned, and the user must explicitly approve them before they run. This ensures that a cloned repository can’t silently install hook code that steals secrets.

Quick Reference

PropertyValue
Default config file.claude/hooks.json (project) or ~/.claude/hooks.json (user)
Block exit code2
Allow exit code0 (stdout JSON may alter input)
Handler typescommand, http, prompt, agent
Key lifecycle eventsPreToolUse, PostToolUse, UserPromptSubmit, Stop, PermissionRequest, SessionStart
Override field in responseaugment (partial merge) or override (full replacement)
Hook essentials
3
Lifecycle cadences
2
Exit code block
0
Exit code allow
60s
Timeout
Key numbers to remember for Claude Code hooks.

Test yourself

Your team wants to prevent Claude Code from running any shell command that contains rm -rf, but you still want it to be able to use rm for individual files. How would you implement this with a hook?

Answer: Define a PreToolUse hook with a matcher for the Bash tool. In the handler, read the tool_input.command field from the event payload provided on stdin. If the command string contains the substring rm -rf, write a JSON decision of {"decision":"block"} to stdout and exit with code 2. For any other command, exit 0 without output structured decisions. To avoid false positives from words in scripts, you can parse the command line argument vector rather than the raw string if the tool provides it, but a substring check on the single command string works for the common case. You can store this hook in a global config to enforce it across all projects.

Frequently Asked Questions

Q: Can a hook modify the model’s prompt before it sees it? Yes, a UserPromptSubmit hook receives the user’s raw text and can return an augment object that injects extra context, such as current git branch or test results. The model then works with the augmented prompt.

Q: What happens if a hook takes too long to run? By default, command hooks have a 60-second timeout. If a handler exceeds it, the hook is considered to have failed, and the action may be blocked depending on your configuration. You can adjust the timeout via the timeout field in the handler definition.

Q: Can I share hooks with my team without committing the hook code? Yes. You can use an HTTP handler pointing to a shared service, or you can package command hooks as scripts in a shared internal tool repository and commit only the hook configuration that points to those scripts via an absolute path or a managed service.

Q: Do hooks run for subagent tasks as well? They can. Subagents emit their own SubagentStart, PreToolUse, and SubagentStop events. If you want the same guardrails (like blocking .env writes) inside a subagent, you need to configure hooks for those events, not just the parent session.

Q: How do I debug a hook that isn’t firing? Check the hook’s matcher pattern carefully. A mismatch in the tool name or path glob is the most common cause. You can run Claude Code with --verbose to see hook evaluation logs, or you can temporarily add a simple echo of the payload to a file to verify the event fires.

If you want this kind of ground-truth breakdown every week, how real developer tools work under the hood instead of rehashed tutorials, subscribe at internalsdecoded.com. Next time, we wire Claude Code into your CI pipeline and show how hooks plus custom commands make it a first-class team member.

Sources

#claude-code-hooks#automation
More from the library
The Newsletter

Keep up with AI. One email a week.

One thoughtful email each week. Unsubscribe whenever you like.