PermissionRequest hooks silently deny in /remote-control when registered with no permissionDecision output

Resolved 💬 1 comment Opened Apr 29, 2026 by vedantdshetty Closed May 31, 2026

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

  1. Register a PermissionRequest hook in ~/.claude/settings.json whose handler exits 0 and emits no JSON on stdout (e.g., a logger that just writes to a file). Async or sync, both reproduce.
  2. Start a Claude Code session, activate /remote-control.
  3. From the /remote-control session, 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).
  4. Observed: the call returns user rejected immediately. No UI prompt is shown anywhere. The hook handler ran (status file got written), but its silence is interpreted as deny.
  5. 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-control from inside the hook subprocess so it can emit an explicit allow.

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 PermissionRequest at 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-permissions already, 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