claude-code - ✅(Solved) Fix PermissionRequest hooks silently deny in /remote-control when registered with no permissionDecision output [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
anthropics/claude-code#54582Fetched 2026-04-30 06:41:42
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Participants
Timeline (top)
labeled ×4

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.

Error Message

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.

Root Cause

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.

Fix Action

Fix / Workaround

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

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.

PR fix notes

PR #808: fix(hooks): emit allow decision for PermissionRequest in DSP-launched sessions

Description (problem / solution / changelog)

Summary

Closes a silent-deny gap that blocks agent-deck-managed Claude Code sessions from filesystem operations when invoked from `/remote-control` (and other headless / no-UI contexts).

Previously, agent-deck registered `PermissionRequest` as `Async: true`. The hook handler is a status tracker, not a permission decider; it writes `~/.agent-deck/hooks/{instance}.json` and exits 0 with no decision. In TUI sessions Claude Code's UI prompts the user. In `/remote-control` (no UI fallback), Claude Code treats the hook's silence as a non-decision and defaults to deny. Bash file system reads (`ls /mnt/c/...`) silently fail with no surfaced prompt.

This PR:

  1. Flips PermissionRequest to `Async: false` so Claude Code consults the hook's stdout for a decision.
  2. Adds an emission of `{"hookSpecificOutput":{"hookEventName":"PermissionRequest","permissionDecision":"allow"}}` when the parent process was launched with `--dangerously-skip-permissions`. DSP is the user-declared trust signal; auto-allow is consistent with that declaration regardless of UI presence.
  3. Detects DSP via either `AGENTDECK_DSP_MODE=1` env var (cross-platform) or `/proc/<ppid>/cmdline` (Linux/WSL).
  4. Status-tracking semantics are unchanged. Non-DSP and non-agent-deck-managed sessions are unaffected.

Test plan

  • `go test ./internal/session/` — passes (13.4s)
  • `go test ./cmd/agent-deck/` — passes (59.6s)
  • New test `TestPermissionRequestHookIsSynchronous` guards the Async: false registration so future regressions are caught
  • New test `TestParentIsDSP_EnvVarOverride` covers cross-platform env-var detection
  • New test `TestParentIsDSP_NoOverrideNoFlag` covers the negative case
  • End-to-end manual verification from a /remote-control session: `agent-deck status`, `agent-deck list`, `ls /mnt/c/...`, `agent-deck session output`, `agent-deck session send` all return cleanly. The previously-blocked FS read now returns directory entries.

Behavior matrix

ModeDSPHook outputCC reaction
TUI + agent-deckonallowallow (consistent with DSP server-side)
TUI + agent-deckoffsilentCC default (UI prompt)
/remote-control + agent-deckonallowallow (closes the silent-deny gap)
/remote-control + agent-deckoffsilentCC default (may still deny; uncommon path)
Non-agent-deck CC session (no `AGENTDECK_INSTANCE_ID`)n/ahook returns silentlyunchanged

Background

This was diagnosed and verified during 2026-04-28 work on /remote-control feature parity. The hook handler had been correctly suspected of "silently auto-denying file ops" in /remote-control; closer source review showed the handler is innocent (purely a status-tracker) and the silent-deny is Claude Code's default response to a synchronous-or-async `PermissionRequest` hook that exits 0 with no `permissionDecision` in a UI-less context. The fix changes both the registration (sync) and the handler (emit `allow` in DSP) to make the trust model honest across both interactive and non-interactive Claude UIs.

Considered but rejected: a /remote-control-aware bypass. No detection mechanism is currently exposed by Claude Code, and DSP is a more durable, user-controlled signal that also covers any future non-TUI invocation path (cron, CI, MCP-only).

Followup, separate PR

`hooksAlreadyInstalled` in `internal/session/claude_hooks.go` only checks presence of the hook command, not whether the `Async` flag matches the current `hookEventConfigs` table. That means upgrading the agent-deck binary in place does not update existing settings.json hook entries; `agent-deck hooks install` no-ops with "already installed." Worth a follow-up PR to extend the check to verify `Async` (and `Matcher` where applicable) match the current config.

🤖 Generated with Claude Code

Changed files

  • cmd/agent-deck/hook_handler.go (modified, +30/-0)
  • cmd/agent-deck/hook_handler_test.go (modified, +22/-0)
  • internal/session/claude_hooks.go (modified, +6/-1)
  • internal/session/claude_hooks_test.go (modified, +43/-0)
RAW_BUFFERClick to expand / collapse

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.

extent analysis

TL;DR

Exposing a signal to hook subprocesses, such as an environment variable, can help resolve the issue of silent denial in /remote-control mode.

Guidance

  • Consider exposing a signal to hook subprocesses, such as CLAUDE_REMOTE_CONTROL=1 or CLAUDE_NO_INTERACTIVE_UI=1, to allow handlers to branch and emit an explicit decision.
  • Review the current behavior of PermissionRequest hooks in headless mode and consider changing the default to fall through to the same behavior as if the hook were not registered.
  • Document the current behavior in the hooks reference to inform harness authors of the expected behavior.

Example

No code example is provided as the issue is more related to the design and behavior of the system rather than a specific code snippet.

Notes

The issue is not a security bug, but rather a usability regression in /remote-control mode. The proposed solutions aim to provide a more explicit and consistent behavior for harness wrappers.

Recommendation

Apply a workaround by exposing a signal to hook subprocesses, such as an environment variable, to allow handlers to emit an explicit decision. This approach provides a clear and unambiguous way for harness wrappers to handle the situation.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - ✅(Solved) Fix PermissionRequest hooks silently deny in /remote-control when registered with no permissionDecision output [1 pull requests, 1 participants]