Pushary
Blog
Engineering

Claude Code hooks explained: PreToolUse, PostToolUse, and Stop

A current reference to Claude Code hook events, with how a PreToolUse hook gates a tool call before it runs.

AG
Aadil Ghani
Founder, Pushary
Jun 4, 20263 min read
Share

Claude Code hooks are shell commands the agent runs at fixed points in its lifecycle. A hook reads a JSON event on stdin, does whatever you want, and can return JSON on stdout to change what happens next. The three you reach for most are PreToolUse (before a tool runs), PostToolUse (after it succeeds), and Stop (when Claude finishes a turn). PreToolUse is the one that can stop a tool call before it executes, and that is the mechanism Pushary runs on.

The full event list is longer than three. The current hooks reference lists many more, including SessionStart, UserPromptSubmit, PostToolUseFailure, SubagentStop, PreCompact, and SessionEnd. Most of them are for logging or context injection. The handful below are the ones that change agent behavior.

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. Exit 2 is a blocking error: the tool call (or the stop, depending on the event) is blocked and stderr is fed back to the agent. Any other non-zero exit is a non-blocking error that gets logged and ignored.

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 decision values are allow (let it run), deny (block it), and ask (escalate to the user). There is also updatedInput, which rewrites the tool arguments before the tool runs. That deny path is real enforcement. The tool never runs. A hook that only logs can see the same command, but it can't stop it.

How Pushary uses PreToolUse

Pushary connects to Claude Code as a PreToolUse hook. The agent is about to run a tool, the hook reads the event on stdin, and the handler resolves the tool and its arguments against your permission policy. If the policy says allow, the hook returns allow and the agent never stops. If it says ask, the hook sends a push notification with the command and waits for your answer, then returns allow or deny based on what you tapped.

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, a proven read-only set of commands is auto-approved by default, decided from 1,721 real production questions, so reads never interrupt you. 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.

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.

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 turns the PreToolUse decision into a push notification you answer from your phone, then logs every call to an audit trail. See the Claude Code notifications page, or wire up other agents over MCP.

AG
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

Press

Pushary, featured on AI Plaza

AI Plaza interviewed our founder about the gap Pushary closes, the moment an agent finishes and nobody is watching.

Jun 29, 20261 min readAadil Ghani
Guides

What an AI agent audit log should capture for teams and compliance

The fields a coding-agent audit record needs to be worth keeping, and the honest line on what GDPR-aligned and self-assessed actually means.

Jun 27, 20265 min readAadil Ghani
Changelog

What shipped: more agents, and agents that ask before risky steps

Native hooks for Gemini CLI and Codex, plus a setup change that makes both agents ask before risky steps instead of only being gated.

Jun 26, 20262 min readAadil Ghani

Get a push the moment your agent needs you

Approvals, done alerts, and a kill switch for Claude Code, Codex, Cursor, and the rest. It takes a couple of minutes to set up.