Phantom session rows + unauthenticated localhost bridge accepts writes from any local process
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
!, labelStalled - 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_id ↔ cliSessionId 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
- Bridge should require a per-launch secret (env var / header / unix socket) and reject POSTs that don't present it.
- Bridge should verify the posted
agent_idmatches a knowncliSessionIdfor the current user session; reject otherwise. - Renderer should never stringify the IPC envelope into user-facing chrome. Fall back to
Unknown requestand put raw payload behind a "Show details" disclosure. - Session list should separate
Needs attentionfromWorkingrather than counting them under a singleN/M Approvalheader. - 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/
This issue has 9 comments on GitHub. Read the full discussion on GitHub ↗