PermissionRequest hooks: subagent prompts don't invoke the hook; when it did fire, a returned allow was ignored

Status Fixed / completed
Reported on v2.1.191
Maintainer reply ✓ Yes — bcherny
Activity 4 comments · opened Jul 19, 2026 · closed Aug 17, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Environment

  • claude-code 2.1.191 (Linux x86_64, Arch package; DISABLE_AUTOUPDATER=1)
  • Hook registered via a plugin's hooks.json under PermissionRequest (matcher covers Read); the same plugin registers a PreToolUse hook, which is how we can prove plumbing liveness below.
  • Settings context: no allow/deny/ask permission rules for Read configured, so the documented tighten-only caveats have nothing to bite on.

Documented behavior (docs.claude.com/claude-code hooks reference, fetched 2026-07-17)

  1. PermissionRequest: "Returning allow skips the interactive prompt but doesn't override permission rules."
  2. "Permission requests generated by subagent tool calls fire PermissionRequest hooks…" (payload documented to carry agent_id/agent_type).

We observed two distinct failure modes, both contradicting the above.

Mode A — hook fires, returned allow is ignored (observed 2026-07-17 ~05:24Z)

A subagent's Read of a world-readable file outside the workspace triggered the built-in out-of-directory permission flow. Our PermissionRequest hook was invoked (payload carried tool_name: Read, the file path, and the subagent identity) and answered within ~1 ms:

{"decision": {"behavior": "allow", "updatedInput": {"file_path": "…"}}}

The interactive prompt was shown to the human anyway. We initially suspected our output shape (top-level decision vs the documented hookSpecificOutput envelope) and fixed it to emit the documented envelope — which brings us to mode B, because we could never verify the envelope fix:

Mode B — the hook is not invoked at all for a subagent's prompt (observed 2026-07-17 ~13:42Z)

A subagent Read of a never-before-approved world-readable path (/etc/os-release) produced the interactive prompt, and our PermissionRequest hook was never invoked — while our PreToolUse hook for the same tool call fired 5 ms earlier (both hooks in the same plugin, same daemon socket — plumbing demonstrably alive). Our hook-side event log records the PreToolUse dispatch and zero PermissionRequest dispatches for that session.

This directly contradicts documented subagent coverage (quote 2). Whether A-vs-B selection is version drift or depends on subagent type / permission mode (we saw A with a ticket-worker-type agent under acceptEdits, B with an Explore-type under default) we cannot determine from outside.

Workaround that works (for triagers' reference)

A PreToolUse hook returning hookSpecificOutput.permissionDecision: "allow" suppresses the out-of-directory prompt for both main-agent and subagent Reads on this build (live-verified at the keyboard, zero prompts). We have moved our read policy to that seat.

Questions

  1. Is PermissionRequest expected to fire for subagent tool-call prompts on current builds?
  2. When it does fire and the hook returns a documented allow (enveloped), is that expected to suppress the built-in out-of-directory ask when no permission rules are configured?

Happy to provide the full timestamped hook-event log for both modes.

View original on GitHub ↗

3 Comments

y49 · 2 months ago

Data points on your two questions, from 2.1.220 (Linux x86_64, interactive TTY), in case they help triage. My hook is a PermissionRequest command hook registered from a plugin's hooks.json with matcher: "*", and I have daemon-side logs that timestamp every dispatch.

Q1 — does PermissionRequest fire for subagent tool-call prompts?

Yes on this build, for backgrounded subagents (the default for the Agent tool since 2.1.198). The payload carries agent_id and tool_name; I have ~10 such dispatches logged today, e.g. agent_id: a3a3a4f8fd49bb54a, tool_name: Read.

Q2 — is a returned allow honored?

Yes, when the decision uses the documented hookSpecificOutput envelope. Measured on one subagent Read:

T+0.000s   PermissionRequest hook invoked   (agent_id present, tool_name Read)
T+26.000s  hook returns
           {"hookSpecificOutput":{"hookEventName":"PermissionRequest",
                                  "decision":{"behavior":"allow"}}}
T+26.080s  PostToolUse for the same agent_id + tool_name → the tool executed
           zero permission_prompt notifications at any point → no prompt was shown

So your Mode A may well have been the top-level-decision shape you already suspected — I could not reproduce it with the enveloped form. If you can re-test the envelope fix, that would settle it.

One diagnostic note that cost me some time and may be relevant to how Mode B was observed: when a hook holds a subagent's request, the hold is invisible from inside the session. No dialog appears and the tool simply takes longer and then succeeds, which reads exactly like "the call was never gated". I misread it that way myself before checking the hook-side log. So "no prompt appeared" is not by itself evidence that the hook did not fire.

Separately, I filed #82150 for an adjacent asymmetry I did reproduce: while the hook is holding, no local dialog is constructed for a backgrounded subagent (measured: zero permission_prompt for a 120s hold, then the dialog appearing 6.079s after the hook returned {}), whereas the main session constructs it in parallel and lets the first answer win.

bcherny collaborator · 1 month ago

Thanks for the detailed report. I tried to reproduce this on 2.1.233 on Linux, and also on your exact version 2.1.191, using your suggested repro: a PermissionRequest hook (matcher Read) in settings.json that logs stdin and prints {"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow"}}}, plus the same hooks registered via a plugin's hooks.json, reading /etc/hostname in a default-mode session in tmux.

In all four combinations (both versions × settings.json/plugin), both modes behaved as documented: the hook fired and the prompt was skipped ("Allowed by PermissionRequest hook"), and a subagent's Read also fired the hook — with agent_id/agent_type in the payload — and the allow was honored. So I can't reproduce either mode as described.

Two things that can look exactly like what you saw:

  1. The permission dialog is drawn while the hook command runs and auto-dismisses when it returns allow. If the hook script is slow or doesn't exit (e.g. a background child keeps stdout open), the prompt stays on screen even though the hook "fired".
  2. If the hook's stdout doesn't match the documented schema exactly (see https://code.claude.com/docs/en/hooks), the decision is silently ignored and the real prompt shows. Also, the hook only runs when a permission request is actually generated — a subagent call that's already allowed by rules or a prior approval won't fire it.

Could you share your exact hook script, its raw stdout, and a claude --debug-file /tmp/cc-debug.txt log from a failing run (grep for PermissionRequest)? Happy to dig back in with that.

🤖 Generated with Claude Code

github-actions[bot] · 1 month ago

We weren't able to reproduce this. Could you provide steps to trigger the issue — what you ran, what happened, and what you expected? This issue will be closed automatically if there's no activity within 7 days.

Showing cached comments. Read the full discussion on GitHub ↗