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
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.
Summary
In
--printmode, aPermissionRequesthook is never invoked, even when a tool callplainly requires a permission decision and the model visibly stalls waiting for one.
A
PreToolUsehook declared in the same settings file, matching the same tool, backedby 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
PermissionRequestunusable for its documented purpose, which is letting automationanswer a permission prompt in a non-interactive session. The consequence is worse than a hook
that errors: because the hook is never called, a
-prun simply hangs on the prompt and thengives up, and nothing anywhere reports that a configured hook was skipped.
Environment
--model haiku; the model is not relevant to the failureReproduction
Three files in an empty directory. This is the exact repro that was run, with relative paths,
not a cleaned-up approximation.
hook.jssettings.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:
Expected
marker.logcontains two lines. ThePermissionRequesthook returnsdecision: "allow",so
hello.txtis written and the run completes.Actual
marker.logcontains exactly one line:hello.txtis never created. The assistant's final output is a request for approval thatnobody can answer in
-pmode, for example:Why the control matters
The
PreToolUsearm is not incidental. It is the same script file, invoked the same way,differing only in an
argvlabel, declared in the samesettings.json. Its firing rules outthe 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
PermissionRequesthook oftype: "http"configured atuser 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 totype: "command".Scope and limits, stated explicitly
--printmode. An interactive TUI session was not tested, so aprint-mode-specific code path is not ruled out. If
PermissionRequestdoes fireinteractively, 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.
an earlier date, so the event is not permanently dead here.
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
PermissionRequestnot firing for subagent prompts. It was closed ascompleted, 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.
PermissionRequest hook is awaited before the local dialog for background subagents; the main session runs them in parallel #82150 (ordering for background subagents), [BUG] PermissionRequest hook 'allow' for ExitPlanMode is ignored since v2.1.199 — plan-approval chooser still blocks #74256 (
allowignored forExitPlanMode), Notification:permission_prompt hook does not fire in v2.1.226 #85171 (Notification:permission_promptnot firing).PreToolUsedoes not fire for MCP tool calls on Windowswhile
PostToolUsedoes.Suggested fix direction
Whatever dispatches the permission decision in non-interactive mode should call the
PermissionRequestchain before falling back to "cannot prompt". Failing that, a session thatskips a configured hook event should say so on stderr, so the failure is visible rather than
presenting as a stalled prompt.