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.
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.
tmux also separates commands at a `;` that ends an argument, so `new-session -d 'npm run dev;' run-shell 'npm run dev'` runs the second dev server without a pane. Split there as well. An escaped `\;` at the end of an argument stays literal, as in tmux.
Two forms got the pane exemption although they run the dev server without a pane.
`tmux -vL new-session run-shell ...`: in a cluster, the last option that takes a value (f, L, S, T) takes the next token, here the socket name `new-session`. A `c` anywhere in a cluster (`-c'npm run dev'`) runs a shell command without a pane.
`tmux new-session -d \; run-shell "npm run dev"`: only the first command of a sequence was checked. The sequence is now split at `;`, and it is exempt only when every command in it that runs the dev server is a pane command. This also closes the same hole for the plain `tmux new-session ... \; run-shell`, which the old regex exempted.
The block allowed a dev server only when the segment began with `tmux new`, `new-session`, `new-window` or `split-window`. It therefore blocked dev servers that do start in a tmux pane, the way its own message asks: `tmux -L work new-session ...` and `tmux -f conf new-session ...` (tmux options before the command), `/opt/homebrew/bin/tmux new-session ...` and `env TERM=... tmux new-session ...`, the aliases `neww` and `splitw`, and `tmux send-keys -t dev 'npm run dev' Enter` or `respawn-pane` for a session that exists.
Read the tmux command from the parsed segment instead, after the leading command word and tmux's own options, and allow the commands that run their shell command in a pane. `tmux run-shell` and `tmux -c` run it without one and stay blocked.
heredocDelimiter, executionRegion and scanShell each carried one long character loop with every quote, escape, heredoc, case and scope rule inline, which is where the three complexity warnings came from. The delimiter reader now has skipDelimiterBlanks, delimiterEscape and readDelimiterWord; the $(...) walker dispatches to regionQuotedEnd, regionSubstitution, openRegionQuote, regionHeredocOperator, regionHeredocBodies, casePhaseStep and closeRegionParen; and the top-level scanner keeps its position and pending word on a scan object that scanQuotedChar, scanEscape, scanSubstitution, openQuote, scanOperator and scanRedirect advance.
Every budget.spend call keeps its amount and order, so the work quota tests still land in the same window. The module still has no requires and exports createBudget, scanShell and scanExpansions unchanged; a differential run of the old and new scanner over the hook test corpus plus 58k generated inputs produced identical commands, nested regions and spend totals.
extractSessionSummary did user-message filtering and two kinds of tool_use collection in one loop body six blocks deep. The per-entry work now lives in userMessageText, recordToolUse and collectEntry. main() is down to the sequence of steps: resolveTranscriptPath, sessionShortId, maybeGenerateLlmSummary, then updateSessionFile or createSessionFile, with the shared footer and empty-session template as constants.
Call order, log lines, file names and the written markdown are unchanged, including the exact summary block replacement and the migration path for files without markers.
gate() handled Edit, MultiEdit and shell in one 220-line function. It now builds a small context and dispatches to gateSingleEdit, gateMultiEdit and gateShell, with the first-touch, sibling-collapse and routine/destructive shell steps as their own functions. isDestructiveGit and isReadOnlyGitIntrospection became subcommand tables; unwrapLeadWrappers got one handler per wrapper; ddLauncherCommandIndex, isDestructiveFindExec and sanitizePath lost their inline option and code point ladders. condensedGateMsg takes an options object instead of seven positional parameters.
Decision order, recorded metrics reasons, state keys and every denial message are unchanged, and the module exports are the same.
checkGitWords, executableWords, shellRole, updateShellState and checkCommand were each well over the complexity budget and checkCommand nested seven blocks deep. Split them into named helpers: git global option scanning and bypass flag search, per-wrapper handlers for command/exec/env/sudo, a shell option cluster classifier, declaration flag parsing, and a per-task/per-state/per-environment pipeline inside checkCommand.
Every budget.spend call keeps its amount and order so the work quota tests still land in the same window. The module still only requires ./lib/shell-scan and exports run unchanged. Wrapper option sets moved to module constants instead of being rebuilt on every call.
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.
- questionIdsFor and condensedHintFor take an optional
{ externalContract: true }; without it a code change that handles data
asks data-schema only, and an unknown profile gets the original four
questions and condensed hint.
- The hook reads GATEGUARD_EXTERNAL_CONTRACT_QUESTION with the same
spellings as GATEGUARD_METRICS and passes it to the full denial, the
condensed denial and the metrics record, so all three agree.
- The effectiveness harness's arm patch forwards the option, so an arm
never drops it silently.
Docs: change-profile.md gains an External-contract question section and
qualifies "byte-identical to the fixed four questions"; design-notes.md,
SKILL.md (switch table, data rule, word-for-word set) and CHANGELOG say
the fifth question is opt-in; question-effectiveness.md and the harness
README note that the runner clears GATEGUARD_* variables, so a gate arm
from a current tree measures the gate without the question.
skill-triggers@1.json: registryDigest refreshed for the SKILL.md body
change; the description and triggers are unchanged.
- Read file-type filters as include and exclusion globs: rg -t/--type
and the Grep tool's type include ripgrep 14.1's globs for the type,
rg -T/--type-not exclude them, fd -e/--extension includes *.<ext>.
An unknown type name, --type-add or --type-clear is an unreadable
filter and never credits, like --exclude-from.
- Treat a path pattern as a filter: the Glob tool's pattern must match
the target below its root; a shell glob operand must match the target
or a directory above it; a git pathspec matches the same way with its
* also matching /. One operand that admits the target is enough.
- Extend "a search of the target itself gives nothing" to searches
confined to the target's own file name: when every include, the Glob
pattern, or every path pattern ends in that exact name, the stem never
credits, and the closest-miss note says it was a lookup.
- Tokenize PowerShell searches with backslashes kept as path separators,
as the read-only shell check already does; the old literal-prefix scope
hid that src\core\*.ts reached the parser as srccore*.ts.
- Make an edit non-trivial when either text holds U+2028, U+2029 or
U+0085, which end a JavaScript or C# line comment; the lexer splits
lines only at \n and \r.
Extracting applyLongFilter, grepDetail and stemCredits leaves every
touched function at or below its complexity on main.
Isolate deadline fixtures from inherited dry-run/disabled hook flags. Remove an unapproved emoji from the new routing regression to satisfy Unicode validation.
Greptile P1: sedScriptParts stopped at -- and discarded the remaining
operands, so 'sed -- <script>' skipped script collection entirely. GNU sed
4.9 (verified): with no -e/--expression anywhere, the first operand after --
is the script and an s///e in it still executes.
The operands after -- are now collected as positionals, so the existing
first-positional-becomes-script rule applies. Operands after the script stay
file names (data). 2 regression tests pin both sides. Suite: 812/812.
Greptile P1 findings (both verified by running GNU sed against the hook):
1. Escaped replacements evade the check: GNU sed decodes \xHH, \oOOO, \dNNN,
case operations and unknown escapes like \g before executing an s///e
replacement or e operand. Scanning the raw text let \x67it decode to git.
Decoding that surface faithfully is out of scope, so any backslash in an
exec-bearing fragment now throws UnsupportedSedEscape and the hook blocks
with an explicit reason — mirroring the existing env -S escape precedent
instead of pretending to interpret sed. Escaped-newline continuations of an
e operand are refused by the same check.
2. Continued commands went unchecked: the e operand reader stopped at every
newline, but GNU sed joins escaped newlines into one operand. Now covered
by the same conservative refusal.
Also decomposes the sed readers into sub-50-line helpers per the repository
function-size rule (AGENTS.md): readSedDelimited, readSedCommand,
readSedExecute, readSedLineOperand, assertSedLiteral.
5 regression tests pin the escape paths and the benign boundary (a backslash
in a non-executing script stays data). Suite: 810/810.
CodeRabbit security review (Major, CWE-693) correctly identified that GNU sed
collects -e/--expression scripts from any argv position: a script option after
positional operands is still executed, so stopping the option walk at the first
positional let an e-flagged script hide behind it.
Verified against GNU sed 4.9 before fixing:
- 'sed -e s/x/y/ - -e s/y/X/e' runs X (the later -e executes)
- with any script option present, every positional is a file operand
sedScriptParts now walks the full argv, collects scripts from all -e/
--expression positions, and only promotes the first positional to the script
when no script option was given. Unknown options after positionals still fall
back to the opaque default.
4 regression tests pin the reported bypass paths (suite: 805/805).
Ported from the #3051 discussion (test intent, not the old scanner):
- A confidently-parsed sed invocation whose script contains no execution
construct is a pure text transformation: the script operand and every file
operand are data. This removes the specific false positives where file names
or non-executing scripts merely containing 'git ...' were blocked.
- The GNU sed `e` command (bare or with operand) and the `s` command with the
`e` flag run text through the shell. Their literal fragments (the s
replacement, the e operand) and any piped/redirected source are scanned as
real shell source, closing the `printf x | sed 's/x/git commit --no-verify/e'`
gap reported on 2026-10-09.
- Anything the literal parser cannot confidently read (addresses it cannot
consume, -f script files, -i suffix ambiguity, unknown options, expansion
residue) falls back to today's opaque default. No behavior changes for
unmodeled launchers.
- shell-scan.js is untouched and remains the canonical scanner.
Tests: 14 focused cases ported from the #3051 litmus set; full hook suite
801/801.