jared hebb ~ %

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
Bash Write mcp__.*

What it receives

The event arrives as JSON on standard input. These are the fields worth reading.

FieldTypeWhat it is
tool_namestringThe tool asking for permission.
tool_inputobjectWhat it wants to run.
permission_typestringWhat kind of permission is being requested.
permission_modestringThe mode in force.
session_idstringStable for the session.
cwdstringWorking 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.

ExitWhat happens
0Proceed, reading JSON on stdout for a decision.
2Deny the permission. stderr is shown.
otherNon-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.