What the hook receives and returns
In Claude Code, a PreToolUse hook gets a JSON object on its input with the event name, the tool’s name, the tool’s input (for the Bash tool, the command) and the working directory. It answers with hookSpecificOutput.permissionDecision: allow, deny, ask or defer, plus a permissionDecisionReason. For ask, the reason is shown to you in the permission prompt; for deny, it is shown to Claude.1
Two details matter for safety. Your own deny and ask rules in the settings still apply whatever the hook returns. And when several hooks answer, deny wins over defer, defer over ask, and ask over allow. A hook that exits with code 2 blocks the call; any other failing exit is an error that doesn’t block, unless the hook sets onFailure to block.1
Ask or deny
ask puts a person in front of the command: you see the reason and decide. deny refuses it and tells the agent why, so it can try another way. Ask suits commands that are sometimes right, such as a migration reset on a local database. Deny suits commands that are never right for an agent, such as dropping a production database.
Each client handles it differently
The same idea exists in other coding agents, but the answers they accept differ:
- Codex CLI reads
askbut doesn’t support it yet: it marks the hook failed and runs the command.3 - Gemini CLI accepts allow and deny only; there is no ask.4
- GitHub Copilot’s cloud agent, and Copilot CLI in autopilot, treat ask as deny.5
So a hook that answers ask in Claude Code should answer deny where ask would let the command run. On one compound command from a public Claude Code issue, the Deedglass guard answers ask in Claude Code, Copilot and Cursor, and deny in Codex CLI and Gemini CLI.2
Where a hook stops
A hook reads the command text before it runs. It can’t see what a script does once it starts, what a database trigger changes, or a tool call that doesn’t go through the client. On Windows, Claude Code runs shell-form hooks with Git Bash or PowerShell, and without Git Bash it doesn’t register the Bash tool at all, so a hook that matches only Bash never fires.1 Treat a hook as one layer: pair it with a database role that can’t do the worst thing, and with a check of what really changed.