Blog
Engineering

Claude Code hooks explained: PreToolUse, PostToolUse, and Stop

Learn PreToolUse, PostToolUse and Stop with command-hook JSON and a TypeScript Agent SDK example. See which events block execution and what exit code 2 means.

Aadil Ghani
Aadil Ghani
Founder, Pushary
Jun 4, 2026Updated Nov 6, 202613 min read
Share
Claude Code lifecycle: PreToolUse gates execution, PermissionRequest handles a needed prompt, PostToolUse follows success, and Stop runs at turn completion.

A simplified successful tool path: PreToolUse can gate the action, PermissionRequest runs only when a permission prompt is needed, and PostToolUse observes success. A turn may contain many tool calls; Stop runs when Claude finishes responding and can require it to continue.

View full-size image

Claude Code hooks run at named lifecycle events. PreToolUse runs before a tool and can block it; PostToolUse runs after a successful tool call; UserPromptSubmit runs before Claude processes your prompt; and Stop runs when Claude finishes a turn. For command hooks, a PreToolUse handler can block a call with exit code 2 or return a structured JSON decision with exit code 0.

Start with the event table, command-hook setup, or PreToolUse example. Building an application with the Claude Agent SDK? Jump to the TypeScript hook callback example. CLI command hooks read JSON on stdin; SDK hooks use callbacks. Claude Code calls the session events SessionStart and SessionEnd, not onStart and onFinish.

Updated October 19, 2026: added the terminal and phone approval behavior introduced in Pushary CLI 1.9.66. The hook-event list was checked on October 15. The original publication date remains June 4, 2026.

Key takeaways

  • Command hooks read a JSON event on stdin and return an exit code or JSON on stdout.
  • There are 33 events. The full table below lists every one, when it fires, whether it can block, and its exact hookSpecificOutput fields.
  • PreToolUse fires before tool execution and is the main place to stop one, by exiting 2 or by printing a permissionDecision of allow, deny, ask or defer. PermissionRequest can also deny, but only when a permission dialog would appear.
  • Exit 2 is not a universal block. It stops the tool on PreToolUse and the turn on Stop, but on SessionStart it blocks nothing, and on PermissionDenied the exit code is ignored entirely.
  • Stop fires when a turn ends and Notification fires when Claude needs input, so both are good places to send yourself an alert.
  • A Stop hook can prevent a turn from ending, but cannot undo a tool call. Pushary uses PreToolUse and PermissionRequest to ask before execution. In supported CLI sessions, you can answer in the terminal or on your phone.
  • If you are looking for onStart and onFinish, Claude Code does not use those names. The nearest equivalents are SessionStart and SessionEnd for a session, and Stop for the end of a single turn.

Every hook event

The list is far longer than three. Checked against the official hooks reference on October 15, 2026, there are 33 events. Most exist for logging or context injection; the ones that can actually stop something are marked below.

"Blocks" means the event can prevent the thing it fires on. The mechanism differs: some use hookSpecificOutput, some a top-level decision field, some just exit code 2.

