claude-code - 💡(How to fix) Fix [FEATURE] Permission relay: notify channel server when prompt is resolved locally (close server-side ghost state) [2 comments, 2 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#54597Fetched 2026-04-30 06:41:17
View on GitHub
Comments
2
Participants
2
Timeline
4
Reactions
0
Author
Timeline (top)
commented ×2labeled ×2

The claude/channel/permission relay (v2.1.81+) is asymmetric: the channel server is told when a permission prompt opens, but never told when it closes locally. As a result, every channel implementation that renders the prompt in its own UI is left with a stale "pending" artifact whenever the user answers in the local terminal dialog instead of through the channel.

The docs for the feature explicitly describe this as the intended behavior. From channels-reference#how-relay-works:

Both stay live: you can answer in the terminal or on your phone, and Claude Code applies whichever answer arrives first and closes the other.

If someone at the terminal answers before the remote verdict arrives, that answer is applied instead and the pending remote request is dropped.

"Dropped" is from Claude Code's perspective. The channel server never receives a signal that its outstanding relay is now moot, so its UI artifact (Telegram message, Discord prompt, web UI card) sits there forever.

Root Cause

I'd guess most of these silently accumulate ghost messages today; they're easy to miss because the "happy path" (channel-first resolution) works correctly.

Fix Action

Fix / Workaround

Builds against this protocol that surface the prompt as a stateful UI element (a card, a chat message awaiting reply, a status line) end up with permanent ghost artifacts whenever the user resolves locally — which is the common case during attended sessions. Workarounds known to the community:

Code Example

// Claude Code → channel server
notifications/claude/channel/permission_resolved
  params: {
    request_id: string,                       // matches the original permission_request
    behavior: "allow" | "deny" | "canceled",  // canceled = local timeout / claude moved on / session ended
    source: "local" | "channel"               // who resolved it
  }
RAW_BUFFERClick to expand / collapse

Summary

The claude/channel/permission relay (v2.1.81+) is asymmetric: the channel server is told when a permission prompt opens, but never told when it closes locally. As a result, every channel implementation that renders the prompt in its own UI is left with a stale "pending" artifact whenever the user answers in the local terminal dialog instead of through the channel.

The docs for the feature explicitly describe this as the intended behavior. From channels-reference#how-relay-works:

Both stay live: you can answer in the terminal or on your phone, and Claude Code applies whichever answer arrives first and closes the other.

If someone at the terminal answers before the remote verdict arrives, that answer is applied instead and the pending remote request is dropped.

"Dropped" is from Claude Code's perspective. The channel server never receives a signal that its outstanding relay is now moot, so its UI artifact (Telegram message, Discord prompt, web UI card) sits there forever.

Current protocol

DirectionMethodTrigger
Claude Code → channelnotifications/claude/channel/permission_requestLocal dialog opens
channel → Claude Codenotifications/claude/channel/permissionChannel forwards a verdict back

There is no notification fired when the local dialog wins, times out, or is otherwise abandoned.

Concrete symptom

Builds against this protocol that surface the prompt as a stateful UI element (a card, a chat message awaiting reply, a status line) end up with permanent ghost artifacts whenever the user resolves locally — which is the common case during attended sessions. Workarounds known to the community:

  • Manual dismiss button. Adds UI weight, splits the audit trail (locally-resolved cards never record an allow/deny verdict).
  • Screen-scrape the tmux pane to detect the dialog disappearing. Brittle: depends on dialog wording, terminal width, render mode; breaks on every Claude Code release that touches the prompt UI.
  • Assume timeout after N seconds. Loses correctness — auto-dismissing a dialog the user is still considering creates a worse failure mode.

None of these are clean. The clean signal lives in Claude Code itself.

Proposed fix

Add one symmetric outbound notification:

// Claude Code → channel server
notifications/claude/channel/permission_resolved
  params: {
    request_id: string,                       // matches the original permission_request
    behavior: "allow" | "deny" | "canceled",  // canceled = local timeout / claude moved on / session ended
    source: "local" | "channel"               // who resolved it
  }

Fire it whenever the request leaves the "open" state, regardless of cause:

Resolution pathbehaviorsource
User answers in local terminal dialog"allow" or "deny""local"
Channel sends notifications/claude/channel/permission and Claude Code applies it"allow" or "deny""channel"
Local dialog times out / claude abandons the request"canceled""local"

The source: "channel" case is technically redundant with the channel server's own state (it just sent the verdict), but emitting it uniformly lets server implementations have one observation point for "this request_id is now closed" instead of two code paths.

Why this shape

  • Mirrors the existing inbound schema. The verdict notification into Claude Code already uses { request_id, behavior }. Adding source and the canceled value is the minimum delta.
  • Stateless on the wire. Channel servers correlate on request_id; no new ID space, no new lifecycle to track.
  • Doesn't change the user-facing contract. The local dialog still wins on first-answer, channel still forwards, etc. This is observability, not policy.
  • Backwards compatible. Servers that don't care about resolution can ignore the notification. Servers that do care need only register a handler — same pattern as the existing permission_request handler.

Affected implementations

Every channel that renders the prompt as a stateful UI element:

  • Anthropic's own telegram, discord, imessage, fakechat plugins (source)
  • Any third-party channel using the v2.1.81 relay capability

I'd guess most of these silently accumulate ghost messages today; they're easy to miss because the "happy path" (channel-first resolution) works correctly.

Out of scope for this issue

  • Whether the local dialog should display the request_id (separate UX question, doesn't block this).
  • Whether to expose the relay as a tool-call rather than a notification (would be a larger redesign).

Related

  • Follow-up to #36854 (docs landed, feature gap remained).

extent analysis

TL;DR

The proposed fix involves adding a symmetric outbound notification notifications/claude/channel/permission_resolved to inform the channel server when a permission request is resolved, regardless of the cause.

Guidance

  • Implement the proposed notifications/claude/channel/permission_resolved notification with the specified parameters (request_id, behavior, and source) to provide a clean signal to the channel server when a permission request is resolved.
  • Update the channel server to handle this new notification and remove any stale UI artifacts when a permission request is resolved.
  • Ensure that the notification is fired whenever the request leaves the "open" state, including when the user answers in the local terminal dialog, the channel sends a verdict, or the local dialog times out.
  • Consider implementing a handler for the permission_resolved notification in existing channel implementations, such as the telegram, discord, imessage, and fakechat plugins.

Example

// Example of the proposed notification
interface PermissionResolvedNotification {
  request_id: string;
  behavior: "allow" | "deny" | "canceled";
  source: "local" | "channel";
}

// Example of how to fire the notification when the request is resolved
function resolvePermissionRequest(requestId: string, behavior: "allow" | "deny" | "canceled", source: "local" | "channel") {
  const notification: PermissionResolvedNotification = {
    request_id: requestId,
    behavior,
    source,
  };
  // Fire the notification to the channel server
  emitNotification("notifications/claude/channel/permission_resolved", notification);
}

Notes

  • The proposed fix is backwards compatible, and servers that don't care about resolution can ignore the notification.
  • The fix only addresses the observability of permission request resolution and does not change the user-facing contract.

Recommendation

Apply the proposed workaround by implementing the `notifications

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 - 💡(How to fix) Fix [FEATURE] Permission relay: notify channel server when prompt is resolved locally (close server-side ghost state) [2 comments, 2 participants]