Claude Code hooks
The PermissionRequest hook in Claude Code
PermissionRequest fires when a permission dialog would appear, and can answer it for you. Includes the decision object shape.
The difference from PreToolUse is scope. PreToolUse runs on every tool call regardless of whether a permission dialog would ever show up; this one is narrower by definition, since its whole premise is that a dialog is about to appear. A hook meant to catch every call, not just the ones headed for a prompt, belongs on PreToolUse instead.
- When it fires
- When a permission dialog is about to appear.
- Can it block?
- Yes. It can deny the permission outright.
- Matcher
- Yes, on
tool_name - Matcher examples
BashWritemcp__.*
What it receives
The event arrives as JSON on standard input. These are the fields worth reading.
| Field | Type | What it is |
|---|---|---|
tool_name | string | The tool asking for permission. |
tool_input | object | What it wants to run. |
permission_type | string | What kind of permission is being requested. |
permission_mode | string | The mode in force. |
session_id | string | Stable for the session. |
cwd | string | Working directory. |
Exit codes
The exit code is the decision. Anything the script writes to standard error on a blocking exit is what Claude gets told.
| Exit | What happens |
|---|---|
0 | Proceed, reading JSON on stdout for a decision. |
2 | Deny the permission. stderr is shown. |
other | Non-blocking error. |
What it can return
Note that the decision here is a nested object with behavior of allow or deny, which is a different shape from the flat permissionDecision that PreToolUse uses. permissionRule records which rule the decision corresponds to.
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow",
"updatedInput": { },
"permissionRule": "string"
}
}
}
Answering the prompts you would always answer the same way
If you find yourself approving the same read-only command forty times a day, the approval has stopped being a decision. Answering it here keeps the prompt for the calls that genuinely need a look.
In settings.json
{
"hooks": {
"PermissionRequest": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "node ~/.claude/hooks/auto-approve-reads.mjs" }
]
}
]
}
}
The script
const input = JSON.parse(require('fs').readFileSync(0, 'utf8')) || {};
const cmd = (input.tool_input || {}).command || '';
if (!/^(git status|git diff|git log)\b/.test(cmd)) process.exit(0);
console.log(JSON.stringify({
hookSpecificOutput: {
hookEventName: 'PermissionRequest',
decision: { behavior: 'allow' }
}
}));
process.exit(0);
For a fixed list of commands, a permission rule in settings does this without a script. Reach for the hook when the answer depends on something a rule cannot see.
The catch
Most of what people write this hook for is a permission rule in settings.json instead. Try the rule first; it is easier to read and it cannot crash.