anthropics / anthropics/claude-code
[BUG] PreToolUse hook `ask` resolves correctly in interactive mode but is silently denied in headless/stream-json mode, because the `PermissionRequest` hook never fires
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 147k
- Forks
- 24k
- PR merge metrics
- PR metrics pending
Description
Title
[BUG] PreToolUse hook ask resolves correctly in interactive mode but is silently denied in headless/stream-json mode, because the PermissionRequest hook never fires
What's Wrong?
With one fixed .claude/settings.json (a PreToolUse hook on Bash returning permissionDecision: "ask", plus a catch-all PermissionRequest hook on matcher: "" that logs the request and resolves it), the same ask is handled two different ways depending on how the session is driven:
- Interactive terminal session (
claudewith a TTY): works exactly as documented. ThePermissionRequesthook fires, gets logged, and resolves the request. Transcript shows⎿ Allowed by PermissionRequest hook. - Headless session (
claude -p ... --input-format stream-json --output-format stream-json): the tool call is denied immediately withdecision_reason_type: "hook". ThePermissionRequesthook never fires, and its log file stays empty. Reproduces identically inpermissionMode: "default"andpermissionMode: "bypassPermissions", so it isn't a bypass-mode carve-out; the ask never reaches any resolution path at all in this mode.
This defeats the documented purpose of PermissionRequest: "When a tool call needs a permission decision." A hook's ask is exactly that case, and headless/SDK callers with no canUseTool callback have no other way to resolve it. PermissionRequest hooks are the documented mechanism for headless permission handling in that situation, and they demonstrably work in interactive mode with the identical config. Headless mode just never invokes it.
What Should Happen?
Headless mode should invoke the PermissionRequest hook the same way interactive mode does. Per the hooks reference, "ask" "prompts the user to confirm," and the PermissionRequest event exists for exactly this: resolving a decision when one is needed. It should not be silently converted to a deny before that hook ever runs.
Steps to Reproduce
Same .claude/settings.json and prompt for both runs. Only the invocation changes.
.claude/settings.json:{ "permissions": { "defaultMode": "bypassPermissions" }, "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"ask\",\"permissionDecisionReason\":\"test ask from hook\"}}'" } ] } ], "PermissionRequest": [ { "matcher": "", "hooks": [ { "type": "command", "command": "cat >> /tmp/permreq.log; echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PermissionRequest\",\"decision\":{\"behavior\":\"allow\"}}}'" } ] } ] } }- Control (interactive): from that directory, run
claudenormally (TTY, no-p) and prompt "Use the Bash tool to run: echo hi." The transcript shows⎿ Allowed by PermissionRequest hook, and/tmp/permreq.loggets a line. - Headless: same directory, same prompt, run instead:
claude -p "Use the Bash tool to run: echo hi. Report exactly what the tool returned." \ --permission-mode bypassPermissions \ --output-format stream-json --input-format stream-json --verbose \ <<< '{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Use the Bash tool to run: echo hi. Report exactly what the tool returned."}]}}' - Check
/tmp/permreq.log. It stays empty this time. The stream shows:
with{"type":"system","subtype":"permission_denied","tool_name":"Bash","decision_reason_type":"hook","decision_reason":"test ask from hook", ...}tool_result_meta:non_execution_kind: "user-rejected". - Repeat step 3 with
--permission-mode default. Identical result:PermissionRequesthook still never fires.
Claude Code Version
2.1.266 (Claude Code)
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux (Debian GNU/Linux 12, bookworm)
Terminal/Shell
Non-interactive/CI environment
Is this a regression?
I don't know
Additional Information
Surfaced through a third-party wrapper that hosts Claude Code sessions via --input-format stream-json --output-format stream-json without a canUseTool callback, relying entirely on a registered PermissionRequest hook (matcher "") as the resolution path for hook-originated ask decisions. That is the pattern the docs describe as the headless alternative to canUseTool. That wrapper's own PermissionRequest hook never received the request either, confirming the gap is in Claude Code's dispatch, not the wrapper's hook.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the .claude/settings.json reproduction and run the interactive and headless stream-json commands to confirm the divergent behavior. Trace the headless permission path from the PreToolUse ask result to PermissionRequest dispatch, comparing it with the interactive path. Done means the PermissionRequest hook fires and can resolve the request in headless mode as it does interactively.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, developer-experience, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100