Phantom session rows + unauthenticated localhost bridge accepts writes from any local process

Resolved 💬 9 comments Opened May 22, 2026 by KevinSegel Closed Jun 23, 2026

Summary

When using a custom --permission-prompt-tool, Claude Desktop's session list and floating approval stack can be populated by any local process via the local HTTP bridge at 127.0.0.1:7437/agent-state — no authentication, no validation that the posted agent_id maps to a known cliSessionId. The resulting "phantom" rows persist after the originating process exits, are mis-grouped under an "Approval" page, and fall back to rendering the raw JSON envelope as the row subtitle.

Environment

  • Claude Desktop 1.7196.3 (Electron 41.5.0)
  • claude-code CLI 2.1.142
  • macOS 25.4.0 (arm64)
  • Permission prompt configured via --permission-prompt-tool stdio / external shim

Observed

A row in the session list:

  • Status: orange !, label Stalled
  • Title: tmp
  • Subtitle: literally {"session_id":"qa3","cwd":"/tmp"} (raw JSON)

…paginated under a header reading "4/5 Approval" alongside three Working sessions that have nothing to do with approvals. The same orphan also surfaces as a floating top-right approval card with the JSON as its ACTION REQUIRED body.

Defects (6 stacked)

| # | Defect | Severity | Category |
|---|---|---|---|
| B | 127.0.0.1:7437/agent-state accepts unauthenticated POSTs; any local process can inject session-list entries. No agent_idcliSessionId validation. | P1 / local-security | Trust boundary |
| A | Raw JSON envelope rendered as row subtitle when tool/title fields are empty/missing | P1 | Fallback rendering |
| C | Phantom rows counted in "N/M Approval" pagination | P2 | Classifier |
| D | Title falls back to cwd basename (tmp) when no session label exists | P2 | Classifier |
| E | Same orphan drives both the list row AND the floating card; no dedupe across surfaces | P2 | Layout / state |
| F | No GC for orphans whose origin PID is dead; rows persist indefinitely | P3 | Persistence |

Reproduction (defect B)

From any local process running as the user:

curl -X POST -H 'Content-Type: application/json' \
  -d '{"event":"approval_request","request_id":"probe-1","tool":"","prompt_b64":"","agent_id":"unknown","pid":1,"cwd":"/tmp","branch":"","framework":"claude-code"}' \
  http://127.0.0.1:7437/agent-state

Returns HTTP 204 No Content. A new row immediately appears in the Claude Desktop session list with title tmp and the raw payload as its subtitle. No CLI session was ever started; no cliSessionId matches agent_id="unknown"; the originating PID (1) is not a real claude-code process.

Expected

  1. Bridge should require a per-launch secret (env var / header / unix socket) and reject POSTs that don't present it.
  2. Bridge should verify the posted agent_id matches a known cliSessionId for the current user session; reject otherwise.
  3. Renderer should never stringify the IPC envelope into user-facing chrome. Fall back to Unknown request and put raw payload behind a "Show details" disclosure.
  4. Session list should separate Needs attention from Working rather than counting them under a single N/M Approval header.
  5. Orphan reaper: rows whose origin PID is dead and that have no decision after T should auto-quarantine or auto-deny.

Impact

Local-only (attacker needs code execution as the user), but allows arbitrary spoofing of Claude Code's approval UI. A malicious local process could surface plausible-looking approval prompts to trick the user into clicking through. Also a quality-of-life regression — legitimate session lists become cluttered with phantom rows that can't be cleared without a desktop restart (no admin API; LevelDB-backed in-memory state).

---

Filed by Kevin Segel — https://www.linkedin.com/in/kevinsegel/

View original on GitHub ↗

This issue has 9 comments on GitHub. Read the full discussion on GitHub ↗