# Claude Code Hooks Reference — Events, Decisions, Scopes, and What Survives Bypass Mode

Where Claude Code hooks live, every event they fire on, the PreToolUse input and output contract (exit codes, allow/deny/ask, updatedInput), matchers for built-in and MCP tools, timeouts, subagents, and the fact most people get wrong: hooks fire in every permission mode, --dangerously-skip-permissions included.

<div style="margin:8px 0 28px;padding:20px 22px;border:1px solid var(--line-2);border-radius:12px;background:var(--color-accent-light,#f0effe);">
  <p style="margin:0 0 12px;font-size:15px;line-height:1.6;color:var(--acp-text);"><strong>Just want the answer?</strong> A PreToolUse hook is a command Claude Code runs before every tool call. It reads JSON on stdin and blocks the call with exit code 2 or with a JSON <code>deny</code>. It fires in every permission mode. One entry in <code>~/.claude/settings.json</code>:</p>
  <pre data-track="CCHooks: Hero Config Copy" style="margin:0 0 12px;background:var(--color-surface,#faf9ff);border:1px solid var(--line-2);border-radius:8px;padding:12px 14px;overflow-x:auto;"><code>{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash|Edit|Write|mcp__.*",
        "hooks": [ { "type": "command", "command": "node ~/.acp/govern.mjs", "timeout": 10 } ] }
    ]
  }
}</code></pre>
  <p style="margin:0;font-size:13px;color:var(--acp-text-dim);"><a href="/docs/setup#i-use-claude-code" data-track="CCHooks: Setup Guide" style="font-weight:600;">Install the ACP hook in one command →</a> &nbsp;·&nbsp; <a href="/install-explained" data-track="CCHooks: Install Explained">what it writes →</a> &nbsp;·&nbsp; free up to 5 agents</p>
</div>

Claude Code's hook system is the widest control surface of any coding agent: every built-in tool and every MCP tool, allow, deny, ask, and input rewriting, in every permission mode. It's also the surface people describe wrong most often, usually in the same sentence: "the bypass flag turns hooks off." It doesn't. This is the reference for what hooks are, what they receive, what they can say back, and where they stop.

## Where hooks live

Hooks are declared in settings, and settings have scopes:

| Scope | File | Notes |
|---|---|---|
| User | `~/.claude/settings.json` | Applies to every project on the machine; where the ACP installer writes |
| Project | `.claude/settings.json` | Checked in, shared with the repo |
| Project (local) | `.claude/settings.local.json` | Per-machine overrides, not committed |
| Managed | Organization-deployed settings | Cannot be overridden by the scopes above |
| Plugin | `hooks/hooks.json` in the plugin | Ships with the plugin |
| Skill / subagent | Frontmatter | Scoped to that skill or subagent |

Two managed-settings switches matter for a fleet: `allowManagedHooksOnly` runs only the hooks in the managed scope and ignores the rest, and `disableAllHooks` turns every hook off along with the custom status line. Both are files on the endpoint. That is the honest limit of any client-side control: someone with write access to the config can remove it, which is why [a team rolls hooks out from managed settings](/blog/set-up-permissions-across-your-teams-coding-agents) and keeps the policy decision somewhere the endpoint can't edit.

## The events

Claude Code fires hooks on far more than tool calls. The current list: `SessionStart`, `Setup`, `UserPromptSubmit`, `UserPromptExpansion`, `PreToolUse`, `PermissionRequest`, `PermissionDenied`, `PostToolUse`, `PostToolUseFailure`, `PostToolBatch`, `Notification`, `MessageDisplay`, `SubagentStart`, `SubagentStop`, `TaskCreated`, `TaskCompleted`, `Stop`, `StopFailure`, `TeammateIdle`, `InstructionsLoaded`, `ConfigChange`, `CwdChanged`, `DirectoryAdded`, `FileChanged`, `WorktreeCreate`, `WorktreeRemove`, `PreCompact`, `PostCompact`, `PreModelSwitch`, `PostModelSwitch`, `Elicitation`, `ElicitationResult`, `SessionEnd`.

For control, two of them do the work. `PreToolUse` runs before a tool executes and can stop it. `PostToolUse` runs after, sees the result, and is where output scanning lives. The rest are observation and lifecycle. `PermissionRequest` is worth knowing: it fires when Claude Code is about to show a permission prompt, which is a different moment from "about to run a tool."

## The PreToolUse contract

**Input**, as JSON on stdin:

```json
{
  "hook_event_name": "PreToolUse",
  "session_id": "…",
  "tool_use_id": "…",
  "tool_name": "Bash",
  "tool_input": { "command": "git push --force origin main" },
  "permission_mode": "default",
  "cwd": "/Users/you/repo",
  "transcript_path": "/Users/you/.claude/projects/…/session.jsonl"
}
```

`tool_input` is the tool's arguments as the model produced them: a `command` for Bash, a `file_path` and content for Edit and Write, the tool's own schema for MCP tools. `permission_mode` tells the hook what mode the session is in, including `bypassPermissions`, which is how you know hooks run there.

**Output**, by exit code or JSON:

| You return | Effect |
|---|---|
| Exit 0, no stdout | No decision; the normal permission flow applies |
| Exit 2, reason on stderr | Blocked; the reason goes back to the model |
| Exit 0 + JSON `permissionDecision: "deny"` | Blocked, with `permissionDecisionReason` shown to the model |
| Exit 0 + JSON `permissionDecision: "allow"` | Runs without a prompt |
| Exit 0 + JSON `permissionDecision: "ask"` | Forces a prompt, even for an allowed tool |
| Exit 0 + JSON `updatedInput: {…}` | The call runs with the rewritten arguments |

