Return the replacement in the tool's own output shape - #8
Merged
Conversation
Every compression was being thrown away.
Claude Code validates a PostToolUse hook's updatedToolOutput against the
schema of the tool it replaces. Read, Grep and Bash all return objects. The
hook handed back a bare string, so Claude Code rejected it and kept the
original:
PostToolUse hook returned updatedToolOutput that does not match Grep's
output shape; using original output.
[{ "expected": "object", "code": "invalid_type", "path": [],
"message": "Invalid input: expected object, received string" }]
The model never saw a compressed result. Orvix could not tell — nothing is
reported back to the hook — so it recorded the saving in gain.jsonl anyway and
`orvix gain` reported compressions that had not happened. On the session where
this was found, all four were rejected and the real saving was zero.
The replacement now goes back in the shape it arrived in, mirroring
extractText case for case so the field written is the one that was read. Grep
states how many lines it returned, so that count is updated too.
Neither the tests nor the benchmark caught this because both replayed
`tool_response` as a bare string, a shape no tool actually produces. Both now
use the real ones, taken from a recorded session, and both fail loudly if the
hook ever returns something the tool would not accept.
The benchmark's ratio is unchanged at 0.699 — the arithmetic was right, it was
measuring a path that did not exist.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # benchmarks/results/synthetic.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every compression was being discarded. This is the
PostToolUse:Grep hook warningthat has been open all session, and the answer is worse than expected.What the transcript says
Claude Code validates a hook's
updatedToolOutputagainst the schema of the tool it replaces. Read, Grep and Bash all return objects. The hook handed back a bare string:The model never saw a compressed result. Orvix cannot tell — nothing is reported back to the hook — so it recorded the saving in
gain.jsonlanyway.On the session where this was found, all four compressions were rejected: three Grep and one Bash.
orvix gainreported 3 864 tokens saved. The real figure was zero.The shapes, taken from a recorded session
{ file: { content } }{ mode, numFiles, filenames, content, numLines, totalLines, appliedLimit? }{ stdout, stderr, interrupted, isImage, noOutputExpected }The replacement now goes back in the shape it arrived in, mirroring
extractTextcase for case so the field written is the one that was read. Grep states how many lines it returned, so that count is updated with it.Why nothing caught it
Both the tests and the benchmark replayed
tool_responseas a bare string — a shape no tool actually produces. The hook was correct against its fixtures and wrong against reality, and the fixtures were the only thing looking.Both now use the real shapes and fail loudly if the hook ever returns something the tool would not accept.
The benchmark ratio is unchanged at 0.699. The arithmetic was always right; it was measuring a path that did not exist. It measures the real one now.
Verification
npm run build,npx tsc --noEmit,npm test— 376 tests passnpm run benchmark— ratio 0.699, success_delta 0, both fixtures preservedexpected 'string' to be 'object'— the same complaint Claude Code made