PermissionRequest hooks never fire in --print mode on 2.1.237, while a PreToolUse control in the same settings file does

Status Open
Reported on v2.1.237
Maintainer reply None cached
Activity 0 comments · opened Aug 20, 2026

Summary

In --print mode, a PermissionRequest hook is never invoked, even when a tool call
plainly requires a permission decision and the model visibly stalls waiting for one.

A PreToolUse hook declared in the same settings file, matching the same tool, backed
by the same script, fires normally in the same run. So the settings file loads, the matcher
matches, the command quoting is valid, and the script is executable. The only variable that
differs between the two arms is the hook event.

This makes PermissionRequest unusable for its documented purpose, which is letting automation
answer a permission prompt in a non-interactive session. The consequence is worse than a hook
that errors: because the hook is never called, a -p run simply hangs on the prompt and then
gives up, and nothing anywhere reports that a configured hook was skipped.

Environment

  • Claude Code 2.1.237 (npm global install)
  • Windows 11 Pro, build 26200, AMD64
  • Node v22.22.0, npm 11.19.0
  • Reproduced with --model haiku; the model is not relevant to the failure

Reproduction

Three files in an empty directory. This is the exact repro that was run, with relative paths,
not a cleaned-up approximation.

hook.js

const fs = require('fs');
let raw = '';
process.stdin.on('data', c => raw += c);
process.stdin.on('end', () => {
  const label = process.argv[2];
  let p = {}; try { p = JSON.parse(raw); } catch {}
  fs.appendFileSync('marker.log', `${label} fired: event=${p.hook_event_name} tool=${p.tool_name}\n`);
  if (label === 'PERMISSIONREQUEST') {
    process.stdout.write(JSON.stringify({
      hookSpecificOutput: { hookEventName: 'PermissionRequest', decision: 'allow' }
    }));
  }
  process.exit(0);
});

settings.json

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Write", "hooks": [
        { "type": "command", "command": "node hook.js PRETOOLUSE", "timeout": 20 } ] }
    ],
    "PermissionRequest": [
      { "matcher": "Write", "hooks": [
        { "type": "command", "command": "node hook.js PERMISSIONREQUEST", "timeout": 20 } ] }
    ]
  }
}

Run:

claude -p "Use the Write tool to create hello.txt containing exactly: hi" \
  --permission-mode default --settings ./settings.json --model haiku

Expected

marker.log contains two lines. The PermissionRequest hook returns decision: "allow",
so hello.txt is written and the run completes.

Actual

marker.log contains exactly one line:

PRETOOLUSE fired: event=PreToolUse tool=Write

hello.txt is never created. The assistant's final output is a request for approval that
nobody can answer in -p mode, for example:

I'm ready to write hello.txt with the content "hi". Please approve the file write when prompted.

Why the control matters

The PreToolUse arm is not incidental. It is the same script file, invoked the same way,
differing only in an argv label, declared in the same settings.json. Its firing rules out
the ordinary explanations for a silent hook: settings file not loaded, matcher mismatch,
command-quoting problem, script not executable, working-directory problem, output-schema
problem. What remains is the event itself.

Second, independent data point

The same machine also has an unrelated PermissionRequest hook of type: "http" configured at
user scope, which logs on every approval it grants. Re-running the test against a path that
hook is specifically written to match produced no log entry from it either. So the failure
is not specific to hooks supplied via --settings, and not specific to type: "command".

Scope and limits, stated explicitly

  • Everything above is --print mode. An interactive TUI session was not tested, so a

print-mode-specific code path is not ruled out. If PermissionRequest does fire
interactively, then the correct reading of this report is narrower: the event is skipped on
the non-interactive path, which is precisely the path where a hook-supplied decision is the
only way a prompt can ever be answered.

  • The same machine has one historical log line showing this hook event firing successfully at

an earlier date, so the event is not permanently dead here.

  • The host settings directory was not empty during the test. A fully isolated config directory

was attempted and abandoned because it is unauthenticated. The control above is what stands
in for that isolation, and it is the reason the control exists.

Relationship to existing issues

  • #79177 covers PermissionRequest not firing for subagent prompts. It was closed as

completed, but its last comment is an automated "we weren't able to reproduce this" notice,
which is a failed reproduction rather than a fix. The behaviour above is broader than that
issue: the hook does not fire for a plain main-thread tool call either.

  • Related but distinct: #82418 (agent-teams teammates), #23983 (Agent Teams subagents),

#82150 (ordering for background subagents), #74256 (allow ignored for
ExitPlanMode), #85171 (Notification:permission_prompt not firing).

  • Adjacent in shape: #85479, where PreToolUse does not fire for MCP tool calls on Windows

while PostToolUse does.

Suggested fix direction

Whatever dispatches the permission decision in non-interactive mode should call the
PermissionRequest chain before falling back to "cannot prompt". Failing that, a session that
skips a configured hook event should say so on stderr, so the failure is visible rather than
presenting as a stalled prompt.

View original on GitHub ↗