A sieve that was not in the flow
skill/redact-secrets, #3725, fixed by an outside contributor in #3815 · one read order, one honest test · 2026-08-30
The one line
src/hooks/src/skill/redact-secrets.ts - const toolOutput = (input as any).tool_result || (input as any).output || ''; + const toolOutput = + (input as any).tool_response || (input as any).tool_result || (input as any).output || '';
CC sends tool_response on PostToolUse (types.ts, #3418, from the decompiled payload builder).
tool_result and output are legacy aliases no real payload carries. So toolOutput was empty on every
invocation, the hook returned silent success, and every pattern in this layer, the #3589 GitLab family included, had never matched
production output. Its sibling posttool/secret-handler.ts had read tool_response first all along.
Send a payload: which field is it in?
| reader | before #3815 | after #3815 |
|---|---|---|
| redact-secrets sees the token | ? | ? |
| the unit suite (fixtures built with the same field) | ? | ? |
Three sieves, one held sideways
output from a tool
|
v
[ secret-handler ] reads tool_response catches (bounded in-place redaction)
|
v
[ redact-secrets ] read tool_result -> '' PASSED EVERYTHING THROUGH <- #3725
|
v
[ crypto.sanitizePayload ] telemetry payloads catches
the GitLab patterns added in #3589 went into all three; the middle one was decorative
The test that earns the merge
| run | result | what it proves |
|---|---|---|
PR head: redact-secrets.test.ts + new integration test | 70 / 70 | two unit cases (reads tool_response; prefers it over tool_result) and three integration cases through the BUILT bundle |
the same integration test against MAIN's plugins/ork/hooks/dist/skill.mjs | 2 fail / 1 pass | the two detection cases fail, the clean-output case passes: it measures the fix, not the mock |
npm run typecheck on PR head | clean | no type drift |
- Spawned tier:
node bin/run-hook.mjs skill/redact-secretswith a CC-shaped PostToolUse payload (hook_event_name,tool_response, no aliases); asserts exit 0, no token in stdout, the::warning::on stderr. - In-process tier: imports
dist/skill.mjsbyfile://URL and calls the registered hook; cross-platform. - Skips itself when the built bundle is absent, so a source-only checkout does not fail for the wrong reason.
The side-finding: every hook no-ops on Windows
Kept out of scope by the contributor, correctly. run-hook.mjs imports its dist bundle by bare absolute path;
on win32 that is not a valid ESM specifier, Node throws ERR_UNSUPPORTED_ESM_URL_SCHEME, and the runner's catch maps it to
{"continue":true} before stdin is read. The Windows smoke test is green, which means it asserts exit codes, not verdicts.
Tracked as #3817 with a fix shape: pathToFileURL, a catch that does not certify a bundle that failed to load, and a Windows probe that expects a deny.