[BUG] PermissionRequest hook decisions are discarded in `--bg` sessions: hook fires, returns allow/deny, session stays blocked on "permission prompt"

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

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.

  1. Create permreq.sh and 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"}}'
``

  1. 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" }] }
]
}
}
``

  1. 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'
``

  1. 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.

  1. 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.

  1. 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 manual does not appear to reach a background session. It was passed on several of the runs above, and the hook payload reports permission_mode as auto; a bare echo was auto-approved despite the flag.
  • tool_use_id is null in the PermissionRequest payload even though the docs list it, which is #83748. permission_request is 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.
  • #88328PermissionRequest never fires in --print mode. 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.
  • #74256PermissionRequest allow ignored for ExitPlanMode. Same shape as this, on a different gate.
  • #82150PermissionRequest awaited before the local dialog for background subagents.
  • #82418PermissionRequest never 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.

View original on GitHub ↗