Skip to content

Escape restored values spliced into streamed tool arguments - #91

Merged
steflsd merged 1 commit into
mainfrom
fix/restore-tool-arg-escaping
Jul 27, 2026
Merged

Escape restored values spliced into streamed tool arguments#91
steflsd merged 1 commit into
mainfrom
fix/restore-tool-arg-escaping

Conversation

@steflsd

@steflsd steflsd commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai ignore

Confirms and fixes the corruption hazard I flagged as unconfirmed earlier in this series. It was real, and the mechanism is a one-line asymmetry between two sibling functions.

The bug

A streamed tool-call argument fragment is JSON text one level deeper than the SSE record carrying it — the client concatenates every partial_json and parses the result.

restoreJsonText escapes a restored value for its string context precisely because of this (vault.ts, response-body path). restoreToolArgText returned the value raw.

So a restored value containing a newline, ", or \ produced invalid JSON in the reconstructed tool input:

SyntaxError: Bad control character in string literal in JSON at position 26

A PEM key, a quoted password, or any multi-line file content the agent was writing back would corrupt the call. Under the default detected policy this hits exactly the values that are supposed to round-trip — content-derived detections the agent already read locally.

Why it went unnoticed

Every existing tool-argument test used values with no JSON-special characters (alpha-registry-eu-west-1x, bravo-content-hostname-9z). The escaping was never exercised. Both new tests JSON.parse the reconstructed argument instead of substring-matching it.

Fix

Escape in restoreToolArgText, and route tool fragments through it under every policy.

all is the second half of this: it sets withhold: policy !== "all", so it skipped the withhold bookkeeping entirely and fell through to the blanket restore — corrupting the same way by a different path. Escaping is a property of the JSON context, not of the withhold policy, so it can't hang off the withhold flag. withheld stays undefined under all so the deep-sweep behaviour below is unchanged.

Scope

Streaming path only. The buffered path already went through restoreJsonrestoreJsonText and was correct. Registry secrets are unaffected under the default policy because they're withheld rather than restored.

pnpm check: 51 files, 565 tests, all pass (563 + 2 new).

🤖 Generated with Claude Code

A streamed tool-call argument fragment is JSON text one level deeper than
the SSE record that carries it: the client concatenates every partial_json
and parses the result. restoreJsonText escapes a restored value for its
string context precisely because of this, but restoreToolArgText returned
the value raw.

So a restored value containing a newline, `"`, or `\` produced invalid JSON
in the reconstructed tool input. A PEM key, a quoted password, or any
multi-line file content the agent was writing back would corrupt the call —
`JSON.parse` fails with "Bad control character in string literal". Under the
default `detected` policy this reaches exactly the values that are supposed
to round-trip: content-derived detections the agent already read locally.

Escape in restoreToolArgText, and route tool fragments through it under
every policy. `all` previously skipped the withhold bookkeeping and fell
through to the blanket restore, so it corrupted the same way; escaping is a
property of the JSON context, not of the withhold policy.

The existing tool-argument tests all used values with no JSON-special
characters, which is why this went unnoticed. Both new tests parse the
reconstructed argument rather than substring-matching it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@steflsd
steflsd merged commit 999473f into main Jul 27, 2026
4 checks passed
@steflsd
steflsd deleted the fix/restore-tool-arg-escaping branch July 27, 2026 08:45
@github-actions github-actions Bot mentioned this pull request Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant