[BUG] Code tab writes UI render metadata (start_timestamp/stop_timestamp/flags) into transcript JSONL, causing unrecoverable API 400 that recurs after full sanitization
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Problem: our long-running Code tab sessions keep becoming permanently unusable in the middle of real work. Once it happens, every request in the session — including /compact — fails with the same 400, so the session cannot recover from inside. It has recurred within hours even after we sanitized every transcript file on the machine, so we are stuck doing repeated manual repairs with no way to prevent it.
Cause on disk: three JSON keys — start_timestamp, stop_timestamp, flags — appear on text content blocks inside the transcript JSONL files at %USERPROFILE%\.claude\projects\**\*.jsonl. When the request is built from that history, the API rejects it: 400 "Extra inputs are not permitted".
Sample block from a backup (text redacted):
{"start_timestamp":"2026-08-26T09:42:33.302643423Z","stop_timestamp":null,"flags":null,"type":"text","text":"[REDACTED]","citations":[]}
What happened (all 2026-08-26 to 08-27, JST):
- A long session (~941 messages) failed with
400 messages.940.content.0.text.start_timestamp. Retries, summary requests, and /compact all failed. - The same error occurred in a new session and with a different Claude account on the same machine (position became messages.2).
- Reinstalling the Desktop app changed nothing.
- We found the keys in 10 JSONL files across 2 projects. After deleting start/stop_timestamp, the same position failed with
.text.flags— a second foreign key in the same blocks. - We whitelist-sanitized ALL 818 transcript files on the machine and verified zero structural occurrences of the three keys.
- The same day, the keys reappeared: 2 dirty files at first scan (both sessions had gone through our manual /compact procedure), 4 by repair time — the count grew across an app shutdown. Write timestamps: 18:18–18:42 JST.
- We sanitized those 4. Within the next hours, new 400s appeared in further sessions: messages.553, messages.1042 (while a background task was running), and messages.2 minutes after opening a session the next morning.
What Should Happen?
Strip these keys when building API requests — exactly as already happens for the caller key — or do not write them onto message content blocks at all. Sessions should never become permanently unusable because of app-written metadata. Until fixed, any supported way to prevent or auto-recover from this would help; currently there is none we could find (details below).
Error Messages/Logs
API Error: 400 messages.940.content.0.text.start_timestamp: Extra inputs are not permitted
API Error: 400 messages.2.content.0.text.flags: Extra inputs are not permitted
Error during compaction: API Error: 400 messages.2.content.0.text.start_timestamp: Extra inputs are not permitted
request_id: will attach on next occurrence
Steps to Reproduce
We have no from-scratch trigger recipe; the keys were written repeatedly during normal use (around our manual /compact runs and around session/app close; auto-compact involvement not isolated). To observe the broken state and the verified recovery:
- Windows Desktop app, Code tab, local Git repo sessions (our setup: Opus/Fable, permission-bypass, multiple parallel sessions with subagents).
- Work in long sessions; run /compact; close sessions/the app.
- Check %USERPROFILE%\.claude\projects\**\*.jsonl — text blocks carry
start_timestamp/stop_timestamp/flagskeys (samples attached). - Any message in an affected session returns the 400 above; /compact fails too.
- Verified recovery: with the app closed, strip the three keys from the JSONL (whitelist: keep API-schema fields +
caller), reopen the session — it works again. This restored every affected session, every time.
Claude Model
None
Is this a regression?
I don't know
Last Working Version
Unknown. First observed 2026-08-26 on engine 2.1.246.
Claude Code Version
2.1.246 (Claude Code), bundled in Claude Desktop MSIX shell 1.37937.1.0. /status and claude --version are unavailable in the Desktop Code tab; version read from %LOCALAPPDATA%\Packages\Claude_pzs8sxrjxfjjc\LocalCache\Roaming\Claude\claude-code\2.1.246\
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Other
Additional Information
What we verified, item by item:
- The keys exist as JSON keys on content blocks (structural jq check, not string match) in the transcript files; text/tool content around them is intact.
- Whitelist-stripping the keys and reopening restores affected sessions — done on 787 files, then 4 files, then per-session; recovery succeeded every time.
- Contrast: a
callerkey sits on content blocks in 786/818 files and has never produced a 400 here — so the request builder stripscallerbut not these three keys. - Recurrence survives: full-disk sanitization (818 files), app reinstall, and switching Claude accounts on the same machine.
- Controlled marker test on a real 9.4MB transcript: we injected a synthetic
start_timestampkey; our SessionEnd hook removed it at session close and the session was fully functional on reopen. (We did not send a request while the marker was present; all 400s above came from keys written by the app.) - A SessionStart-time sanitize fires and stages a backup, but the in-place file replace does not complete at session start (lock or concurrent append — not isolated). SessionEnd-time sanitize works reliably; this is our current workaround.
- ANTHROPIC_BASE_URL is not picked up by the Desktop Code tab in our tests (settings.json env block, and a persisted user environment variable verified in a fresh shell; app fully restarted) — so a local sanitizing proxy cannot be wired in the GUI.
- A custom PreCompact hook also runs in these sessions (not present in user-level settings.json). A disabled-hook control run is pending; we will post the result in a comment.
Environment: Windows 11 build 26200.9106; Desktop app Code tab (GUI), engine Desktop-managed/auto-updated.
Related issues (same error class): #19513 (thinking.valid — closest), #4378 (tool_use.text), #37452 (compaction corruption), #50375, #62396 (transcript-sanitization precedents).
Attached: claude-issue-evidence.txt — redacted contaminated-block samples from timestamped backups of 4 affected sessions.
11 Comments
Two hops, not one store.
The
callercontrast already shows the request builder can strip keys. The 400s are the three keys that still ride the request. A zero-occurrence grep after sanitizing 818 files is a store receipt. The same-day rewrite (2 dirty files, then 4, then new 400s after the SessionStart in-place replace failed to complete) is the writer.SessionEnd sanitize restoring a session is recovery, not a stop-writing receipt. A SessionStart sanitize that stages a backup but leaves the live file dirty is the earlier hop: downstream only sees what the writer already left.
I did not run Desktop.
Acceptance:
start_timestamp/stop_timestamp/flagsstill 200s because request-build strips them the waycalleris stripped./compacton a previously-400 session succeeds without a manual disk rewrite.#19513 is the same extra-inputs class on
thinking.valid. This report's witness is that sanitizing the store does not stop the writer.Additional occurrences + real request_ids (2026-08-27):
Latest event: API Error: 400 messages.445.content.0.text.start_timestamp: Extra inputs are not permitted
The session recorded three request_ids for it (retries), preceded by the same error at messages.424:
req_011CeSfgbVB1UJj6ERpTx78w / req_011CeSfsX1MBDT4PBn6Hz5wZ / req_011CeSgMDYQLy8WQK86z9zoo
Model shown: Fable 5; Desktop Code tab, engine 2.1.246, permission-bypass. Screenshot attached.
Scale correction: grepping the last ~24h of transcript JSONL for the error string shows it recorded at 30 distinct message positions across ~18 transcript files (some being parent/subagent duplicate copies) — including 5+ fresh sessions failing at messages.1–2. The "6+ events" in the report was an undercount.
Real request_ids for the events cited in the body (superseding "will attach on next occurrence"):
Observability note: the Code tab UI (error card and its "show details" view) never surfaces a request_id, but it IS recorded in the session transcript JSONL — which is how we extracted the above. Surfacing it in the error card would make Desktop-side reports much easier.
These occurred with our SessionEnd sanitize hook deployed — consistent with the triage framing: sanitizing the store does not stop the writer.
Recovery via the documented workaround (close session → SessionEnd whitelist sanitize → reopen) worked again.
The card appeared right after a background-task-stop notice and a user "continue" message, with the context indicator at max.
<img width="635" height="434" alt="Image" src="https://github.com/user-attachments/assets/e2619a4c-0959-44f8-89d2-7a1209361b5a" />
The messages.1–2 failures on fresh sessions are the writer, not leftover dirty files. A SessionEnd sanitize that restores a session can still 400 on the next Code-tab session's first request. Store sanitize remains recovery.
The 30 positions / ~18 files (including parent/subagent copies) is volume. The keys are unchanged:
start_timestamp/stop_timestamp/flagsstill ride the request;calleris already stripped.Add to acceptance:
The missing request_id on the error card is a separate observability bug. The JSONL already records it.
Still 2.1.246. I did not run Desktop.
Follow-up on the pending control experiment (PreCompact hook):
The custom PreCompact hook is ruled out as a cause. We removed it from
settings.json at 11:45 JST on 08-27. A session created fresh at 18:08 JST
(so it never ran that hook) still got the three keys written — the
contaminated block's own start_timestamp value reads
2026-08-27T09:19:55Z (18:19:55 JST) — and the session failed with
400 messages.92.content.0.text.start_timestamp(request_id req_011CeT1XJ6joWJ4sYHFgGgsk, retry req_011CeT1kQ6G8ZtVwyEdvWDq9).
Frequency data point: on 08-27 alone, 13 of 14 sessions on this machine were
contaminated (measured from the one-generation backups our SessionEnd
sanitizer takes before cleaning — most contaminations were silently repaired
at session close and never surfaced as a 400). In every case the contaminated
line sat within the last 35 lines of the transcript at close time.
PreCompact ruled out is the right control. A hook that never ran cannot be the writer.
The fresh 18:08 JST session (never had that hook) still got
start_timestampat 2026-08-27T09:19:55Z and 400'd at messages.92 (req_011CeT1XJ6joWJ4sYHFgGgsk). That is a Code-tab write during the session.13/14 sessions contaminated on 08-27, with the dirty line always in the last 35 lines at close, is the rate the SessionEnd sanitize had been hiding. Silent close-time repair is not a stop-writing receipt. The 400s are only the sessions that continued after the write.
Acceptance add:
I did not run Desktop.
Also affected, on the same Windows build (11 25H2, 26200.9106).
My reproduction is far simpler than the original report, which may help
narrow the cause: it fails on the 2nd message of a brand-new session,
with no long history, no /compact, and no custom hooks.
Steps to reproduce
claudein a terminal)with any project folder.
hello. It succeeds normally.how are you.API Error: 400 messages.2.content.0.text.start_timestamp: Extra inputs are not permittedReproduced 10+ times today. Failure rate: 100%.
Scope
| Environment | Result |
|---|---|
| Desktop app, Code tab (shell 1.37937.3) | Fails |
| Claude Code CLI, native install | Fails |
| Desktop app, Cowork mode | Works |
| claude.ai (web) | Works |
Variables ruled out
a freshly created one.
record the exact version numbers, so please treat this point as
weaker than the others.
Tried without effect
.claude.json.the session once, but the error returns afterwards — matching the
recurrence described in the original report.
Observation
The original report associates the writes with long sessions, /compact
runs, and session close. In my case the 400 appears on the second
message of a fresh session, so at least on this machine the metadata is
already present after a single exchange. I have not inspected the JSONL
at that exact moment to confirm when the keys are written, so this is
an observation about timing rather than about the write path itself.
The 100%
messages.2failure on a brand-new session is the tighter timing bound. SessionEnd has not run between the first 200 and the second 400. The writer is the hop that persists turn 1.Native CLI failing with Code tab, while Cowork and claude.ai 200, is a serializer split — not an API-host split and not Code-tab-only. Those two attach
start_timestamp/stop_timestamp/flagsonto transcript text; Cowork/web do not.Uninstall, deleting
.claude.json, and a one-shot jsonl strip that 400s again already match the store-vs-writer split.Acceptance add:
I did not run Desktop or Windows.
Thanks — the timing bound you drew (writer = the hop that persists turn 1) told me exactly where to look. Here is that measurement, taken before turn 2 was sent. Acceptance item 8 fails: the keys are already there.
Setup
Brand-new Desktop Code tab session. Sent
hello, got a reply, then read the jsonl. Turn 2 had NOT been sent.entrypoint : claude-desktop
version : 2.1.246
shell : Desktop 1.37937.3
OS : Windows 11 25H2, build 26200.9106
cwd : C:\Users\<user> (no project files involved)
hooks : none configured
What the file contains
11 records. The
userrecord is clean. Only theassistantrecord carries the three keys.Assistant record, redacted — ids and uuids replaced, structure verbatim:
{"parentUuid":"<uuid>","isSidechain":false,
"message":{
"id":"msg_<redacted>","type":"message","role":"assistant",
"parent_uuid":"","uuid":"<hex>",
"content":[{"start_timestamp":"2026-08-28T00:26:41.175899535Z",
"stop_timestamp":null,
"flags":null,
"type":"text",
"text":"cached data for request <uuid> not found\n*******\nHello! How can I help you today?",
"citations":[]}],
"stop_reason":"end_turn","stop_sequence":null,
"usage":{...},"stop_details":null},
"requestId":"req_<redacted>","type":"assistant","uuid":"<uuid>",
"timestamp":"2026-08-28T00:26:41.196Z","effort":"high",
"userType":"external","entrypoint":"claude-desktop",
"cwd":"C:\\Users\\<user>","sessionId":"<uuid>",
"version":"2.1.246","gitBranch":"HEAD"}
Sending turn 2 in this session then produced the usual
API Error: 400 messages.2.content.0.text.start_timestamp.One more thing in the same block
The
textfield is contaminated too. It opens withcached data for request <uuid> not found, then a*******separator, and only after that the actual reply. That prefix is not model output and is rendered verbatim in the app UI. It may travel the same path as the three keys, but I have not verified that.The pre-turn-2 jsonl closes item 8 as fail. The assistant text block already has
start_timestamp/stop_timestamp/flagsafter turn 1. SessionEnd has not run. I did not run Desktop.The
cached data for request … not foundprefix is a second contamination in the same block. Extra inputs 400s the three keys; the prefix lives intext, so request-build accepts it and the UI renders it. A first-turn 200 with that prefix is still a dirty transcript, just a schema-legal one.Do not treat the prefix as the same writer until a session has the keys without the prefix, or the prefix without the keys. Score {keys, text_prefix, turn2_400} separately.
Acceptance add:
textmust not contain a non-model prefix such ascached data for request … not found.Agreed on scoring them separately. Reporting what I have, not a conclusion.
Across every session I have looked at so far, all three score together: keys present, prefix present, turn 2 400s. I have not produced a session with keys but no prefix, or a prefix but no keys, so I cannot settle the same-writer question from this end. Item 11 stays open as far as my data goes.
Co-occurrence across every session you have is still not a same-writer receipt. It is a bound on this serializer: the two contaminations have fired together so far.
You do not need a naturally split session to separate the Extra-inputs 400 from the prefix. On a dirty jsonl you already have:
start_timestamp/stop_timestamp/flags, leave thecached data for requestprefix. Turn 2 should 200 — the prefix is schema-legal.If (1) 200s and (2) 400s, the Extra-inputs hop is the keys even when the prefix always rode along. Item 11 stays open until a session is written with one and not the other.
I did not run Desktop.