PermissionRequest hooks silently deny in /remote-control when registered with no permissionDecision output
Summary
When a session is in /remote-control mode (or any other headless / no-UI context), a registered PermissionRequest hook that exits 0 with no permissionDecision on stdout is treated as a non-decision and Claude Code defaults to deny. The denial is silent: the user-facing error is "user rejected" but no prompt was ever surfaced to a user, because there is no UI to surface it through.
This makes any harness wrapper that registers a PermissionRequest hook for purposes other than permission decisions (status tracking, logging, observability) silently break filesystem-touching operations in /remote-control.
Reproduction
- Register a
PermissionRequesthook in~/.claude/settings.jsonwhose handler exits 0 and emits no JSON on stdout (e.g., a logger that just writes to a file). Async or sync, both reproduce. - Start a Claude Code session, activate
/remote-control. - From the
/remote-controlsession, request a Bash op that triggers a permission prompt (e.g.,ls /mnt/c/Users/<user>on WSL, or any path the harness flags as needing approval). - Observed: the call returns
user rejectedimmediately. No UI prompt is shown anywhere. The hook handler ran (status file got written), but its silence is interpreted as deny. - Expected: either the hook's silence falls through to a default-allow path consistent with whatever UI-less default makes sense for
/remote-control, OR the harness/wrapper has a way to detect/remote-controlfrom inside the hook subprocess so it can emit an explicitallow.
Concrete impact
agent-deck (https://github.com/asheshgoplani/agent-deck) registers PermissionRequest only as a status tracker. Every agent-deck-managed Claude Code session in /remote-control mode silently lost filesystem access until we shipped a client-side workaround at https://github.com/asheshgoplani/agent-deck/pull/808.
The workaround is to:
- Register the hook synchronous (
Async: false). - Emit
{"hookSpecificOutput":{"hookEventName":"PermissionRequest","permissionDecision":"allow"}}on stdout when the hook detects the parent process was launched with--dangerously-skip-permissions(the harness's user-declared trust signal).
This works but couples agent-deck to a Claude Code-internal default that is not documented and may change. Any other harness wrapper hitting the same surface today has to independently reverse-engineer this.
Suggested fixes (any one would help)
Option 1: expose a /remote-control or no-UI signal to hook subprocesses
An env var (e.g., CLAUDE_REMOTE_CONTROL=1 or CLAUDE_NO_INTERACTIVE_UI=1) set on hook subprocess invocations would let handlers branch on it and emit an explicit decision. Most surgical from the harness side.
Option 2: change the default for unsignaled hook output in headless mode
When no UI is available and a PermissionRequest hook returns no decision, fall through to the same default behavior the harness would have if the hook were not registered at all (which today appears to be allow, given that removing the hook from settings.json is the working workaround). The current "treat silence as deny" semantics is brittle for any non-decision-making hook registration.
Option 3: document the current behavior
If the silent-deny is by design, document it in the hooks reference so harness authors know to:
- Always emit an explicit decision for
PermissionRequest, even when the hook is purely observational. - Or refrain from registering
PermissionRequestat all if they are not going to make a decision.
The current behavior is in await codex resume-adjacent harness code paths that wouldn't necessarily expect a hook-handler-side decision contract, hence the hidden footgun.
Reproduction environment
- Claude Code: latest as of 2026-04-28
- WSL2 Ubuntu 22.04
- agent-deck: vedantdshetty/agent-deck PR #1 / asheshgoplani/agent-deck PR #808 documents the agent-deck side of the trace
Notes
- This is not a security bug. The harness in question (
agent-deck) runs sessions with--dangerously-skip-permissionsalready, so the silent deny is purely a usability regression in/remote-control. But the asymmetry is surprising and silent, and the reproduction is reliable enough that other harness wrappers will hit it too. - The workaround (PR #808) lands in agent-deck, not Claude Code. Claude Code-side, the most useful single change is option 1 (expose a signal). That gives every harness an unambiguous way to do the right thing.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