anthropics / anthropics/claude-code

A `/goal` Stop-hook record written mid-turn splits an advisor server-tool call from its result, permanently bricking the session (400 loop)

Open
#95,463 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area:core area:hooks bug has repro platform:linux
Dominant language
TypeScript
Stars
147k
Forks
24k
PR merge metrics
PR metrics pending

Description

Version: 2.1.277 (Claude Code)
Platform: Linux 6.8.0 / bash
Model: claude-opus-5 (1M context)
Session: 468afc33-879a-43de-8605-ce59e6436546

Summary

Setting a session-scoped goal (/goal, a Stop hook) while a turn is in
flight causes the isMeta: true notice record to be appended to the
transcript in the middle of an assistant message that has an
outstanding advisor server-tool call
. The single assistant message is
then persisted as two runs of records separated by a user record.

On a later request the API rejects the rebuilt history:

API Error: 400 messages.5.content.0: unexpected `tool_use_id` found in
`advisor_tool_result` blocks: srvtoolu_01XJ8SNvoZBHgPB8mgjgt5Cv. Each
`advisor_tool_result` block must have a corresponding `server_tool_use`
block before it.

Every subsequent request fails identically. The session cannot be
resumed, compacted, or continued — /compact re-sends the same history
and fails the same way. The only recoveries are /rewind (lossy) or
hand-editing the transcript JSONL.

Evidence

All records below share assistant message id msg_011Cf9b2qQDjRFtbabVkdtgS
except the two injected ones. Line numbers are from the session JSONL.

line timestamp (UTC) type content
21862 19:46:28.459 assistant thinking
21863 19:46:28.481 assistant server_tool_use: srvtoolu_01XJ8SNvoZBHgPB8mgjgt5Cv (name advisor)
21864 19:46:35.947 attachment
21865 19:46:35.948 user, isMeta: true "A session-scoped Stop hook is now active with condition: …"
21866 19:47:58.854 assistant advisor_tool_result: srvtoolu_01XJ8SNvoZBHgPB8mgjgt5Cv
21867 19:47:58.856 assistant thinking

The advisor call took ~90s (19:46:28 → 19:47:58). The /goal record was
written 7.5s into it, i.e. squarely inside the in-flight turn.

This is the only defect of its kind in the session: 20 server_tool_use
blocks (all advisor) and 20 advisor_tool_result blocks; the other 19
pairs are contiguous within a single assistant message and are fine.

Delayed onset. The session kept working for ~52 minutes after the
split was written, then began failing at 20:40:08 and never recovered —
12 consecutive identical 400s through 22:00:09, when it was abandoned
(session cost at that point: $93). There was no compaction in that
window (last compact_boundary was 19:45:14, i.e. before the split),
so the in-memory history was evidently fine and something later rebuilt
the request from a form where the pair straddled a boundary.

The reported index messages.5 is consistent with a short prefix (the
19:45 compaction summary is messages[0]): the intervening user record
gives the prefix trimmer a legal cut point between the call and its
result, which no other advisor pair in the session has. Once the trim
reached that point, the server_tool_use half was dropped and the
result became content.0 of the next assistant message.

Repro (hypothesized)

  1. Start a turn that makes a long-running advisor (server-tool) call.
  2. While it is in flight, issue /goal <condition>.
  3. Observe that the Stop-hook notice is appended to the transcript
    between the server_tool_use and advisor_tool_result records.
  4. Continue the session until the prefix trim reaches that boundary.

Expected

Either the meta record is buffered until the assistant turn closes, or
the request builder keeps a server-tool call and its result in one
assistant message regardless of interleaved meta records.

Actual

The turn is persisted split; the history becomes unserializable; the
session is unrecoverable in-product.

Suggested fixes (any one prevents the brick)

  1. Don't write meta/system-notice records into an open assistant
    turn.
    Queue them until the turn boundary. This is the root fix — the
    transcript should never represent one assistant message as two runs
    separated by a user record.
  2. Merge on rebuild. When reconstructing the API payload, coalesce
    adjacent assistant records sharing a message.id across intervening
    meta-only user records (which are not sent to the API anyway).
  3. Treat a server-tool call + result as atomic in the prefix trimmer.
    Never choose a cut point between a server_tool_use and its matching
    result block.
  4. Fail soft. On a 400 of this shape, self-heal (drop the orphaned
    result, or the pair) rather than looping the same rejected request 12
    times. At minimum, surface actionable guidance instead of a bare 400 —
    nothing in the product tells the user the session is permanently dead
    or how to recover it.

Workaround

With the session exited, delete the injected attachment + meta-user
records from the transcript and re-point the advisor_tool_result
record's parentUuid at the server_tool_use record's uuid, making
the assistant message contiguous again. Then claude --resume <id>.
This recovered the session with no loss of conversation.

Caveat for anyone copying this: address the records by uuid, not by
line number
. A line-numbered sed -i that runs twice deletes the next
two records instead (in our case the advisor_tool_result and a
thinking block), leaving the mirror-image corruption.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

No source files or tests are named. Start by reproducing the /goal Stop-hook scenario, then trace transcript JSONL writes, assistant-message reconstruction, and prefix trimming around the server_tool_use and result. Done means an in-flight advisor call remains atomic and resumed sessions produce valid API history without a 400 loop.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
cli
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.