require a bearer token on the MCP HTTP server (T-362)

D-71's threat model — another user on the same host must not drive my
IDE — was enforced with 0600 on the unix socket and then bypassed
wholesale by the unauthenticated localhost SSE port, which since D-86
serves every clide verb as a tool. The server now mints 32 bytes of
CSPRNG token per start, publishes it via the /ide discovery lock
file's authToken slot (the field Claude Code's client reads), chmods
the lock to 0600, and rejects any request that doesn't present the
token in x-claude-code-ide-authorization with 401.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:58:01 +02:00
co-authored by Claude Fable 5
parent a919d79ce1
commit 9889afdc35
5 changed files with 117 additions and 4 deletions
@@ -3992,3 +3992,5 @@ Cross-refs: T-274 (resumed-session status bar empty), T-167/T-185 (resume/fork s
UPDATE 2026-06-08: the active hang did NOT reproduce — clide is running fine inside the 31b214bd primary session (this very session resumes cleanly). So the original break was a one-off (likely the single corrupted transcript), not a live resume bug. This ticket stands as defensive hardening only: the two gaps (no init-event timeout/fallback; resume keyed off file-exists not content) are real but latent — they''d only bite again if a resume genuinely stalls or a metadata-only transcript appears. Lowering to low priority.
T-361 (done, 2026-06-12) added the session-level building blocks this ticket can reuse: StreamJsonSession now watches the process exit code (SessionEnd with stderr tail, replay-latest via session.end) and the pane surfaces ''claude exited (code N) — /clear to restart''. A resume that dies at spawn now surfaces instead of hanging silently; what remains here is the timeout/fallback for a resume that starts but never produces the init event, and resume-decided-by-content.', NULL, '2026-06-11 22:53:52', '2026-06-11 22:53:52', '2026-06-11 22:53:52', NULL, '5a99913696126412ccc7b18f75d3ec15', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBN5F0F8SDF15P21DNKT1W', 'status', 'backlog', 'in_progress', NULL, '2026-06-11 22:54:18', '2026-06-11 22:54:18', '2026-06-11 22:54:18', NULL, '2cadeabff0a0a89bbcc06db35dfa2f15', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBN5F0F8SDF15P21DNKT1W', 'status', 'in_progress', 'done', NULL, '2026-06-11 22:57:42', '2026-06-11 22:57:42', '2026-06-11 22:57:42', NULL, 'cb060646e7e967d607dc013a75ec0a28', 2) ON CONFLICT(hash) DO NOTHING;
+6
View File
@@ -4060,3 +4060,9 @@ Cross-refs: T-274 (resumed-session status bar empty), T-167/T-185 (resume/fork s
UPDATE 2026-06-08: the active hang did NOT reproduce — clide is running fine inside the 31b214bd primary session (this very session resumes cleanly). So the original break was a one-off (likely the single corrupted transcript), not a live resume bug. This ticket stands as defensive hardening only: the two gaps (no init-event timeout/fallback; resume keyed off file-exists not content) are real but latent — they''d only bite again if a resume genuinely stalls or a metadata-only transcript appears. Lowering to low priority.
T-361 (done, 2026-06-12) added the session-level building blocks this ticket can reuse: StreamJsonSession now watches the process exit code (SessionEnd with stderr tail, replay-latest via session.end) and the pane surfaces ''claude exited (code N) — /clear to restart''. A resume that dies at spawn now surfaces instead of hanging silently; what remains here is the timeout/fallback for a resume that starts but never produces the init event, and resume-decided-by-content.', 'backlog', 'medium', NULL, NULL, 'D-77', '2026-06-08 13:39:51', '2026-06-11 22:53:52', NULL, '06cc0a2e629ee7a192d24286f58af27f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBN5F0F8SDF15P21DNKT1W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'MCP HTTP server exposes the full dispatcher with zero auth', 'lib/src/ipc/mcp_server.dart:138-195, started unconditionally at boot (lib/main.dart:174-180). D-71 threat model (another user on the same host must not drive my IDE) is enforced with 0600 on the unix socket — then bypassed wholesale by an unauthenticated localhost HTTP port that, since D-86, serves every clide verb as a tool.
Fix: generate a token in the lock file (Claude Code /ide lock format has a slot for it) and require the auth header on every request. Tests: request without token is rejected; token round-trips via the lock file.', 'in_progress', 'critical', NULL, NULL, NULL, '2026-06-11 21:55:13', '2026-06-11 22:54:18', NULL, '320e3e3eeb46d8a70e5ecfd5c903563d', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBN5F0F8SDF15P21DNKT1W', 'bug', '06FBHBGHNEQTAEPGNJKN42C1E8', 'MCP HTTP server exposes the full dispatcher with zero auth', 'lib/src/ipc/mcp_server.dart:138-195, started unconditionally at boot (lib/main.dart:174-180). D-71 threat model (another user on the same host must not drive my IDE) is enforced with 0600 on the unix socket — then bypassed wholesale by an unauthenticated localhost HTTP port that, since D-86, serves every clide verb as a tool.
Fix: generate a token in the lock file (Claude Code /ide lock format has a slot for it) and require the auth header on every request. Tests: request without token is rejected; token round-trips via the lock file.', 'done', 'critical', NULL, NULL, NULL, '2026-06-11 21:55:13', '2026-06-11 22:57:42', NULL, '14de71ed19e921a370001097887a7e6f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);