Skip to content

Align hooks with Claude Code's documented output contract (#15) - #33

Open
1aifanatic wants to merge 1 commit into
scaledown-team:mainfrom
1aifanatic:fix/hook-output-contract
Open

1aifanatic wants to merge 1 commit into
scaledown-team:mainfrom
1aifanatic:fix/hook-output-contract

Conversation

@1aifanatic

Copy link
Copy Markdown

Closes #15

Problem

  • PostToolUse printed a top-level {tool_response: ...}, which isn't part of Claude Code's hook contract, so every replacement was ignored.
  • extractText didn't recognise Claude Code's real Bash ({stdout, stderr, interrupted, isImage, noOutputExpected}) or Read ({type:"text", file:{content, ...}}) result shapes. Those results never reached the filter or compression path.
  • UserPromptSubmit printed {prompt: ...}, which this event can't do: it can only add context. The intent hint was dropped, and "prompt compression" called the API and booked savings for text the model never received.

Changes

hooks/post-tool-use.ts

  • extractText/replaceText handle Bash stdout and Read file.content. The rest of the shape (stderr, interrupted, isImage, file metadata) is preserved. Image output is never rewritten. The existing output/content/text fallbacks are kept for other tools.
  • New postToolUseOutput() emits {"hookSpecificOutput":{"hookEventName":"PostToolUse","updatedToolOutput":<same shape>}}.
  • The decision logic moved into a pure processToolResponse(). main() books savings and requests only after a replacement has been written. No-op and API-failure paths print {} and book nothing.

hooks/user-prompt-submit.ts

  • Emits only {"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"[DietCode intent: …]"}}.
  • Removes the NIAH prompt compression and its savings booking. The SCALEDOWN_COMPRESS_THRESHOLD/SCALEDOWN_NIAH_DISABLE config fields stay in Config, but the hook no longer reads them.

Docs: README "What it does", "How it works", the feature matrix (the "Auto compression (large prompts)" row is removed), the env var table, "How compression works" (rewritten), the dietcode setup summary and agents-md/AGENTS.md. This also removes the unqualified "50–70% before the prompt reaches the model" claim (related to #19).

Tests

  • hooks/post-tool-use.test.ts: the mock-only "compression logic" tests are replaced with real processToolResponse tests. They cover realistic Bash, Read and MCP-style inputs, check that output is replaced rather than appended, keep stderr and status, and cover the small-output no-op, API failure and the disabled toggle.
  • hooks/user-prompt-submit.test.ts: the mock-only tests are replaced with output-shape tests.
  • New hooks/hook-contract.test.ts runs the built dist/hooks/*.js over stdin/stdout, as Claude Code does. It uses a temporary HOME, so it never touches the real ~/.scaledown, and a local stub Scaledown API. It asserts the exact JSON contract, which endpoints get called, and that stats.json only changes when a replacement is emitted. It's added to npm run test:unit; CI already builds first, and the suite skips if dist/ is missing.
  • Results: npx tsc --noEmit is clean, and the 73 tests across the hook and niah suites pass.

Live verification (the request actually sent to the model)

I ran claude -p on Claude Code 2.1.310 with --settings registering the built hooks, a stub Scaledown API, and a prompt asking the model to run ls -l <dir> and echo the tool result verbatim, plus any [DietCode intent text in its context:

Hooks What the model reported receiving
origin/main Raw ls -l (permissions, owners, dates); intent hint: NONE; totalSaved: 0
this branch sub/ … alpha.txt 3B … Summary: 3 files, 1 dirs (the filtered output replaced the original); [DietCode intent: search (82%)]

Not covered / follow-ups

  • Codex CLI: the same commands are registered, but I haven't independently verified Codex's output contract (Align Claude Code hooks with the documented input/output contract #15 asks for that). The README says so.
  • MCP tool results that arrive as content-block arrays are still passed through unchanged. Only string fields are handled.
  • hook-integration.test.ts (the claude -p smoke tests that need ANTHROPIC_API_KEY) is unchanged. The live check above could become a CI assertion there if maintainers want to spend API budget on it.

🤖 Generated with Claude Code

PostToolUse now emits replacements as hookSpecificOutput.updatedToolOutput
in the tool's own shape (Bash stdout with stderr/interrupted kept, Read
file.content with metadata kept) instead of a top-level tool_response that
Claude Code ignores. Bash and Read results now actually reach the filter and
compression path. Savings are booked only after a replacement is emitted.

UserPromptSubmit cannot replace the prompt, so it now emits only the intent
hint as hookSpecificOutput.additionalContext and no longer calls compress or
books prompt-compression savings that never reached the model.

Adds subprocess contract tests for the built hooks (temporary HOME, stub
Scaledown API) and updates README/setup/AGENTS.md claims.

Refs scaledown-team#15

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Align Claude Code hooks with the documented input/output contract

1 participant