[BUG] Remote Control: MCP permission prompts for non-read tools never surface in claude.ai/code web UI
Summary
When using --remote-control to drive Claude Code from the claude.ai/code web UI, permission approval prompts for some MCP tool calls never render in the web UI. They appear only in the local TUI (screen session on the host), where they block the session until manually answered via SSH.
This is broader than a single MCP server — observed against both a bundled claude.ai connector and a custom user-built MCP server.
Environment
- Claude Code version: 2.1.143
- Launch:
claude --remote-control <session-name>(interactive, wrapped inscreen -DmS claude-<name>under a systemd user unit) - Host: Linux x86_64, Ubuntu 24.04, kernel 6.17.0
- Remote UI: claude.ai/code/ (web)
- No
--dangerously-skip-permissions, nopermissionMode: bypassPermissions
Steps to Reproduce
- Configure two MCP servers — one bundled
claude.aiconnector (e.g. a file-server), one custom user MCP. Do NOT pre-allowlist their write/delete tools insettings.local.json. - Launch
claude --remote-control session-1on the host (inside ascreensession under a systemd user unit). - Connect to the same session from claude.ai/code's web UI.
- From the web UI, ask Claude to call either:
claude.aiMCPwrite_fileto an allowed path, or- Custom MCP
memory_write/memory_delete(or any write/destructive tool)
- Observe the web UI vs. what's actually on the host.
Expected
A permission approval prompt renders in the claude.ai/code web UI. User clicks Allow / Allow Once / Deny.
Actual
- The web UI shows the tool call as in flight, then reports
Tool result missing due to internal error; the conversation appears hung. No indication that approval is needed. - The actual prompt is sitting in the local TUI in the screen session:
```
Do you want to proceed?
- Yes
- Yes, don't ask again for X in this session
- No
```
- The session stays blocked until someone SSHes in,
screen -rs, and answers manually. Prompts have sat 50+ minutes this way.
Affected vs. Unaffected Tools
In the same session, with the same MCP servers, prompts for some tools surface fine while others do not:
| Tool | Web-UI prompt surfaces? |
| --- | --- |
| MCP run_command (read-only) | ✅ |
| MCP read_file | ✅ |
| claude.ai MCP write_file | ❌ |
| Custom MCP memory_write, memory_delete | ❌ |
Pattern looks category-based (write/destructive ops?), not a per-server bug. Not Agent Teams / --teammate-mode — this is a vanilla single-session --remote-control.
Possibly Related
- #48549 (closed) — same symptom in Agent Teams via Remote Control. The teammate-specific fix may not cover single-session
--remote-control. - #46392 (open) — tmux teammate mode, similar symptom but different code path.
Workaround
Pre-allowlist the specific MCP tool names in .claude/settings.local.json:
\\\json\
{
"permissions": {
"allow": [
"mcp__<server-id>__<tool-name>"
]
}
}
\\
This eliminates the prompt entirely, sidestepping the bug — but requires knowing each tool's MCP name in advance and defeats per-call control.
Notes
- I do not believe this is server-side. The MCP spec puts permission UI entirely on the client; server tools shouldn't need to do anything special.
- First noticed locally on 2026-05-14 with
write_file; reconfirmed 2026-05-19 with a custom memory-management MCP server. Bug is stable across multiple sessions and Claude Code versions in the 2.1.x line.
20 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
🔍 Root Cause Analysis & Proposed Fix
claude-code #60385 — Remote Control MCP Permission Prompts Not Surfacing in Web UI
Issue Summary
When using
--remote-controlwith claude.ai/code web UI, permission approval prompts for write/destructive MCP tools never render in the web UI. They only appear in the local TUI (screen session), blocking the session until someone SSHes in to answer.Pattern: Read tools (run_command, read_file) work fine. Write/destructive tools (write_file, memory_write, memory_delete) fail to surface prompts.
Root Cause Analysis
Permission Prompt Routing Architecture
Claude Code has two rendering paths for permission prompts:
claudeis running (screen session)The bug: write/destructive tool permission prompts are routed only to the local TUI path, bypassing the WebSocket relay that feeds the web UI.
Likely Root Cause
The permission prompt system has a tool-category-based routing decision:
This is likely a security-conscious design that was never updated for
--remote-controlmode. The assumption was: "write operations need local confirmation for safety." But with--remote-control, the user expects ALL interactions through the web UI.Evidence
--remote-controlProposed Fix
Fix: Route All Permission Prompts Through WebSocket Relay in Remote-Control Mode
Alternative: Add Remote-Control Override
Competitive Analysis
--remote-controlhas reprolabel + clear tool-by-tool comparison tableEstimated Value
--remote-controlusers with non-allowlisted write tools---
Solution developed via automated analysis. Zero competition. Related to #48549 (Agent Teams fix didn't cover single-session remote-control).
Additional data point: read-only tool also affected — pattern may be per-server, not per-tool-category
Hit this again 2026-05-19 with a different tool, and the new observation contradicts the original framing of "non-read / write-class" tools.
Hung tool:
mcp__claude_ai_MemoryManagement__memory_read_all— read-only, idempotent, no destructive effects.Verbatim prompt from local TUI (via
screen -X hardcopy):Sat for 58m 6s in the local TUI. Web UI showed no prompt — same Mode A signature as the original report.
Revised pattern
Affected-vs-unaffected, with the new data added, suggests per-MCP-server, not per-tool-category:
| MCP server | Tools confirmed surfacing in RC UI | Tools confirmed NOT surfacing in RC UI |
|---|---|---|
|
claude.ai Faure_ca_MCP(HTTP-proxied custom MCP) |run_command,read_file| none observed yet ||
claude.ai MemoryManagement(HTTP-proxied custom MCP) | none observed yet |memory_read_all,memory_write,memory_delete|Both servers are configured similarly (HTTP MCP behind Apache TLS reverse proxy on the same host), but only one consistently fails to bridge permission prompts. That's a sharper signal than "writes vs. reads."
If reproduction effort would help, I can test additional scenarios on my end (different transports, different tool annotations on the affected server, etc.).
Follow-up: answering the Mode A prompt does not always resolve the call
Continuing the
memory_read_alldata point from my prior comment. Same session, fuller picture of what happens AFTER the user accepts the prompt in the local TUI: there's a second hang on the dispatch side, and Esc alone doesn't recover from it.Full sequence (UTC, ha-mcp session, 2026-05-19)
| Time | Event | Notes |
|---|---|---|
| ~12:49 |
/clearto reset context | Standard session prep, no issue || 12:50:06 | User prompt: "Read phase-b-recovery-prompt-2026-05-19.md and follow it" | |
| 12:50:09–12:50:14 |
Read→ToolSearchformemory_read_allschema → success | All logged to JSONL normally || ~12:50:17 |
memory_read_alltool_usegenerated | NOT yet flushed to JSONL — held in memory || ~12:53 | Mode A prompt renders in local TUI; web UI shows indefinite spinner | Same as my earlier comment |
| 12:53 → 13:51 | Prompt sits in local TUI for ~58 min | Web UI gives no indication a prompt exists |
| ~13:51 | User accepts option 2 ("Yes, don't ask again") in local TUI via attaching to the screen | Prompt clears from TUI |
| 13:51 → 14:28 | Screen goes completely blank. JSONL still frozen at 12:50:14. Zero MCP traffic. ~37 min stuck post-answer. | This is the new finding |
| 14:28:16 | External
screen -X stuff $'\e'(Esc) sent | || 14:28:16 | JSONL gets exactly one new entry: the held
memory_read_alltool_usewith its original12:50:17timestamp (preserved on write). | Flush triggered; no dispatch || post-Esc | Still no
tool_result, no MCP traffic, screen still blank | Esc partially recovered (flushed held state) but didn't dispatch || later | User types a new message in the web UI ("Where are you at?") | Any new user message would likely do |
| immediately | Session resumes. Claude self-reports the prior
memory_read_all"errored," retries, succeeds (now allowlisted), continues normally. | Full recovery |What this adds to the bug picture
tool_usewas held in memory, not on disk. Its12:50:17timestamp was assigned when the call was generated; the JSONL line wasn't written until Esc fired at14:28:16— 1h38m later. The original timestamp survived the deferred write. This suggests JSONL flush is gated on something tied to the permission/dispatch path, and that gate can stay closed after a prompt is answered.tool_useto disk but didn't dispatch the call.mcp__claude_ai_MemoryManagement__memory_read_all) in.claude/settings.local.jsonbetween the hang and the retry was effective — the retry didn't re-trip the prompt and succeeded in <2s.Updated user-facing recovery sequence
If a Mode A prompt sits in local TUI and the user accepts it but the session doesn't visibly resume:
.claude/settings.local.json(closes the loop for next time).screen -S <name> -X stuff $'\e') — may flush held state.I can share the relevant JSONL window (with redactions) on request.
The follow-up data point makes this look broader than prompt rendering. I would split it into three state transitions: prompt created and bound to a concrete tool_use/session id, prompt surfaced to each client, and prompt resolution waking the dispatcher so the tool call actually emits and gets a result. The post-answer blank screen plus delayed JSONL flush suggests the remote-control path may keep a dispatch gate closed even after local approval is resolved. If you share a redacted JSONL slice, the most useful window is last normal event -> held tool_use timestamp -> prompt resolution -> first event after Esc/new user message.
I would treat this as a lost control-plane event, not only a UI rendering bug.
If remote control is the active surface, a permission prompt for an MCP tool call needs to be represented as a resumable pending decision in that surface. Otherwise the system has two inconsistent truths: the local TUI is waiting for authority, while the remote UI reports a missing tool result or internal error.
The event shape I would want the remote UI to receive:
Acceptance tests:
Tool result missing;That keeps remote control from becoming a split-brain permission model. The active control surface should see the same pending authority boundary the local TUI sees.
Redacted JSONL window for the
memory_read_all(read-only) hangFollowing up on my 2026-05-19 data point, and answering @Snailflyer's and @rpelevin's request for the transcript window. This confirms the framing has to move past "UI rendering of write-tool prompts" — it's a lost/abandoned in-flight tool call on the remote-control dispatch path, and it happens for a read-only MCP tool too.
Setup
Single vanilla
claude --remote-control <session>(no Agent Teams, no--dangerously-skip-permissions). MCP toolmcp__claude_ai_MemoryManagement__memory_read_all— read-only, idempotent — was not pre-allowlisted. Driven from claude.ai/code web UI; agent also visible in the local TUI insidescreen.What the transcript shows (envelope only — timestamps/ids/tool-names verbatim, payloads + paths redacted)
The decisive fact
Grepping the raw transcript for that tool_use id:
Read,ToolSearch) completed in 7–15 ms — the relay is healthy; only the permission-gated MCP call stalled.memory_read_allcall never received atool_result, even after manual recovery.Why this matters for the fix
This supports @rpelevin's "two inconsistent truths" model. Two distinct defects are visible:
tool_usewas never reconciled to atool_result; it was dropped rather than held as a resumable pending decision.A fix that only mirrors the prompt to the web UI would address (1) but likely leave (2): the held tool call still needs to be a durable, resumable pending decision keyed by
{remote session id, local session id, permission request id, tool_use id, MCP server, tool name}so that resolving it from either surface emits the awaitedtool_result.(Versions/env as in the original report: Claude Code 2.1.x line, Ubuntu 24.04,
--remote-controlunderscreen/systemd. Paths,service_id, and tool inputs redacted; happy to share more of the envelope on request.)Thanks for posting the transcript window. This is the missing distinction.
The important new fact is not only that the prompt failed to surface remotely. It is that
toolu_01Th...became a permission-gatedtool_usewith no matchingtool_result, and recovery created a new turn instead of resolving the held call.So I would tighten the fix boundary to three invariants:
tool_use: allowed result, denied result, expired/timeout result, or cancelled result.tool_useand require a fresh user message to restart work.The useful regression test is the read-only case here:
memory_read_all;tool_useid;tool_resultor terminal denied/expired result for that original id;That would keep the repair from stopping at prompt mirroring. The pending authority object needs to own dispatch wakeup and terminal reconciliation too.
Follow-up: a fully-logged silent hang on an allowlisted MCP tool — the allowlist workaround didn't prevent it
A second occurrence, captured end-to-end. The fact that matters: the hung tool was already in
permissions.allow, so no permission decision was ever pending — and it still hung silently for 2.5 hours. The standard allowlist workaround recommended on this issue (and on #60388) does not cover this path.Client side — what the session JSONL shows
mcp__claude_ai_Faure_ca_MCP__run_command— custom HTTP MCP, allowlisted in.claude/settings.local.json.tool_use).| UTC | Event (session transcript) |
|---|---|
| 20:06:14.082 → 20:06:16.486 | 4
run_commandtool_use blocks emitted (4 assistant msgs in 2.4s) || 20:06:17 → 22:38:08 | transcript silent — no
tool_result, no error, no permission event; JSONL has no events || 22:38:09.620 | user interrupt → all 4 cancelled simultaneously |
| 22:42:04 → 22:43:00 | 5 sequential single calls, each returned < 1 s |
Server side — what the Apache access log shows (the correction)
At the exact dispatch second the server received 8
POST /mcprequests and answered them:(16:06 EDT = 20:06 UTC;
Claude-User= the connector relay.)So the requests were dispatched, traversed the relay, hit the server, and got HTTP responses — including two
202 Accepted(empty body), i.e. "accepted; the result arrives over the SSE stream." Themcp-serverbackend was healthy and continuous through the entire 2.5 h (no restart / stop / error in its journal). Yet the tool results never surfaced in the session.Corrected diagnosis: this is not a failure to dispatch outbound. It's a breakdown on the result-return / correlation path — most likely the SSE response channel for the
202-accepted calls never delivered (or was never correlated back to the pendingtool_use). Still a client/relay-side problem, consistent with the "dispatch-gate stays closed" theory on this issue — but the gate is on the return leg, not the outbound one.Honest uncertainty:
mcp-serverdoesn't log request bodies, so I can't tell from these logs whether the server actually executed the four commands and the results were lost on return, or the202-accepted calls never produced an SSE result. What's certain: the requests reached the server and were answered at the HTTP layer.Repro attempts (same session, later — could NOT reproduce)
echo) → both OK, < 1 secho) → all OK, < 1 sError POSTing to endpoint,not connected), never the silent hang.So it's intermittent, and server-side faults are not the cause (they fail loud and fast). Concurrency is a suspected correlate (the only failed case was a 4-way parallel batch; every sequential call before and after worked), but a 4-way batch did not reproduce it on retry.
Why it's worth recording here
Environment as before: Claude Code 2.1.x line,
--remote-controlunderscreen/systemd, Ubuntu 24.04. Redacted detail available on request.This new allowlisted case usefully separates two gates that were blended together earlier.
The permission/authority gate was already open: the tool was in
permissions.allow, so no approval prompt was pending. But the dispatch lifecycle still failed: fourtool_useblocks were emitted, the backend never saw the requests, and the transcript did not get atool_result, dispatch error, or terminal cancellation until manual interrupt.So I would make the regression target broader than prompt surfacing:
tool_uselifecycle accounting.tool_useshould reach exactly one terminal state: success result, tool error result, dispatch failed, denied, cancelled, or expired.tool_use_idshould receive a terminal dispatch-failed result rather than leaving a silent gap.tool_use_idin the batch, not just wake the UI.The useful additional test is the allowlisted concurrent case: issue four parallel allowlisted MCP calls, verify either four backend receives or four terminal results, then interrupt mid-flight and verify every original
tool_use_idhas a recorded terminal outcome.That would cover both families now visible in the thread: permission-gated calls that need a resumable pending decision, and allowlisted calls that still need dispatch accounting.
Conclusive: the hang is client-side — I reproduced the server's exact byte sequence and the client handled it fine
Following the correction above (the requests do reach the server and are answered), I went after the mechanism by recreating the server-side conditions of the hang. Result: the server behaves identically whether the call hangs or not — so the failure is on the client/relay side, in result-to-
tool_usecorrelation across a session re-init.What the original hang's server sequence was
The original batch's first request got a session-not-found
404(the MCP session had gone stale after a long idle — no server restart occurred, confirmed in the journal). The client then re-initialized and the server answered everything:Every request was answered at the HTTP layer. The results simply never surfaced in the session.
Forcing the same conditions
I evicted the MCP session on purpose (restart the backend — same effect as an idle-expiry: the in-memory session is gone) and then immediately fired a burst of tool calls. The first hits the now-missing session and triggers a re-init, exactly like the original:
The re-init handshake is byte-for-byte identical to the original. The only difference: this time the client correlated every
200back to itstool_useand returned the results. The original dropped that correlation and orphaned the calls → indefinite hang.Conclusion
404→re-init→all-answered sequence in both the hung case and the healthy case.tool_usecorrelation when a session is re-initialized mid-flight. The most likely shape: tool calls dispatched against session A; a404forces adoption of session B; the answers that come back for the session-A calls are not matched to the still-pendingtool_uses → orphaned, no error, no result, forever (until the user interrupts).Likely real-world trigger
A tool-call burst that lands just as the client is re-initializing a session that went stale after idle. Remote-control sessions that sit idle (e.g. periodic/scheduled work) are most exposed: the session expires server-side, and the next burst of calls races the re-init.
What would pin it down on the client side
Instrumentation of how the client maps incoming results to pending
tool_uses across aninitialize/session-id change — particularly whether in-flight requests issued under the previous session id are abandoned rather than retried/rebound when the client adopts the new session id after a404.(Environment as before: Claude Code 2.1.x,
--remote-controlunderscreen/systemd, Ubuntu 24.04. Server-side captured from Apache access logs; client-side from the session JSONL.)That reproduction usefully narrows the boundary: if the server response can be replayed and handled, then the fragile point is the handoff from a completed backend call back into the transcript's tool_result lifecycle.
I would make the invariant per tool_use_id rather than per UI prompt:
The client-side regression I would add is:
That turns this from "did the tool run" into "did every emitted tool call become accountable in the transcript", which seems like the boundary this bug keeps exposing.
Live recurrence (two sessions at once) + a scope-broadening finding
Hit this again today on two Remote Control sessions simultaneously. Three technical observations that sharpen — and broaden — the original report.
1. The remote approval is a silent no-op that survives a refresh
The original framing here is "the prompt never renders in the remote UI." What actually happens (observed in the mobile app) is more specific:
So the remote approval isn't missing — it is accepted by the UI but never delivered to the pending decision. The held tool call stays pending, and a refresh just re-surfaces the still-unresolved prompt. (This lines up with the earlier finding in this thread that the underlying held
tool_usenever gets correlated back to a result.)2. It is NOT MCP-specific — a built-in
Readprompt deadlocks identicallyThe title scopes this to MCP tools, but the same deadlock occurs for a built-in tool. In a separate Remote Control session, a
Readof a file path outside the project root (which triggers a normal permission prompt) hung with:Approving on every available surface did nothing, and the remote UI offered no way to recover. So the real surface area is any permission-gated tool over Remote Control, not just MCP servers. The fix can't live solely in the MCP layer — suggest broadening the issue scope/title accordingly.
3. Answering the prompt clears it, then the call hangs indefinitely
For the MCP case, the prompt rendered locally and was answered; the prompt cleared — and the call then hung for 11+ minutes with the UI spinner running, the token counter frozen, and no
tool_resultever returned (until it was forcibly interrupted). So even when the prompt is answered, the dispatch can still die silently afterward — consistent with the result-correlation failure described earlier in this thread (the request reaches the server and is answered, but the result never gets bound back to the pending call).Net
Three distinct surfaces of the same underlying defect: (a) remote approvals are accepted by the UI but never delivered, (b) the deadlock is tool-agnostic (built-in tools included), and (c) answering doesn't guarantee the dispatch completes. All point at the Remote Control permission/dispatch path, not any individual tool or server.
(Environment unchanged: Claude Code 2.1.x,
--remote-controlunderscreen/systemd, Ubuntu 24.04.)That recurrence changes the bug boundary again: it is no longer just prompt visibility or MCP result reconciliation. If Approve dismisses in the remote UI, survives refresh, and the same deadlock happens for a built-in Read prompt, then the failing unit is the delivery of a decision to the pending call, independent of tool source.
I would separate three states:
The invariant should be: a UI approval cannot be considered successful until it transitions the pending call out of pending. Dismissing the prompt is only a local UI event unless the original call receives allow, deny, expired, cancelled, or delivery_failed.
The regression I would add is:
That gives the fix a cleaner boundary: prompt rendering, decision capture, and decision delivery are separate obligations, and all three have to be accountable against the original call id.
The day-to-day impact, and this isn't an isolated setup
Stepping out of the mechanism for a second (@rpelevin has that well covered) to flag severity and breadth.
As a user: on Remote Control, roughly 60–70% of permission prompts never resolve unless I catch them within the first couple of minutes on a live, foregrounded surface. If I browse away on my phone, close the laptop, or close the console session, the prompt either never surfaces, or it surfaces and does nothing when I tap Approve — the popup dismisses but the held call stays stuck. So I have to babysit every session, which defeats the point of remote control.
This is widely reported and recurring, across Android / iOS / Linux / WSL / macOS, since February — not one person's environment:
Tool result missing due to internal error."The common failure: an emitted
tool_usethat never reaches any terminal state — whether a decision was dropped (approve → never delivered) or a result was dropped (allowlisted call, server answered, result never bound back). Both leave the call orphaned with no success, error, deny, cancel, or timeout. It also reproduces with built-in tools (Bash, Read), so it isn't MCP-specific.Two asks:
Happy to share redacted JSONL/server-log windows if useful.
Thanks for spelling out the user impact this clearly. The 60-70% number is the part that makes this hard to treat as an edge case: once Remote Control needs babysitting, the core value of the mode is gone.
I think your broader title is right. The common invariant across these reports is not MCP, mobile, or a particular tool class; it is that an emitted tool_use can lose its terminal state.
For triage, I would frame the bug as one lifecycle obligation:
once a tool_use is emitted, it must close exactly once as success, tool error, denied, cancelled, or timeout, regardless of whether the approval came from local TUI, web, mobile, refresh/reconnect, or an allowlist path.
That would also make the sibling reports easier to consolidate: some lose the decision delivery, some lose the result binding, but they all leave the same orphaned pending call behind.
The fix target is probably less "show the prompt everywhere" and more "make the pending call durable until a terminal event is written."
Fully instrumented reproduction: remote approval is accepted by the UI but never reaches the local gate — and the call never reaches any terminal state
I sat down and reproduced this end-to-end with a syscall trace (
strace) on the local process and a live read of the on-disk session transcript, so every surface could be cross-checked at the same timestamps. Summary up front: with the remote surface kept live, approval works; the moment the surface is backgrounded/closed during the wait, the remote Approve is accepted by the app and even leaves the device — but it never reaches the local permission prompt, and the heldtool_useis left permanently orphaned.Environment: Claude Code v2.1.176, Opus,
claude --remote-controlon Linux, driven from the mobile app.Trigger (deliberately not MCP — a built-in tool)
A built-in
Readof/etc/hostname(a path outside the project root, so it's permission-gated). Harmless, deterministic, and confirms the bug is not MCP-specific.Run 1 — control (surface stays live)
Fire the prompt, leave the app open, approve. → Completes instantly, result displays. Healthy path. (Even after deliberately ignoring the prompt for ~8 minutes on a live surface, approving still worked — so a long wait alone is not the trigger.)
Run 2 — the repro (surface closed during the wait)
Readprompt.At the moment of approval, the five surfaces disagreed with each other — and that disagreement is the bug:
| Surface | What it showed |
|---|---|
| Mobile chat | Prompt cleared → phantom "Reading…" (looks like it's working) |
| Session list | Still showing "Approve once" for the same session — prompt still pending |
| Local console (the real
claudeprocess) | The permission prompt still open and unanswered (1. Yes / 2. … / 3. No) || strace | At the exact second of the tap, the client opened DNS + TLS connections to the relay — the Approve left the device and reached the network |
| On-disk transcript | The
Readtool_useorphaned — notool_result— transcript frozen since the call was issued |So the decision was captured by the app and transmitted, but never delivered to the local prompt, which is the authoritative gate. The local process stayed blocked there, so the
tool_usecould never resolve.Recovery hierarchy (what does and doesn't free it)
| Action | Outcome |
|---|---|
| Tap Approve (remote) | Transmitted to the relay, never reached the local gate |
| Send a new message (remote) | Reaches the host but queues behind the blocked gate — no recovery |
| Answer the local gate (press
2) | Cleared the gate, advanced to "Forming…", then re-hung mid-response (token counter frozen — a second, post-answer stall) ||
Escat the local console | The only thing that recovered the session |The core finding
**Even after
Escrecovered the session, the originalReadtool_usewas never written a terminal state to the transcript — no result, no error, no cancellation. It remains permanently orphaned in the record. It reached none** of {success, tool-error, denied, cancelled, expired, dispatch-failed}.Update — still no terminal state after an hour+. The call was issued at 22:10:37 UTC. After the gate was finally answered locally, the turn advanced to "Forming…" and then froze there: the local console reads
Forming… 53m16s · ↓179 tokens— the elapsed timer keeps climbing while the token count stays pinned at 179, i.e. a dead dispatch that is displayed as in-progress. As of ~23:27 UTC (~1 h 17 min after the call was issued) there is still no terminal state in the transcript and no automatic timeout has fired. It has simply stayed open, presenting a live spinner the whole time.Why this matters / what to assert
This lines up with the open-obligation framing earlier in the thread: every emitted
tool_use_idmust reach exactly one terminal state, and a UI approval must not be considered successful until it transitions the pending call out of pending. Here:prompt_visiblestayed true (session list still said "Approve once"),decision_accepted_by_surfacewas true (app cleared it; the tap hit the network),decision_delivered_to_callnever happened (local gate stayed open;tool_usestayed orphaned).Three surfaces, three different states, and the call itself never closed. And it's not MCP-specific — this was a built-in
Read.Repro is reliable when the surface is closed during the wait (it's intermittent on a live surface — consistent with the "majority of prompts" failure rate reported above). Happy to share the redacted
stracewindow (showing the Approve hitting the network with no corresponding resolution) and the transcript slice (the orphanedtool_usewith notool_result) if useful for a fix/regression test.Screenshots
Console and web view side-by-side, ~53 minutes into the post-answer freeze. The local console reads
Forming… 53m16s · ↓179 tokens— the elapsed timer climbs while the token count stays pinned at 179, a dead dispatch rendered as in-progress — while the web view shows the phantom "Reading hostname" and the queued "test" message that never runs:!Console and web side-by-side: console shows "Forming… 53m16s · 179 tokens" while the web view shows a phantom "Reading hostname"
The session list still shows "Approve once" for the session — even though it had already been approved from the chat:
!Session list still showing "Approve once" after approval
The chat itself: the prompt is gone and it shows a phantom "Reading…", with a later message left queued (it never gets processed, because the local gate is still blocked):
!Chat showing phantom "Reading…" and a queued message that never runs
This is the cleanest repro boundary so far.
The useful split is that the remote approval path has two separate success conditions, and only the first one is happening here:
In this repro, the first happens and the second does not. That makes Approve look successful to the reviewer while the authoritative gate still says pending.
For a regression test, I would make the backgrounded-surface case explicit:
Two negative assertions seem just as important:
That keeps the fix centered on the handoff this repro isolates: not just whether the prompt is visible, and not just whether the network saw a tap, but whether the decision actually reached the pending call that owns the tool_use.
After a day of waiting for the Chat to return, i gave up. Here is the final screenshot.
<img width="1112" height="295" alt="Image" src="https://github.com/user-attachments/assets/d2d4573f-4562-4be4-95cf-772fc3b29362" />
Resolved on Claude Code v2.1.207.
Filed originally on 2.1.143; the instrumented repro was on 2.1.176. Re-tested end-to-end today on 2.1.207 and the remote-control permission flow now recovers correctly:
tool_resultreturns.Esc.)Closing as resolved. Thanks to all who added repros.