Skip to content

Return the replacement in the tool's own output shape - #8

Merged
lightningpixel merged 2 commits into
mainfrom
fix/hook-output-shape
Aug 9, 2026
Merged

Return the replacement in the tool's own output shape#8
lightningpixel merged 2 commits into
mainfrom
fix/hook-output-shape

Conversation

@lightningpixel

Copy link
Copy Markdown
Owner

Every compression was being discarded. This is the PostToolUse:Grep hook warning that has been open all session, and the answer is worse than expected.

What the transcript says

Claude Code validates a 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:

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 cannot tell — nothing is reported back to the hook — so it recorded the saving in gain.jsonl anyway.

On the session where this was found, all four compressions were rejected: three Grep and one Bash. orvix gain reported 3 864 tokens saved. The real figure was zero.

The shapes, taken from a recorded session

tool result
Read { file: { content } }
Grep { mode, numFiles, filenames, content, numLines, totalLines, appliedLimit? }
Bash { stdout, stderr, interrupted, isImage, noOutputExpected }

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 with it.

Why nothing caught it

Both the tests and the benchmark replayed tool_response as 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 pass
  • npm run benchmark — ratio 0.699, success_delta 0, both fixtures preserved
  • The new test was written first and failed with expected 'string' to be 'object' — the same complaint Claude Code made

lightningpixel and others added 2 commits August 9, 2026 16:23
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
@lightningpixel
lightningpixel merged commit 1704702 into main Aug 9, 2026
11 checks passed
@lightningpixel
lightningpixel deleted the fix/hook-output-shape branch August 9, 2026 16:10
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