From claude-code-session-repair
Repair a Claude Code session whose transcript JSONL has an orphan `server_tool_use` / `*_tool_result` content block, causing every API call (including `/compact` and `claude --resume`) to fail with HTTP 400 "unexpected tool_use_id found in advisor_tool_result blocks: srvtoolu_... Each ... block must have a corresponding server_tool_use block before it". Use when: (1) a Claude Code session dies mid-advisor call with "socket connection was closed unexpectedly", (2) resume / compact returns the 400 above naming `srvtoolu_...`, (3) `/remote-control` (or any system event) activated mid-stream while the assistant was emitting an advisor response. Covers diagnosis via grouping JSONL lines by shared `message.id`, surgical deletion of the orphan pair (both halves), and the line-number-drift gotcha that occurs when the file grows between inspection and edit.
How this skill is triggered — by the user, by Claude, or both
Slash command
/claude-code-session-repair:claude-code-session-jsonl-orphan-advisor-tool-resultThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
A Claude Code session becomes permanently stuck: every API request — typing a
A Claude Code session becomes permanently stuck: every API request — typing a
new prompt, /compact, or even claude --resume <id> — returns:
API Error: 400 messages.<N>.content.0: unexpected `tool_use_id` found in
`advisor_tool_result` blocks: srvtoolu_<id>. Each `advisor_tool_result`
block must have a corresponding `server_tool_use` block before it.
The session can no longer be used. The transcript file holds the conversation hostage.
All of the following are common triggers:
advisor() and the message stream was interrupted
("API Error: The socket connection was closed unexpectedly")./remote-control (or any other system command) activated while the assistant
was mid-stream emitting the advisor response, splitting the writes across
more JSONL lines than usual./compact, also got 400.The Anthropic Messages API requires that a server_tool_use block AND its
corresponding *_tool_result block live in the same assistant message's
content array. Claude Code's JSONL stores each "delta" of an assistant
response on a separate line and groups them back into one message at send
time using the shared message.id. The bug happens when the grouping logic
emits them as two separate messages — the _tool_result lands in a
message whose content[0] has no preceding server_tool_use → 400.
The corrupted session is still loaded in their terminal. It can't write anything (every API call 400s), so it's effectively read-only, but quit it before editing to be safe.
# Project dir = main repo, NOT a worktree. Worktree sessions still live under
# the repo's project dir, indexed by the cwd Claude Code was launched in.
SF=~/.claude/projects/-Users-<user>-<repo-path>/<sessionId>.jsonl
ls -la "$SF"
If claude --resume <id> reports "No conversation found", you are running it
from a directory whose project dir doesn't contain the session. Resume from
the directory the session was originally launched from (often the main repo
root, even if the session later worked inside a worktree). Note the encoded
project-dir name collapses both / and _ to -, so a folder like
...-worktrees-cute-tab may actually be the worktree cute_tab — git worktree list reveals the real path (see Notes for the false-MISSING gotcha).
Grab the srvtoolu_<id> from the 400 error message, then:
grep -n "srvtoolu_<id>" "$SF" | awk -F: '{print $1}'
You will typically see 2–4 line numbers. The actionable ones are lines whose content contains either:
"type":"server_tool_use" with that id, OR"type":"advisor_tool_result" (or tool_result) with "tool_use_id":"<id>".Any other lines that mention the id are usually just error-log strings (e.g., the 400 response itself recorded back into the JSONL) — leave those alone.
Both halves should share message.id. Find the full group:
grep -n "msg_<messageId>" "$SF" | awk -F: '{print $1}'
Check the surrounding lines — what's the message structure (thinking,
text, tool_use, etc.) when you remove the orphan pair? The remaining
blocks must still form a valid message (at minimum: one non-empty content
block). For a typical advisor mid-stream, the message looks like:
thinking, text, server_tool_use, [advisor_tool_result], thinking, text, tool_use(next_skill). Dropping the middle two leaves a valid skeleton.
BK="${SF}.bak-$(date +%Y%m%d-%H%M%S)"
cp "$SF" "$BK"
# Delete the orphan pair — replace 95 and 99 with YOUR line numbers
awk 'NR!=95 && NR!=99' "$BK" > "$SF.tmp" && mv "$SF.tmp" "$SF"
Critical: delete BOTH halves of the orphan pair, not just the
*_tool_result. If you delete only the result, the server_tool_use becomes
the new orphan (tool_use without tool_result) and the API will reject it
just as forcefully.
# Orphan content blocks should now be zero. The id may still appear in
# 1–2 *error-log text* lines (the 400 message Claude Code recorded) —
# that's harmless plain text.
grep -cE '"(server_tool_use|advisor_tool_result|tool_result)"[^}]*srvtoolu_<id>|srvtoolu_<id>[^}]*"(server_tool_use|advisor_tool_result|tool_result)"' "$SF"
# Expect: 0
cd <directory-where-session-was-originally-launched>
claude --resume <sessionId>
Derive that cwd from git worktree list, do NOT construct it by decoding
the encoded project-dir name — the encoding is lossy (/ AND _ both →
-), so a name like ...-my-app-propensity--claude-worktrees-feature-v7
can decode to the wrong path: the repo is really my_app_propensity
(underscore) and the --claude-worktrees- segment is /.claude/worktrees/.
Handing the user a literally-decoded cd path (e.g. the hyphen spelling) will
fail with cd: no such file or directory even though the session is fine.
This applies when telling the user where to resume, not just when locating
the JSONL — match the encoded <name> against git worktree list output by
normalising _/-, then quote the real on-disk path.
The 400 is gone. The advisor consultation text is lost from history, but every other turn, todo, commit, and tool result is intact. Prompt the assistant to continue from where it was ("continue with Task N" or similar).
server_tool_use or _tool_result
content block (only — possibly — in error-log text strings).claude --resume <id> opens normally and accepts a new prompt without
returning 400.Re-grep for the current line numbers immediately before running the awk
delete. If any time has passed since your initial inspection — especially
if the user attempted /compact or another resume that got logged as an
error — the JSONL has GROWN, and the line numbers you noted earlier no
longer point at the orphans. Deleting the old numbers will corrupt random
unrelated lines and is hard to detect because there's no immediate error.
Symptom that you hit this: after your delete, grep -c for the orphan id
in server_tool_use|*_tool_result content blocks still returns > 0.
Recovery: restore from the .bak file you (hopefully) created, re-grep for
current line numbers, and try again.
Session 97f294f0-f54b-42d1-9f51-a23c3bee0a68, file at 345 lines:
$ grep -n "srvtoolu_01PqQwhhaakjWqL3YpqAjo7H" "$SF" | awk -F: '{print $1}'
95
99
328
334
Inspect each:
"type":"server_tool_use" with that id → orphan, delete"type":"advisor_tool_result","tool_use_id":"<id>" → orphan, delete<local-command-stderr> text block (the /compact error
Claude Code recorded) → leave alonetext block (the second 400 echoed
back) → leave aloneAll four lines (95, 99, 328, 334) share message.id
msg_013PHxJ8qHe88WcYWgEhKXNR plus its neighbours (93, 94, 100, 101, 102),
confirming 95 and 99 are halves of one assistant response. Backup, awk 'NR!=95 && NR!=99', file → 343 lines, orphan content blocks → 0, resume
succeeds.
~/.claude/projects/<encoded-cwd>/<sessionId>.jsonl. The <encoded-cwd>
is the launch cwd with both / AND _ replaced by - (the encoding is
lossy — underscores and slashes both collapse to hyphens). Worktree sessions
usually live under the main repo's encoded path (because the session was
launched from the main repo, then cd'd), not under a worktree-specific
path. If claude --resume <id> says "No conversation found", you are running
it from the wrong cwd._ → -, an encoded segment like
...-worktrees-cute-tab can map to a real directory named cute_tab (or
cute-tab) — you can't tell which from the encoded name alone. An ls of
the hyphen spelling will report MISSING even though the underscore-named
worktree is alive. To find the true launch cwd, run git worktree list (or
check sibling dirs) and match by normalising _/-, rather than trusting a
single literal ls.<X>_tool_use / <X>_tool_result pair
(web_search, code_execution, etc.), not just advisor — the API contract
is identical.worktree-index-corrupt-async-post-commit-hook and
git-amend-hits-async-post-commit-hook-commit, which corrupt the git
index rather than the Claude Code session transcript.server_tool_use content block and its corresponding *_tool_result
content block must appear in the same assistant message's content
array — splitting them across two assistant messages returns HTTP 400).Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub wan-huiyan/claude-code-session-repair --plugin claude-code-session-repair