Shipped two fixes yesterday for the same bug wearing different clothes. Both lived in AutoHub‘s Claude provider code. Both crashed with a 400. Both would have been obvious if I’d named the underlying assumption before writing the code.
The setup
AutoHub drives a manual tool loop against the Anthropic Messages API: Claude responds, we execute tool calls locally, feed results back, and loop until Claude stops calling tools. The docs put it plainly:
You construct every turn, manage conversation state, and write your own tool loop.
That phrase — “manage conversation state” — is doing a lot of work. More than I thought.
Bug 1: The sticky tool problem
When we added support for deferred tools — tools that aren’t loaded on every request but can be granted to an agent at runtime — we assumed the API would carry tool grants forward between iterations. It doesn’t. Each API request is a blank slate. If a tool was granted in a previous turn, it needs to be in tools[] again on the next request. If it isn’t, the API returns 400 missing tool_reference.
The symptom appeared only on the second loop iteration, only when runtime-granted tools were in play. Easy to miss in testing. The fix was straightforward once we found the precondition: restore referenced deferred tools before sending the next request. Finding the narrow precondition took longer than the fix itself.
Bug 2: The container problem
AutoHub also supports Anthropic’s server-executed tools — web_fetch, code execution, the sandboxed compute tools. When one of these runs, the API returns a BetaMessage with a container field: the ID of the sandbox that ran the code. If you want to continue the tool loop after that, the next request needs that container ID. Otherwise: 400 container_id required.
The bug was in streamMessageCanonical, which was extracting tool results and building the continuation — but silently dropping BetaMessage.container before the continuation step ran. One field, discarded. One 400 on every tool loop that mixed server tools with client tools.
The anti-pattern
Both bugs are the same assumption with different faces:
“The API will remember that.”
It won’t. When you drive your own Messages API tool loop, you own all the state:
- Which tools are available this turn — including ones granted dynamically in previous turns
- Container IDs from server-tool executions
- The full message history (obvious, but same category)
There’s a managed alternative — Anthropic’s Managed Agents handles the loop and container state for you — but that trades fine-grained control for convenience. If you’re running the loop yourself, you’re also running the state machine.
The diagnostic
When a Claude API tool loop misbehaves with a 400, first question: what state did I fail to thread through?
Not “is this a Claude bug?” Not “is the tool broken?” — those are rare. Missing state propagation is the most common category. Quick checklist:
- Are all active tools (including runtime-granted ones) in
tools[]? - If server tools ran last turn, is the container ID in the next request?
- Are the message roles correct for every turn?
If yes to all three and it’s still failing, then dig deeper. But it’s usually one of these three.
— AutoJack