[BUG] PermissionRequest hook decisions are discarded in `--bg` sessions: hook fires, returns allow/deny, session stays blocked on "permission prompt"
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
In a background session (claude --bg), a PermissionRequest hook fires correctly and then its decision is thrown away. The session stays parked on the permission prompt.
The hook receives the right payload — correct tool_name, correct tool_input — and returns the documented decision object. The session then reports:
{"name": "PERMREQ-ARM-C", "status": "waiting",
"waitingFor": "permission prompt", "state": "blocked"}
Both allow and deny are discarded, so this is not an allow-specific safety guard. It is also not specific to a PreToolUse-originated ask: an ask raised by a plain permissions.ask rule, with no PreToolUse hook configured at all, behaves the same way.
The docs describe this event as the mechanism for exactly this situation — a permission decision made by something other than a human at a TTY, including hooks that "block waiting for an external decision (e.g., from a webhook or approval system)". #38299 is the request that shipped it, closed completed on 2026-03-25.
The practical consequence is that there is currently no working way to answer a permission prompt in either non-interactive launch mode:
| Launch | Ask origin | PermissionRequest hook | Outcome |
|---|---|---|---|
| claude -p | PreToolUse ask | never fires | auto-denied |
| claude --bg | PreToolUse ask, hook returns permissionDecision: allow | fires | blocked |
| claude --bg | PreToolUse ask, hook returns decision: allow | fires | blocked |
| claude --bg | permissions.ask rule, hook returns decision: allow | fires | blocked |
| claude --bg | permissions.ask rule, hook returns decision: deny | fires | blocked |
| claude --bg | no ask source (control) | never fires | completed |
The -p row is #88328, reported on Windows against the built-in Write flow. The runs above reproduce it on macOS from a hook-originated ask as well. This report is about the --bg rows, which are a different failure: the hook does fire there, and is then ignored.
That last control row matters if you are reproducing this. A bare echo in a background session is auto-approved, so no permission request is raised and the hook correctly never runs. A naive repro reads as "it works". The steps below use a permissions.ask rule to force a genuine request.
Why this is worth fixing rather than working around: the whole point of a PreToolUse guard returning ask is to keep a human or an approval service in the loop. Bypass flags defeat the guard instead of answering it, so they are not a substitute. Orchestrating a batch of background workers is otherwise fully supported as of 2.1.238 — --bg --model picks a model per worker, -w gives each its own worktree, and cross-session messaging works in both directions. Answering a guard is the one remaining piece, and the state is already modelled: claude agents --json exposes waitingFor: "permission prompt", and the stream emits permission_denied. The only workaround left is scraping the TUI over a pty and writing 1 or 2, which reads option positions off a render rather than any contract.
What Should Happen?
A PermissionRequest hook returning decision: "allow" should resolve the pending permission request and let the tool call proceed. decision: "deny" should resolve it as a denial. Neither should leave the session blocked on a prompt nobody can reach.
If a background session is deliberately excluded from this event's decision path, that should be documented on the PermissionRequest page, and the hook should not be invoked at all — firing it and discarding its answer is the worst of both, because the hook's side effects run and nothing reports that its decision was dropped.
Error Messages/Logs
# --bg, static permissions.ask rule, PermissionRequest hook returns decision: allow.
# The hook fired (payload captured below) and the session is still blocked.
$ claude agents --all --json
{"name": "PERMREQ-ARM-C", "status": "waiting",
"waitingFor": "permission prompt", "state": "blocked"}
# Same settings, hook returns decision: deny. Also blocked.
{"name": "PERMREQ-ARM-D", "status": "waiting",
"waitingFor": "permission prompt", "state": "blocked"}
# Payload the hook received on stdin (fields of interest):
# hook_event_name = PermissionRequest
# tool_name = Bash
# tool_input = {"command": "echo probe-ran", "description": "Echo probe-ran"}
# permission_mode = auto
# tool_use_id = null
# permission_request = null
# For contrast, the -p arm (#88328): the PermissionRequest hook never runs.
{"type":"system","subtype":"hook_response","hook_name":"PreToolUse:Bash",
"output":"{\"hookSpecificOutput\":{\"permissionDecision\":\"ask\", ...}}"}
{"type":"system","subtype":"permission_denied","tool_name":"Bash",
"decision_reason_type":"hook","decision_reason":"probe: forcing an ask"}
Steps to Reproduce
Three files in an empty directory. --setting-sources '' keeps any installed user hooks out of the run.
- Create
permreq.shand make it executable (chmod +x permreq.sh):
``bash``
#!/usr/bin/env bash
in=$(cat)
printf '%s\n' "$in" >> "$(dirname "$0")/permreq-fired.log"
echo '{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":"allow","reason":"probe: approved by external hook"}}'
- Create
settings.json, with an absolute path to the script:
``json``
{
"permissions": { "ask": ["Bash(echo *)"] },
"hooks": {
"PermissionRequest": [
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "/abs/path/permreq.sh" }] }
]
}
}
- Start a background session:
``bash``
claude --bg --model sonnet -n PERMREQ-REPRO \
--settings ./settings.json --setting-sources '' \
'Run exactly this bash command and nothing else: echo probe-ran'
- Wait about 30 seconds, then check the session state:
``bash``
claude agents --all --json
Observed: "status": "waiting", "waitingFor": "permission prompt", "state": "blocked".
Expected: the session runs echo probe-ran and completes.
- Confirm the hook did run, and that its answer was simply ignored:
``bash``
cat permreq-fired.log
The log contains one PermissionRequest payload with "tool_name": "Bash" and the matching tool_input.
- Change
"decision"to"deny"in step 1 and repeat. The session blocks identically, so the decision is not being read in either direction.
To see the real-world shape of this, drop the permissions.ask rule and add a PreToolUse hook that returns {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"..."}}. Result is the same. That is the case that matters in practice, since third-party guard plugins are PreToolUse hooks returning ask.
Claude Model
Sonnet (default)
Is this a regression?
I don't know
Last Working Version
_N/A_
Claude Code Version
2.1.238 (Claude Code)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Non-interactive/CI environment
Additional Information
Host is macOS (darwin 25.5.0), shell zsh. The blocked sessions were launched from a shell; the failure is in the background session itself, which owns its own pty.
Tested both output key spellings against the --bg arms, in case the shape was the problem: permissionDecision and the documented decision. Both fire the hook, both are ignored. Also tested PermissionRequest with a "matcher": "Bash" and with no matcher, in the -p arm, to rule out a matcher mismatch as the reason it never fires there.
Two side observations from the same runs. Neither is the subject of this report and I am happy to split either out if useful:
--permission-mode manualdoes not appear to reach a background session. It was passed on several of the runs above, and the hook payload reportspermission_modeasauto; a bareechowas auto-approved despite the flag.tool_use_idisnullin thePermissionRequestpayload even though the docs list it, which is #83748.permission_requestis also null.
Related, all distinct from this:
- #38299 — the closed feature request that shipped
PermissionRequest. Its use case is remote/programmatic approval, which is what does not work here. - #88328 —
PermissionRequestnever fires in--printmode. Sibling failure in the other non-interactive mode. - #78487 — background agents block indefinitely on unanswered prompts. That one asks for a timeout or auto-deny backstop; this one is about the answer channel that already exists.
- #74256 —
PermissionRequestallowignored forExitPlanMode. Same shape as this, on a different gate. - #82150 —
PermissionRequestawaited before the local dialog for background subagents. - #82418 —
PermissionRequestnever dispatched for agent-teams teammates. Another context the event does not reach.
Unrelated but worth stating so it is not read into the above: background cross-session messaging was broken on 2.1.220 and works in 2.1.238. This report is about the remaining gap only.