EventFiresBlockshookSpecificOutput fields
SessionStartSession starts or resumesNoadditionalContext, initialUserMessage, watchPaths, sessionTitle, reloadSkills
Setup--init-only, or --init / --maintenance in -p modeNonone (Claude Code discards Setup JSON output)
UserPromptSubmitYou submit a prompt, before Claude reads itYes, top-level decisionadditionalContext, sessionTitle, suppressOriginalPrompt
UserPromptExpansionA typed command expands into a promptYes, top-level decisionadditionalContext
PreToolUseBefore a tool call executesYes, permissionDecisionpermissionDecision, permissionDecisionReason, updatedInput, additionalContext
PermissionRequestA permission dialog appearsYes, decision.behaviordecision (behavior, updatedInput, applyRules)
PermissionDeniedA call is denied by the auto mode classifierRetry onlyretry
PostToolUseAfter a tool call succeedsNo, the tool already ran. A top-level decision of block only gives Claude a reasonupdatedToolOutput, additionalContext
PostToolUseFailureAfter a tool call failsNo, the tool already failedadditionalContext
PostToolBatchAfter a batch of parallel calls resolvesYes, top-level decisionadditionalContext
NotificationClaude Code sends a notificationNonone
MessageDisplayWhile assistant text is displayedRewrites textdisplayContent
SubagentStartA subagent is spawnedNoadditionalContext
SubagentStopA subagent finishesYes, top-level decisionadditionalContext
TaskCreatedA task is created via TaskCreateYes, exit 2 or top-level decisionnone
TaskCompletedA task is marked completedYes, exit 2 or continue: falsenone
StopClaude finishes respondingYes, top-level decisionadditionalContext
StopFailureThe turn ends on an API errorNonone
TeammateIdleAn agent team teammate is about to idleYes, exit 2 or continue: falsenone
InstructionsLoadedA CLAUDE.md or .claude/rules/*.md loadsNonone
ConfigChangeA config file changes mid-sessionYes, top-level decisionnone
CwdChangedThe working directory changesNowatchPaths
DirectoryAddedA working directory is added mid-session with /add-dir or the SDKNonone
FileChangedA watched file changes on diskNowatchPaths
WorktreeCreateA worktree is createdYes, any non-zero exitworktreePath
WorktreeRemoveA worktree is removed at exit, when a subagent finishes, or when a background session is deletedYes, any non-zero exitnone
PreCompactBefore context compactionYes, top-level decisionnone
PostCompactAfter compaction completesNonone
PreModelSwitchBefore a model switch you or a client asked for takes effectYes, permissionDecisionpermissionDecision, permissionDecisionReason
PostModelSwitchAfter the session model changes, including changes Claude Code makes itselfNoadditionalContext
ElicitationAn MCP server requests user inputYes, actionaction, content
ElicitationResultAfter the user answers an elicitationYes, actionaction, content
SessionEndA session terminatesNonone

The events below are the ones that change agent behavior, and the ones Pushary attaches to.

How a hook is wired

You register hooks in settings under a hook event, with an optional matcher to filter when they fire, and a command to run. A matcher of Bash means the hook only fires for the Bash tool. The command receives the event as JSON on stdin and signals its decision two ways: exit codes, or structured JSON on stdout.

Exit 0 means success, and Claude Code parses stdout for decision fields. Other exits can interact with valid JSON and onFailure settings. Check the current output rules before relying on a failure to block.

Exit 2 is where people get caught out, because it is often described as "exit 2 blocks" as though that were universal. It is not. What exit 2 does depends on the event:

  • On PreToolUse it blocks the tool call and feeds stderr back to the model.
  • On UserPromptSubmit it blocks the prompt, on Stop it prevents the turn ending, on SubagentStop it prevents the subagent stopping, on PreCompact it blocks compaction.
  • On SessionStart and SubagentStart it blocks nothing at all. It shows stderr as a hook error notice and the session or subagent carries on regardless.
  • On Notification the exit code is ignored.
  • On PermissionDenied and StopFailure the exit code is ignored outright, because the outcome has already happened.

If you need a decision rather than a signal, prefer the JSON path. It says what you mean and it does not depend on remembering which event you are in.

PreToolUse

PreToolUse fires before a tool call executes. The event carries the tool name and its arguments, so your hook sees exactly what the agent is about to do:

{
  "session_id": "abc123",
  "transcript_path": "/path/to/transcript.jsonl",
  "cwd": "/current/dir",
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": { "command": "rm -rf /tmp/build" }
}

To gate the call, return hookSpecificOutput with a permissionDecision:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Destructive command blocked by hook"
  }
}

The permissionDecision values are allow (let it run), deny (block it), ask (escalate to the user), and defer (end the run so the tool call can be resumed later, honored only in non-interactive -p mode). Alongside it, updatedInput rewrites the tool arguments before the tool runs, and additionalContext injects text the model sees.

That deny path is real enforcement. The tool never runs. A hook that only logs can see the same command, but it cannot stop it.

PermissionRequest

PermissionRequest fires when a permission dialog would appear, which makes it the second place a decision can be settled without you touching the keyboard. It answers through hookSpecificOutput.decision rather than permissionDecision, and the shape is different enough to be worth writing out:

{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": { "behavior": "deny", "message": "Denied by policy for Bash(rm -rf:*)" }
  }
}

behavior is allow or deny, and the decision object also accepts updatedInput and applyRules.

The ordering catches people out when both hooks are registered. PermissionRequest fires after PreToolUse, and only on the ask path: if PreToolUse already returned allow or deny, the dialog never happens and this event never fires. A handler registered on both events must distinguish an answer already collected from a deliberate handoff. For example, PreToolUse can return ask without creating a phone question, leaving PermissionRequest to create it once.

How Pushary uses PreToolUse

Pushary checks the proposed tool call in PreToolUse. It resolves the tool and its arguments against your permission policy before the action runs.

Since Pushary CLI 1.9.66, supported approvals stay answerable in Claude Code's terminal and on your phone at the same time. PreToolUse returns ask; PermissionRequest sends the phone question while the terminal prompt is open. The first answer wins. Answer in the terminal and the phone question closes.

This applies to CLI-managed interactive sessions in default or acceptEdits mode when only built-in defaults ask about the call. It does not make already permitted actions ask again. Rules you wrote, chosen presets, temporary modes and Team approval routes keep their configured behavior. Bypass and headless sessions keep the existing wait because there is no terminal prompt to answer. Pushary for Mac is unchanged in this release.

After setup or upgrade refreshes the permission hook, its phone question can stay open for up to 10 minutes. An unanswered default prompt stays pending. See answer Claude Code from your terminal or phone for the update command and limits.

Two details matter here. First, the policy matches on the arguments inside tool_input, not just tool_name, so git status and git push --force get different treatment from the same Bash tool. Second, the policy can allow recognized read-only commands without a prompt. That is not permission for every command that claims to be a read. More on both in the permission policy writeup. The Claude Code wiring is in the setup guide.

Enforced gating needs a hook to attach to. CLI agents have one. The Claude Desktop connector can notify and ask through MCP, but Desktop exposes no PreToolUse hook, so it cannot block a tool call. That is a hard limit of the surface, not a setting.

If you ship an agent in your own product, put the approval gate in the backend that owns the action. Pushary Partner APIs connect that decision to your customer. decisions.ask() waits for an answer within its caller timeout, which defaults to 55 seconds. A pending return is not approval: save the decision ID and resume through decisions.get() or a verified callback. The decision has a separate expiry. Follow the retry and resume walkthrough before running the protected action.

PostToolUse

PostToolUse fires after a tool call succeeds. It cannot stop the call, the work already ran, but it sees the result and can feed feedback back to the agent or replace the tool output. Use it for logging, formatting after an edit, or flagging a result the agent should reconsider. It runs the cleanup half of the loop, after PreToolUse has already let the call through.

Stop and UserPromptSubmit

Stop fires when Claude finishes responding. Returning a decision of block with a reason forces the agent to keep going instead of ending the turn, which is how you make a turn continue until tests pass or a phone-queued instruction is picked up.

UserPromptSubmit fires when you submit a prompt, before Claude reads it. Plain stdout from this hook is added to the prompt as context, and it carries the message text, which is how a running session can capture a real task title for the fleet board.

Hooks in the Claude Agent SDK

The Claude Agent SDK accepts hook callbacks through the hooks option of query(). Check the SDK hooks reference and the types in your installed version for supported events; do not assume TypeScript and Python expose identical sets. Each event takes a list of matchers. A matcher has an optional matcher pattern, a list of callbacks and an optional timeout in seconds. A callback receives the same input a command hook reads on stdin, and it returns the same JSON a command hook prints:

import { query, type HookCallback } from "@anthropic-ai/claude-agent-sdk"

const blockEnvReads: HookCallback = async (input) => {
  if (input.hook_event_name !== "PreToolUse") return {}
  const toolInput: unknown = input.tool_input
  if (typeof toolInput !== "object" || toolInput === null) return {}
  if (!("file_path" in toolInput) || typeof toolInput.file_path !== "string") return {}
  const fileName = toolInput.file_path.split(/[\\/]/).at(-1)
  if (fileName !== ".env" && !fileName?.startsWith(".env.")) return {}
  return {
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "Reading .env files is blocked",
    },
  }
}

for await (const message of query({
  prompt: "Summarise the config in this repo",
  options: { hooks: { PreToolUse: [{ matcher: "Read", hooks: [blockEnvReads] }] } },
})) {
  if (message.type === "result") console.log(message.subtype)
}

This is a narrow example, not a secrets sandbox. A Bash command, MCP tool or symlink can access the same file through another route. Keep secrets outside the agent's accessible environment when access must be prohibited.

The permissionDecision values are the same four: allow, deny, ask and defer. In the SDK, defer ends the query so you can resume it later. updatedInput and additionalContext work the same way too.

A hook is the right place for a fixed rule. When a person should decide, the SDK has a separate callback for that, canUseTool, which runs when a tool call needs permission. @pushary/claude-agent-sdk implements it: the question goes to your user's phone, and Approve or Deny comes back as the permission result. That package is for agents you ship to your own users, on the Partner plan.

Putting it together

The hook contract is small: read JSON on stdin, decide with an exit code or a JSON object on stdout. Gate with PreToolUse. Watch with PostToolUse. Stop decides whether a turn actually ends. Pushary connects that permission flow to your phone, so supported approvals can be answered at your desk or away from it. Its audit trail records decision activity subject to your history settings. See the Claude Code notifications page, or wire up other agents over MCP.

If you would rather not wire it yourself

Hooks are a shell contract, so anything that reaches your phone from one is something you either build or buy. Claude Code notifications is the buy side written out in full: what arrives on the phone, what you can answer from the lock screen, and what it costs. The honest comparisons are worth reading before you pick either way: Pushary vs Happy and Pushary vs Omnara cover the tools that mirror a whole session to your phone, Pushary vs HumanLayer covers approval APIs you call from your own code, and Pushary vs ntfy covers the free route where you keep the hook and add a push channel. All comparisons has the rest.

Does your team run AI agents? With AI agent approvals for teams, each question goes to whoever’s agent asked, or to a lead you choose, and you see who said yes.

Common questions

What are Claude Code hooks?

Claude Code hooks run handlers at specific events. This guide focuses on command hooks; HTTP, MCP, prompt and agent handlers are also supported. A command hook reads a JSON event on stdin, does whatever you want, and can return an exit code or JSON on stdout to change what happens next. The events you reach for most are PreToolUse (before a tool runs), PostToolUse (after it succeeds), and Stop (when Claude finishes a turn).

What exit code must a PreToolUse hook return to block a tool call?

Exit code 2. A PreToolUse hook that exits 2 blocks the tool call and feeds its stderr back to the model as the reason. Exit 0 without a decision leaves the normal permission flow in place. Return explicit decision JSON when the hook must approve or deny. For structured control you can instead exit 0 and print hookSpecificOutput JSON on stdout with a permissionDecision of allow, deny, or ask.

Which hook event fires when Claude Code finishes a turn?

Stop. It fires when Claude finishes responding, which is the end of a single turn rather than the end of the session. SubagentStop is the subagent equivalent, and SessionEnd fires when the session itself ends. Stop is also one of the events that can block: returning a decision of block with a reason forces the agent to keep going instead of ending the turn.

Getting that event to reach you away from the machine is a separate problem from the hook contract, and the Claude Code notifications page covers it. If a Stop or Notification hook is wired and no alert shows up, or it fires on every reply, Claude Code notifications not working walks through the causes.

Which hook stops Claude from reading sensitive .env files, and what tool names does it match?

Use PreToolUse before the relevant tool executes. A Read-only matcher is not a complete secrets boundary: shell commands, search, MCP tools, alternate paths and symlinks need separate controls. The SDK example demonstrates direct Read requests for .env and .env.* files; it does not secure every route to a secret.

What are the permissionDecision values for a PreToolUse hook?

allow, deny, ask, and defer. allow runs the tool, deny blocks it, ask escalates to the user, and defer ends the run so the tool call can be resumed later. Claude Code honors defer only in non-interactive -p mode and ignores it in an interactive session. They go inside hookSpecificOutput alongside permissionDecisionReason, and optionally updatedInput to rewrite the tool arguments and additionalContext to inject text the model sees.

How many hook events does Claude Code have?

Thirty-three, checked on October 15, 2026. The list includes: PreModelSwitch, PostModelSwitch and DirectoryAdded. Most events are for logging or context injection. The ones that can block are PreToolUse, PermissionRequest, UserPromptSubmit, UserPromptExpansion, PostToolBatch, SubagentStop, Stop, PreCompact, PreModelSwitch, ConfigChange, TaskCreated, TaskCompleted, TeammateIdle, WorktreeCreate, WorktreeRemove, Elicitation and ElicitationResult. Other events can observe or transform context without blocking the underlying action.

How do you use PreToolUse hooks in the Claude Agent SDK?

Pass a function in the hooks option of query(), under PreToolUse, with an optional matcher such as Bash or Read. The function receives the same input a Claude Code command hook reads on stdin, including tool_name and tool_input, and returns the same hookSpecificOutput, so a permissionDecision of deny blocks the call. Check the event types in your installed SDK version. To ask a person rather than apply a fixed rule, use the SDK's canUseTool callback, which runs when a tool call needs permission. The example above shows the full shape.

What is hookSpecificOutput?

It is the object a hook prints on stdout to return a structured decision instead of relying on an exit code. For event-specific output, include hookEventName; the rest of the fields depend on the event: permissionDecision and permissionDecisionReason on PreToolUse, a decision object with behavior on PermissionRequest, updatedToolOutput on PostToolUse, additionalContext on most of the observing events. Use exit 0 when returning structured decisions.

Frequently asked questions

What are Claude Code hooks?

Claude Code hooks run handlers at specific events. This guide focuses on command hooks; HTTP, MCP, prompt and agent handlers are also supported. A command hook reads a JSON event on stdin, does whatever you want, and can return an exit code or JSON on stdout to change what happens next. The events you reach for most are PreToolUse (before a tool runs), PostToolUse (after it succeeds), and Stop (when Claude finishes a turn).

What exit code must a PreToolUse hook return to block a tool call?

Exit code 2. A PreToolUse hook that exits 2 blocks the tool call and feeds its stderr back to the model as the reason. Exit 0 without a decision leaves the normal permission flow in place. Return explicit decision JSON when the hook must approve or deny. For structured control you can instead exit 0 and print hookSpecificOutput JSON on stdout with a permissionDecision of allow, deny, or ask.

Which hook event fires when Claude Code finishes a turn?

Stop. It fires when Claude finishes responding, which is the end of a single turn rather than the end of the session. SubagentStop is the subagent equivalent, and SessionEnd fires when the session itself ends. Stop is also one of the events that can block: returning a decision of block with a reason forces the agent to keep going instead of ending the turn. Getting that event to reach you away from the machine is a separate problem from the hook contract itself.

Which hook stops Claude from reading sensitive .env files, and what tool names does it match?

Use PreToolUse before the relevant tool executes. A Read-only matcher is not a complete secrets boundary: shell commands, search, MCP tools, alternate paths and symlinks need separate controls. The SDK example demonstrates direct Read requests for .env and .env.* files; it does not secure every route to a secret.

What are the permissionDecision values for a PreToolUse hook?

allow, deny, ask, and defer. allow runs the tool, deny blocks it, ask escalates to the user, and defer ends the run so the tool call can be resumed later. Claude Code honors defer only in non-interactive -p mode and ignores it in an interactive session. They go inside hookSpecificOutput alongside permissionDecisionReason, and optionally updatedInput to rewrite the tool arguments and additionalContext to inject text the model sees.

How many hook events does Claude Code have?

Thirty-three, checked on October 15, 2026. The list includes: PreModelSwitch, PostModelSwitch and DirectoryAdded. Most events are for logging or context injection. The ones that can block are PreToolUse, PermissionRequest, UserPromptSubmit, UserPromptExpansion, PostToolBatch, SubagentStop, Stop, PreCompact, PreModelSwitch, ConfigChange, TaskCreated, TaskCompleted, TeammateIdle, WorktreeCreate, WorktreeRemove, Elicitation and ElicitationResult. Other events can observe or transform context without blocking the underlying action.

Does Claude Code have onStart and onFinish hooks?

No. Claude Code does not use onStart or onFinish as event names. A session starting is SessionStart and a session ending is SessionEnd; the end of a single turn is Stop, and SubagentStop for a subagent. If you are porting from an SDK that uses onStart and onFinish, map onStart to SessionStart and onFinish to Stop for per-turn work or SessionEnd for per-session work.

Can a hook pause Claude Code until a human answers?

Yes. PreToolUse runs before execution and can return ask to require a permission prompt. PermissionRequest can then collect a human answer before the tool runs. Pushary CLI 1.9.66 uses both events to keep supported interactive Claude Code approvals answerable in the terminal and on your phone. Rules you chose and headless sessions keep their configured wait. Stop cannot protect a tool call that already ran.

How do you use PreToolUse hooks in the Claude Agent SDK?

Pass a function in the hooks option of query(), under PreToolUse, with an optional matcher such as Bash or Read. The function receives the same input a Claude Code command hook reads on stdin, including tool_name and tool_input, and returns the same hookSpecificOutput, so a permissionDecision of deny blocks the call. Check the event types in your installed SDK version. To ask a person rather than apply a fixed rule, use the SDK's canUseTool callback, which runs when a tool call needs permission.

What is hookSpecificOutput in a Claude Code hook?

It is the object a hook prints on stdout to return a structured decision instead of relying on an exit code. For event-specific output, include hookEventName; the rest of the fields depend on the event: permissionDecision and permissionDecisionReason on PreToolUse, a decision object with behavior on PermissionRequest, updatedToolOutput on PostToolUse, additionalContext on most of the observing events. Use exit 0 when returning structured decisions.

Aadil Ghani
Aadil Ghani
Founder, Pushary

Building Pushary so an AI agent can reach you on your phone and wait for a yes before it does something you would not want.

Read next

Engineering

Approve refunds over $500: the four things that decide whether a threshold holds

Picking the number is the easy part. Rule ordering, a missing field, a numeric string, and a retry are the four places a threshold leaks.

Aug 23, 20265 min readAadil Ghani
Engineering

Build vs buy: the real cost of a human-in-the-loop approval loop

The prototype takes a weekend. The system that will not silently approve a refund takes a quarter. An honest itemization, including when building is right.

Jul 17, 20263 min readAadil Ghani
Engineering

Fail-closed by default: why agent approvals should block instead of fire and forget

The failure case for approvals is not exotic. It is a phone in a pocket. Fail-closed design decides in advance that the ordinary case means stop.

Jul 17, 20263 min readAadil Ghani

Let your agents work. Stay in control.

Get the questions that need you on your phone or Mac. Approve, reject, or answer so your agent knows what to do next.