A raw Docker configuration, as `cat ~/.docker/config.json` prints it, keeps its credential under `"auth":`, which only the auth_config name matched. Count `auth` as a credential word when it ends the word, so `"auth":`, `AUTH=` and `oauth=` are redacted while `author` and `auth_method` are not.
Three review findings on the generic credential rule:
- The unbounded name parts around the credential word made the expression rescan the rest of the text after each dot or hyphen. 128,000 characters of `a.` took about 8 seconds, and 256,000 ran past the hook timeout. The name parts are now bounded to 64 characters each, which brings the same input down to about 30 ms.
- A bare value ran to the next whitespace, so `{"token":null,"status":"failed","exit_code":1}` lost its status and exit code. A bare value now ends at the next comma, brace, bracket, semicolon or quote. A JSON object or array value is still redacted to the end.
- A quoted value stopped at an escaped quote, so `{"api_key":"part1\\" part2"}` kept `part2`. Quoted values now skip escaped characters.
An unquoted value ran to the next whitespace, so `DOCKER_AUTH_CONFIG={"auths": {"registry": {"auth": "..."}}}` kept everything after its first space, the auth value included. A value that opens with { or [ is now redacted to the end of the summary.
The generic redaction matched a credential word only right before the separator, so `AWS_SECRET_ACCESS_KEY=...` or `DOCKER_AUTH_CONFIG=...` kept its value in the activity summary. Match a name that contains secret, token, password, api_key, access_key, private_key, credential or auth_config anywhere, and take an unquoted value up to the next whitespace. `max_tokens`, `tokenizer` and `author` stay as they are.
With Claude Code's tool_response read, command output such as `cat .env` now reaches the activity log's output_summary. Its redaction covered a few token forms, not the ones governance-capture detects: generic key, secret and token assignments (`api_key = "..."`, `OPENAI_API_KEY=...`, `"token": "..."`), JWTs, private keys, and ghu_/ghr_ tokens. Redact those too.
Claude Code passes a PostToolUse hook the result of its tool as tool_response. For Bash, that is an object with stdout, stderr, interrupted and isImage. Three hooks read only tool_output, the shape that the Cursor adapter builds, so under Claude Code they never saw any output:
- post-bash-pr-created never printed the PR URL and the review command after `gh pr create`;
- governance-capture never ran its secret detection on command output, such as that of `cat .env`;
- session-activity-tracker logged an empty output_summary for every tool.
Add getToolResult and getToolResultText to hook-input.js. They read tool_response first, then tool_output. Use them in the three hooks. governance-capture now also reads the adapter's { output }, where it used to accept only a string.
Preserve complete source ancestry for PRs #3512, #3513, #3515, #3516, #3517 and #3518. Repair bounded recovery, alias compatibility and evaluator packaging gaps while keeping registry reuse inside the canonical loader. Verified actual hook, setup, dashboard, evaluator, sandbox and profile workflows plus full npm, Python, lint and supply-chain checks. ECC-043 preparation only; integration remains with the delivery owner.
CodeRabbit security finding on the merged sed work (CWE-693, verified live
on main via #3495): GNU sed labels for b, t, T and : end at ';' or newline —
verified against GNU sed 4.9 ('bar;e cmd' parses label 'a' and still runs
cmd; '}' is not a label terminator). Reading those operands to end of line
let 'sed :a;e git commit --no-verify' pass the hook while sed executed the
e command.
Labels are now read up to ';' or newline and command parsing resumes after
them. Text, filename and quit operands (a, c, i, r, w, q, Q) keep the
end-of-line reading: nothing after them on the line executes.
5 regression tests pin the three bypass paths and the benign boundary
(a bare label and a bare branch stay data). Suite: 817/817.
Scoped adaptation of PR #2716: keep compact rendering as the default and require ECC_STATUSLINE_USAGE=1. Installer registration, managed aliases and the Codex background bar remain outside this slice.
8a7d6129 added the external-contract question for every code change that
handles data and updated the pinned tests to match, including the legacy
test "code condensed Edit hint for an unknown profile is unchanged". Its own
evaluation then found the gated arm did not improve (25 to 23 of 48), so the
question has no evidence behind it as a default.
- Restore the test expectations 8a7d6129 changed: questionIdsFor, the
byte-identical code text for src/a.py, the condensed hints, and the hook
question lists default to data-schema without external-contract.
- Add questionIdsFor/condensedHintFor cases with { externalContract: true }.
- Add hook cases: GATEGUARD_EXTERNAL_CONTRACT_QUESTION=1/true/ON asks it
beside data-schema and in the unknown-profile set, never without data;
unset, 0, false and off keep the original four.
Against main: target-class 5 failures, fact-force 10 failures.
Each of these is credited on main, reproduced against upstream 2d515e4f:
- rg -t py / -T js / --type-not js, fd -e py, and the Grep tool's
type: "py" credit an Edit of tokenizer.js. Type and extension values
are skipped as plain value flags, so they never filter.
- Glob **/tokenizer.py, rg tokenizer src/*.py, and
git grep tokenizer -- '*.py' credit tokenizer.js. The Glob pattern is
never a filter, and a glob operand scopes as its literal directory.
- Glob **/tokenizer.js, find -name tokenizer.js, rg -g tokenizer.js and
Grep glob: "tokenizer.js" credit tokenizer.js, although each search
could only find the target itself.
- An Edit of "// note<U+2028>foo(1);" to "// note<U+2028>foo(2);" passes
as comment-only, although JavaScript ends the comment at U+2028 and
foo(2) runs.
Scenario 21-bypass-filter-forms adds ten must-deny steps and four
controls. Four hook controls that asserted exact-name lookups still
credit (find -name widget.py, Grep glob widget.py, Get-ChildItem
-Filter widget.py) now assert the lookup is denied; each keeps its
original purpose, that the exclusion parser leaves a positive include
alone, through a wildcard include (widget.*).
Replace JSON-interpolated child JavaScript with fixed preload and summarizer programs. Pass paths and dates through dedicated fixture environment variables; use a fixed quoted exec wrapper so configured executable and script paths remain data. Preserve real Date.now, deadlines, calendar rollover, captured resume metadata and no-next-day duplicate assertions. Actual13/0 Node18.20.8, Node20.20.2 with Bun setting and Node24.9.0; actual13/0 with spaces/apostrophes/literal dollar in fixture paths; next-day negative control12/1; lint and diff checks pass. Addresses CodeQL393/394/395 without suppression or dismissal.
The Stop hook captures its session filename date before waiting for a slow summary, while the fixture previously recomputed it after timeout. Advance only the fixture calendar when the fake summarizer starts and assert persistence under the captured date, matching resume metadata, and no next-day duplicate. Keep Date.now, child delay and lifecycle deadlines unchanged. Actual workflow baseline and corrected fixture13/0; mismatched-day lookup negative control12/1; Node20 with Bun setting and Node18.20.8 both13/0; lint and diff checks pass.
Isolate spawned Stop hooks through TMPDIR/TMP/TEMP, open transcript fixtures exclusively with private modes, and derive intentional stale-race fixture identity from its opened descriptor.
Isolate deadline fixtures from inherited dry-run/disabled hook flags. Remove an unapproved emoji from the new routing regression to satisfy Unicode validation.