The JSON shape is `{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "…"}}`. A `systemMessage` at the top level is shown to the user. A fourth value, `defer`, exists for print mode under the Agent SDK. The older top-level `decision: block` form is not what the current docs describe; use `hookSpecificOutput`.

**Matchers** narrow which tools a hook sees. Plain names (`Bash`), alternation (`Edit|Write`), regex (`mcp__.*`). MCP tools are named `mcp__<server>__<tool>`, so `mcp__github__.*` is one server and `mcp__.*` is every MCP tool the session has. No matcher means every call.

**Timeouts.** Command hooks default to ten minutes; set `timeout` per hook to bring that down. A hook that exceeds its timeout is treated as no decision, which for `PreToolUse` means the normal permission flow applies.

## A minimal working hook

Deny a force-push to `main`, allow everything else:

```js
#!/usr/bin/env node
let raw = ""; for await (const c of process.stdin) raw += c;
const ev = JSON.parse(raw);
const cmd = ev.tool_name === "Bash" ? String(ev.tool_input?.command ?? "") : "";
if (/git\s+push\b.*(--force|-f)\b.*\bmain\b/.test(cmd)) {
  process.stdout.write(JSON.stringify({ hookSpecificOutput: {
    hookEventName: "PreToolUse", permissionDecision: "deny",
    permissionDecisionReason: "force-push to main is blocked by policy" } }));
}
```

Register it under `PreToolUse` with a `Bash` matcher and it holds in every mode. It is also exactly as good as the regex, which is the limit of a hand-written hook: [command strings have many spellings](/blog/claude-code-deny-list-bypass), and the rule lives on one machine.

## What the ACP hook does with the same contract

The one-command installer registers `PreToolUse` and `PostToolUse` entries pointing at `~/.acp/govern.mjs`, with `ACP_CLIENT` set so the row records which harness made the call. Three things it does that the minimal hook can't:

- **Classifies instead of matching.** The command is classified (`Bash.git`, `Bash.rm`, `Bash.curl` to a host) before the rule is looked up, so the rule is about the action, not the spelling.
- **Reads the tier off the mode.** `permission_mode` maps to an agent tier: `auto` is treated as a subagent, `bypassPermissions` as a background agent, anything else as interactive. Rules are written per tier, so the policy for an unattended session is the one an unattended session gets.
- **Budgets itself.** Four seconds to a decision. Interactive sessions fail open with a loud `[ACP] UNGOVERNED` line so a slow gateway never stops you working; unattended tiers stay blocked until the gateway answers, because nobody is there to notice the gap.

In `--local` mode the same hook calls an on-device engine instead of the gateway: policy in `~/.acp/policy.json`, every decision in `~/.acp/audit.jsonl`, no account. [What the installer writes, file by file.](/install-explained)

## Bypass mode

`--dangerously-skip-permissions` puts the session in `bypassPermissions`. That auto-allows everything Claude Code's own permission system would have asked about. It does not disable hooks: `PreToolUse` fires before any permission-mode check, in every mode, and a hook `deny` blocks the call under the flag. Deny rules at every settings scope and the `rm -rf /` circuit breaker also still apply. What the flag removes is the human confirmation, which makes the deny rules and hooks you wrote beforehand the whole boundary. [What the flag actually turns off.](/blog/claude-code-dangerously-skip-permissions)

## Subagents

Tool calls inside a subagent pass through the same `PreToolUse` and `PostToolUse` hooks as the parent. `SubagentStart` and `SubagentStop` fire around the subagent itself, and a subagent definition can carry hooks of its own. For policy, the useful fact is that the ACP hook sees the same `permission_mode` on subagent calls, so a fan-out under a bypass-mode parent is governed at the background tier, not the interactive one.

## Known limitations

- **Hooks see tool calls, not tokens.** Cost lives on the model path; see the [cost tracking reference](/blog/claude-code-cost-tracking-reference).
- **Hooks are files on the endpoint.** Write access to settings is the ability to remove them. Managed settings and `allowManagedHooksOnly` narrow that; a decision that lives at a gateway the endpoint can't edit removes it.
- **A hook can only judge what it's shown.** A script the agent writes and then executes is one `Bash` call whose contents the hook has to classify; the [deny-list bypass catalog](/blog/claude-code-deny-list-bypass) lists the spellings.

## Comparing to Codex hooks

Codex CLI's hooks are the same shape and a narrower surface: shell first, with `apply_patch` and MCP on current builds, and `deny` as the operational decision. Both keep hooks running unattended. The [Codex CLI hooks reference](/blog/codex-cli-hooks-reference) has the detail, and [the side-by-side](/blog/claude-code-vs-codex-permission-models) has the table.

## Frequently asked questions



## Where to read more

- [Claude Code hooks guide](https://code.claude.com/docs/en/hooks-guide) &mdash; the upstream reference this page checks against
- [Claude Code cost tracking reference](/blog/claude-code-cost-tracking-reference) &mdash; the other plane
- [Claude Code headless and approvals](/blog/claude-code-headless-approvals) &mdash; what a hook `ask` means when nobody is on stdin
- [Which Claude Code tools to deny out of the box](/blog/which-claude-code-tools-to-deny-out-of-the-box) &mdash; a default posture argued from blast radius
- [Set up permissions across your team's coding agents](/blog/set-up-permissions-across-your-teams-coding-agents) &mdash; managed settings and one policy for the fleet
