config-protection.js and post-edit-typecheck.js attached process.stdin
listeners at module scope. tests/hooks/hook-input.test.js requires
config-protection.js, so that test never exits while stdin stays open.
Attach the listeners only when the script runs directly
(require.main === module), as stop-format-typecheck.js and quality-gate.js
already do.
post-edit-typecheck.js picked tsc diagnostics with line.includes(candidate),
so editing a root index.ts reported the errors of src/index.ts, and a
Windows relative path (src\index.ts) never matched tsc's forward-slash
output. Compare the file at the start of each diagnostic instead, the same
check diagnosticMatchesFile() does in stop-format-typecheck.js.
Refs #3821
Reject numeric prefixes, fractions and unsafe window integers consistently with summary scheduling while preserving resolver precedence. Cap only the default context reminder at eighty percent of a valid earlier native compaction budget; explicit thresholds, including zero, retain their intentional policy. Document the independent capacity, summary and advisory contracts.
RED controls reproduce malformed-budget summary triggers and the known-1M/native-200k missed reminder. Resolver65, helper47, actualStop18 and actualPreToolUse54 pass on Node18 and24. Parent and independent reviewer approved the exact diff before commit. Registry rebinding remains the integration coordinator responsibility after combining documentation repairs.
Include captured stderr, status, signal and spawn error message when the existing unexpected-monitor-failure warning assertions fail. Preserve all status/raw-input/warning assertions, deadlines and product bytes.
The macOS hosted failure did not print its actual stderr, so its transient cause remains unverified. The unchanged source matches green main; 24 prior local diagnostic repeats passed 120 checks. This diagnostic-only patch passes all five fixture cases, affected ESLint and diff checks. Parent independently reviewed the diff. Fresh exact-head hosted CI remains required.
Restore the shipped payload-first contract from #3464/#3495 and the explicit source #3755 scope clarification. Two provider conversations must retain distinct IDs despite an inherited registered ECC parent ID. Normal and ambient producer24 tests, canonical resolver10 and independent differential review pass. Keep native importer guards; explicit provider/native correlation is queued separately.
Verify review discussion4419348312 against actual native producer/importer behavior. Prefer the independently sanitized ECC_SESSION_ID for the ECC-owned metrics feed, retain payload then legacy fallback for standalone hooks, and leave sibling hooks unchanged.
Actual Node producer plus read-only cached native binary reproduces zero imported metrics before the fix and exactly one tool/file/log after repeated sessions/status commands. Four producer regression controls and normal/adversarial inherited-env suites pass22/22. Focused coverage96.47percent lines and91.47percent branches; parent independent source review passed.
Separate confirmed model capacity from Stop-summary scheduling. Use the smaller confirmed capacity/native budget, or native-only budget for inferred capacity. Three unit and one actual hook RED cases now pass; helper43, actualStop15 and ten independent controls pass without changing the capacity resolver.
Dropping the previous-character check in toggleQuote let the `'` of `\'` close an ANSI-C `$'...'` string, so `echo $'it\'s'; (rm -rf /tmp/x)` left a quote open and the group was never extracted. Track `$'...'` in the quote state and consume backslash escapes inside it, as bash does, and cover the inner scanners (`(...)`, `$(...)`, `{ ...; }` bodies) with quotes ending in a backslash.
getopt_long accepts a unique prefix, so sudo --use root rm -rf build is
--user root and the resolver read root as the command. Expand unique
prefixes from sudo's long option table before the value/non-exec checks.
--help joins the non-executing modes.
sudo accepts VAR=value between its options (parse_args.c is_envar), so
sudo FOO=1 -u root rm -rf resolved -u as the command. Keep reading options
past assignments. Add sudo -R/--chroot, -a/--auth-type, -c/--login-class
and doas -a as value flags, and stop a short-cluster scan at any unknown
letter, so sudo -R/live rm -rf no longer reads the l in /live as -l.
doas -C only checks config, so it is non-executing; the existing doas -C dd
case moves to the passive list.
sudo -l/--list, -v, -K, -V and -e/--edit (and doas -L) do not run the
command operand, so sudo -l rm -rf build was denied as destructive. The
privilege-wrapper resolver now reports no executable command for those
modes. Short clusters stop at the first value flag, so sudo -ul rm -rf
(user l) stays destructive.
Dict tool I/O is JSON-encoded before scrubbing, so a newline before a bare
GitHub token or AWS key id became a literal \n and the leading \b never
matched. Replace it with a lookbehind that also accepts a preceding JSON
escape (\b \f \n \r \t or \uXXXX).
Capture truncation (5000 chars, 2000 in the parse-error fallback) can cut
a DSN right before its '@', which defeated the (?=@) password lookahead.
Let the password run to end of string, except a bare trailing host:port.
Applied to both matcher copies. Regression tests cover structured output
with tokens after escaped whitespace and DSNs cut before '@' on the
string, structured input/output and fallback paths. Refresh registryDigest.
Signed-off-by: Wen <jwcastillo@gmail.com>
The observation scrubber in observe.sh only redacted a value placed
directly after a bare keyword (token=, secret:, ...). These reached
observations.jsonl in plain text:
- AWS_SECRET_ACCESS_KEY=... / aws_secret_access_key = ... / SECRET_KEY=
- scheme://user:password@host DSN passwords
- PEM private key blocks
- ghp_/gho_/ghu_/ghs_/ghr_ and github_pat_ tokens, AKIA/ASIA key ids
Both identical _SECRET_RE copies (parse-error fallback and main path)
now accept an optional _access_key/_key suffix on the keyword, and a
new _SECRET_VALUE_RE redacts the value-shaped secrets above. Every
branch is anchored on a literal prefix with bounded classes, keeping
the linear-time constraint from #2278.
The existing redaction test gains canaries for each case on both the
input and output paths, plus a negative case that ordinary code,
credential-free URLs and SSH remotes are left untouched.
registryDigest in skill-triggers@1.json is refreshed, which
validate-context-profiles requires after any skills/ change.
Signed-off-by: Wen <jwcastillo@gmail.com>
isDestructiveRm/isDestructiveGit keyed on argv[0], so sudo rm -rf,
command rm -rf and env git reset --hard read as calls to the wrapper and
were allowed once the routine shell gate was satisfied. Run both checks on
the unwrapLeadWrappers argv too, the resolution the dd and sh -c paths
already use. Overlaps #2832, which also reworks sh -c parsing and has
conflicts with main; this is the minimal rm/Git part.
Fixes#3762. pre:bash:gateguard-fact-force was missing from
FAIL_CLOSED_ON_TRUNCATION_HOOKS, so a Bash payload over the stdin cap
exited 0 while the Edit and PowerShell GateGuard ids blocked. Share one
GateGuard id set for the fail-closed list and the off-switch branch, and
add runner-level tests for all three ids plus each off switch.
Fixes#3761. config-protection only read tool_input.file_path, so a MultiEdit
with the config path inside edits[] edited an existing ruff.toml/.eslintrc
without the block. Check every effective target and block if any is an
existing protected config.
Integrates #3697 on current main. Prefer the observer project ID, retain a sole legacy ID until the new observer folder exists, and release the ending session lease across same-root aliases. Replace the unsafe live merge hint with read-only project inspection, and validate alias IDs before path use. #3711 independently identified the computed-ID selection gap; its early switch without observer data and unchanged SessionEnd path were not adopted.
Port #3656 onto current main, preserving both the contributor fix and the unrelated current changelog entry. Includes the follow-up glob normalization from 2c11fd74.
The held-open subprocess cases inherited ECC_HOOKS_ENABLED and
ECC_DISABLED_HOOKS from the caller, so disabling block-no-verify or all
hooks made them fail even though the dispatcher behaved correctly. Pin
both flags in the spawned environment.
Refs #3665
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The held-open subprocess cases only sent partial JSON. Add a complete
`git commit --no-verify` request with stdin left open and assert the
dispatcher reaches block-no-verify instead of reporting incomplete input.
Refs #3665
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- bash-hook-dispatcher: honor the reader's truncated flag. pre-Bash
blocks (exit 2) because safety checks need the complete request;
post-Bash skips its hooks and does not echo a partial payload.
- resolveStdinIdleTimeout: treat whitespace-only values as unset instead
of Number(' ') === 0 silently disabling the deadline.
- isCompleteJsonObject: do not accept a JSON array as a hook payload.
- tests: wait for the child's close event so stdio is drained before
asserting; cover both bash-hook-dispatcher modes.
- hooks/README.md: keep the example below the 5 s per-hook timeout.
Refs #3665
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setTimeout fires after 1 ms for any delay above 2147483647 ms, so a large
ECC_HOOK_INPUT_IDLE_TIMEOUT_MS turned the deadline into ~1 ms and marked
every hook input incomplete. Clamp to the maximum with a diagnostic, and
surface the invalid-value diagnostic when readStdinRaw resolves the env.
Refs #3665
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
readStdinRaw() only settled on end, error or close. If the parent kept
stdin open without writing EOF, every hook that reads through it waited
until the harness killed it (an 8-10 s stall per call).
Add an idle deadline (default 3000 ms, ECC_HOOK_INPUT_IDLE_TIMEOUT_MS,
0 disables). It is reset on each chunk, unref()ed, and on expiry detaches
from and pauses the stream so a held-open pipe cannot keep the hook alive.
Input that already parses as a complete JSON object is accepted; anything
else is marked truncated so fail-closed safety hooks still block.
bash-hook-dispatcher.js now reads through the shared reader instead of its
own end/error-only loop.
Fixes#3665
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prettier's config searcher also reads .prettierrc.json5, .prettierrc.ts/.mts/.cts and prettier.config.ts/.mts/.cts. resolve-formatter knew none of them, so a project configured only through prettier.config.ts was never formatted by post-edit-format, quality-gate or the Stop batch. config-protection's rc pattern stopped at .yaml/.toml, so an existing .prettierrc.ts or .prettierrc.json5 could be edited freely.
The Stop typecheck reported every tsc line that contained the edited path anywhere. A root index.ts has the relative path `index.ts`, which every `src/index.ts(...)` diagnostic contains, so a clean root file was reported with the errors of src/index.ts. The same happened to src/utils.ts with web/src/utils.ts, and to any file named in another file's message.
Compare the path that starts the diagnostic (`path(line,col): `), keeping the slash normalization for Windows.
With diff.relative=true, `git diff --name-only` reports paths relative to the working directory, so joining them to the repository root pointed at files that do not exist, and a session in a subdirectory again got no console.log warning. getGitModifiedFiles now runs `git -c diff.relative=false diff --name-only HEAD`. The -c form also works on Git versions older than --no-relative.
readToken dropped every backslash inside double quotes, while bash drops it only before $, `, ", \\ or a newline. `"echo ready\;"` therefore reached the tmux check as `echo ready;`, read as a command separator, and `tmux send-keys -t dev "echo ready\;" "npm run dev" Enter` was blocked although it types into a pane.
check-console-log took the paths from `git diff --name-only HEAD`, which are relative to the repository root, and checked them with fs.existsSync from the hook's working directory. In a session started in a subdirectory, such as a monorepo package, no path existed from there, so the hook never warned. Resolve each path from the repository root. The exclusion patterns and the warning still use the root-relative path.