The Claude Code PermissionRequest hook runs at the moment Claude Code is about to show you a permission prompt, and it can answer that prompt for you. To allow, the hook exits 0 and prints {"hookSpecificOutput": {"hookEventName": "PermissionRequest", "decision": {"behavior": "allow"}}}; to deny, it uses "behavior": "deny" with a message for Claude. It matches on tool name, it cannot override your deny rules, and exit code 2 does nothing on this event. If the hook prints no decision, the normal prompt appears.
01PermissionRequest compared with PreToolUse and Notification
Three hook events are involved with permissions, and they do different jobs:
PreToolUseBefore every tool call, whether it needs permission or notYes: allow, deny, ask or deferPermissionRequestOnly when Claude Code is about to ask youYes: allow or denyNotification (permission_prompt)After a prompt has waited about six seconds while you're awayNoUse PermissionRequest when you care about the prompts themselves: answering them, logging them, or sending them somewhere. It fires straight away, with no six-second delay. Use PreToolUse for rules that should apply to every call, such as "never run rm -rf", because it runs even when no prompt would appear. The Notification hook guide explains that event's timing.
02What the hook receives
Along with the common fields, a PermissionRequest hook gets the tool_name and tool_input of the call Claude wants to make, and an optional permission_suggestions array:
{
"session_id": "abc123",
"cwd": "/Users/...",
"permission_mode": "default",
"hook_event_name": "PermissionRequest",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf node_modules",
"description": "Remove node_modules directory"
},
"permission_suggestions": [
{
"type": "addRules",
"rules": [{ "toolName": "Bash", "ruleContent": "rm -rf node_modules" }],
"behavior": "allow",
"destination": "localSettings"
}
]
}
permission_suggestions holds the rule changes Claude Code would suggest for this request. It is not an exact copy of the options in the dialog, because each dialog builds its own options.
03The decision object
Return a decision inside hookSpecificOutput:
behavior"allow" or "deny"updatedInputAllow only. Replaces the tool's whole input, so include unchanged fields tooupdatedPermissionsAllow only. Permission changes to apply, such as adding a rule or changing modemessageDeny only. Tells Claude whyinterruptDeny only. true also stops ClaudeTwo rules to remember:
- Deny and ask rules still apply. A hook returning
"allow"doesn't override a matching deny rule in your settings, and modified input is checked against those rules again. - Exit code
2is ignored here. On most events exit2blocks, but aPermissionRequesthook that exits2without adecisionobject leaves the prompt as it was. To deny, return the decision.
04Example: auto-approve one prompt
Anthropic's own example approves ExitPlanMode, the prompt that appears when Claude has finished a plan, so you aren't asked every time:
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "echo '{\"hookSpecificOutput\": {\"hookEventName\": \"PermissionRequest\", \"decision\": {\"behavior\": \"allow\"}}}'"
}
]
}
]
}
}
Keep matchers narrow. An empty matcher or .* with a blanket allow would approve every prompt, file writes and shell commands included.
05Example: decide by looking at the command
This script (needs jq) denies anything that touches git push with a reason Claude can read, and leaves every other prompt to you by printing nothing:
#!/bin/bash
# ~/.claude/hooks/permission-check.sh
input=$(cat)
cmd=$(jq -r '.tool_input.command // empty' <<<"$input")
case "$cmd" in
*"git push"*)
jq -nc '{hookSpecificOutput: {hookEventName: "PermissionRequest",
decision: {behavior: "deny",
message: "Pushing is not allowed from this session. Commit and stop."}}}'
;;
esac
exit 0
{
"hooks": {
"PermissionRequest": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "~/.claude/hooks/permission-check.sh" }]
}
]
}
}
Build the JSON with jq rather than by joining strings, so quotes in the message can't break it. And make sure nothing else prints to stdout. If your shell profile echoes a line, Claude Code won't read the decision.
06Example: allow and remember for the session
To approve a request and stop Claude asking again until the session ends, add an updatedPermissions entry with destination: "session". A hook can echo one of the permission_suggestions it received, changing only the destination:
#!/bin/bash
input=$(cat)
jq -c '{hookSpecificOutput: {hookEventName: "PermissionRequest",
decision: {behavior: "allow",
updatedPermissions: [(.permission_suggestions // [])[]
| select(.type == "addRules")
| .destination = "session"]}}}' <<<"$input"
As written, this allows every request it sees, so pair it with a narrow matcher. The destination can also be localSettings, projectSettings or userSettings, which write the rule to a settings file instead of keeping it in memory.
07Often a permission rule is the better tool
If the answer never changes, you don't need a hook. Put the rule in settings.json and Claude Code applies it without running anything:
{
"permissions": {
"allow": ["Bash(npm run *)"],
"deny": ["Bash(git push *)"]
}
}
Rules are evaluated deny first, then ask, then allow. A hook earns its place when the decision depends on something a rule can't see, such as the time of day, the branch you're on, or a person answering from somewhere else. Permission modes explained covers the other built-in options.
08Things that catch people out
- Non-interactive runs. With
claude -p, there is no one to prompt.PermissionRequesthooks still run, and if none returns a decision the call is denied. - Timeouts. A command hook can run for up to 600 seconds by default, and Claude waits. Set
timeout(in seconds) if your hook waits on something slow, such as a person. - Some prompts don't trigger it. A sandboxed command's network request doesn't run
PermissionRequest. Use thepermission_promptnotification type for that. - Security. A hook that returns
"allow"is approving actions in your name with your user's permissions. Review it like any other code that can run commands.
09Where Claude Code Notifier fits
The most common reason to write a PermissionRequest hook is to answer prompts when you're not at the keyboard. A DIY version needs the hook to send the request somewhere, wait for an answer, and return it before the timeout. That is doable, and some people build it with a chat bot.
Claude Code Notifier does this for you. Its free command line tool, ccn, runs on Mac, Windows and Linux and installs its hooks into ~/.claude/settings.json. With the iPhone or Android app paired, a permission request arrives on the phone and you can allow once, allow for the session, or deny; ccn returns that decision from the PermissionRequest hook. If you typed in the last 30 seconds, nothing waits and the terminal asks as usual. If you are away, the terminal waits for the phone up to a ceiling you choose (hold_ceiling_seconds, default 600), and stops waiting the moment you touch the keyboard. It is independent and not made by Anthropic. See approving permissions from your phone for the workflow and is remote approval safe? for the security side.
10FAQ
How do I auto-approve permissions with a hook?
Add a PermissionRequest hook with a narrow matcher, and have it print a JSON decision with "behavior": "allow" inside hookSpecificOutput, then exit 0. If the same command should always be allowed, a permissions.allow rule in settings.json is simpler and doesn't need a hook.
Why doesn't exit code 2 deny the request?
PermissionRequest is one of the events where exit 2 isn't honoured. Claude Code ignores it and shows the normal prompt. Return "behavior": "deny" in the decision object instead.
Can a PermissionRequest hook override my deny rules?
No. Deny and ask rules are still evaluated, so an "allow" from a hook doesn't get past a matching deny rule. Any updatedInput you return is checked against those rules too.
What is the difference between PermissionRequest and PreToolUse?
PreToolUse runs before every tool call and can allow, deny, ask or defer. PermissionRequest runs only when Claude Code is about to show a prompt, and can allow or deny it. Use PreToolUse for policy, PermissionRequest for answering prompts.
What happens if my hook prints nothing?
Nothing changes. Claude Code shows the usual permission prompt in the terminal, as if the hook weren't there.