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.
Honor ECC_DRY_RUN=1 in install and repair arg parsing (previously only --dry-run flag worked, so ecc --dry-run install/repair silently performed real writes). Make writeInstallState atomic via writeFileAtomic (O_EXCL staging, fsync, cleanup on failure, atomic rename preserving previous state). Add writeInstallState failure/concurrency/symlink regression tests and disposable-home CLI dry-run regression tests for both global and subcommand forms.
Validate env -S and --split-string escape syntax before shell scanning or
opaque fallback. Preserve literal single-quoted data and charge each
validation character against the shared input-proportional work budget.
Verified 787 focused cases and 290 real Git/config, harmless native env
and registered-routing controls. Keeps the prior paid-copy protection.
Co-authored-by: Nguyen Thanh Dat <ntdat812.dev@gmail.com>
Co-authored-by: Davemaina1 <dmain7015@gmail.com>
Carry forward bounded literal include/includeIf configuration capabilities
and env -S/--split-string words from PRs #2858 and #2897. Charge array
copy and expansion work before allocation to preserve the shared quota.
Verified 731 hook cases and 60 real Git/config and registered-route
controls. Dynamic remainder and original full source PRs remain held.
Co-authored-by: Nguyen Thanh Dat <ntdat812.dev@gmail.com>
Co-authored-by: Davemaina1 <dmain7015@gmail.com>
Resolve six conflicts:
- hooks/hooks.metadata.json: keep this branch's gateguard description and
fingerprint, add upstream's hookify-runtime entries.
- package.json: keep both gateguard-report.js and github-coordination.js.
- scripts/hooks/gateguard-fact-force.js, tests/hooks/gateguard-fact-force.test.js,
skills/gateguard/SKILL.md: upstream added a simpler session denial cap; this
branch already has the fuller one (cap threaded through
markCheckedAndCountDenial, malformed-value warning, miss and target notes),
so keep it. Upstream's taskset and su wrapper handling, fail-closed
recursion limit and soft-hyphen sanitising merged cleanly and their tests pass.
- manifests/context-packs/skill-triggers@1.json: take upstream's file and
recompute registryDigest for the merged registry.
Treat non-2xx activity responses as feed errors, preserve non-Markdown Copilot agent assets, and apply the reviewed reference escape parity correction with regenerated bindings. Allow the test runner to drain piped output on its existing success or failure exit status. Validate installed workflows and bounded real Node20/24 pipe regressions.
Match the Windows missing volume-serial exception while retaining strict reported-device, inode, ownership, permission, link and snapshot checks. Add loader warning/block and race regression controls with isolated platform metadata injection.
The entries file was written in place with writeExclusive. A build that
stopped mid-write left partial bytes under the digest name, and every
later build refused them ("content changed under its digest"), so the
SessionStart builder could never restore the index.
Write it through the store's atomicJson (temporary file, then rename), so
a digest-named file only ever holds complete bytes, and replace an
existing file whose bytes do not hash to its name instead of trusting it.
Open with supported no-follow and nonblocking flags before relying on file metadata. Validate descriptor, pathname and rule-directory consistency before and after reading at most64KiB plus one overflow byte. Reject observed replacement, mutation, premature EOF and non-regular files, and close descriptors on every outcome.
Add12 race and I/O regressions. Keep the Hermes uppercase protected-file fixture real on case-sensitive filesystems and verify that the installed guard still permits first-time creation.
Validation: full6688 reported tests across347 files,12 race tests, full80% coverage gates, scoped Hookify coverage, installed Hookify and Hermes workflows, packed artifact lifecycle, lint, signatures and IOC.
- Call patterns now skip a require/import preceded by a dot, with or
without whitespace or optional chaining, so records.require('./label')
and obj?.import('./x') are method calls rather than module loads.
- Replace the one-level dynamic-argument pattern with a balanced-paren
scan over the string-free skeleton, so require(path.join(__dirname,
getName())) and require(a(b(c()))) are reported as dynamic.
Rescanning all 802 repository JavaScript files against the previous head
changes 6 results, each a newly reported dynamic argument nested deeper
than one level (docker eval checks and one test); no specifier or package
result changes. The real registry is unchanged: 35/35 resolved, zero
failures or warnings.
- An extensionless require now tries only the path, .js, .json, and a
directory's index.js/index.json, as Node does; .cjs and .mjs must be
named, so require('./helper') no longer passes on helper.cjs alone.
- Treat `}` as a statement boundary, so a regex after a block is read as
a regex and cannot swallow a later require.
- Decide whether a paren opens a condition from the token before the
keyword, ignoring whitespace, so `obj.\nif(x) / 2` and
`obj?. while(x) / 2` stay method calls followed by division.
- Record which slots came from template literals and skip them for
static `from` and side-effect `import`, which accept only a quoted
string; a tagged template named `from` is no longer an import.
A template passed to import() is still a dependency.
Rescanning all 802 repository JavaScript files against dd14493e changes
no result, and the real registry still reports 35/35 resolved with zero
failures or warnings.
Follow up PR #3187 at 38b8c07f while preserving the original contribution history and all current-main helper bodies. Remove both CI safety-suite exclusions, clean stale imports and the warning glyph, retain shipped test/coverage entrypoints, and keep unknown test totals honest. The tree matches the fully tested 6246-pass/325-file tree; disposable Claude/OpenCode lifecycle passes.
- Track whether each relative specifier arrives through require or
through import()/static import, and resolve the import forms by exact
path. Node's ESM resolver adds no extension and rejects directory
imports even from a CommonJS file, so import('./helper') and
import('./data') no longer pass on helper.js or data/index.json alone.
require keeps the CommonJS extension and index candidates.
- Stop normalizing backslashes in relative specifiers. Linux and macOS
read them as filename characters, so require('./lib\helper.js') now
fails as non-portable-separator instead of resolving to lib/helper.js.
- Record whether each closing paren ends an if/while/for/with condition,
so a `/` right after one starts a regex. A method named `if` and calls
nested inside a condition still read as division.
extractReferences gains an `imports` list. Rescanning all 802 repository
JavaScript files against the previous scanner changes no result, and the
real registry still reports 35/35 resolved with zero failures.
Carry forward PR #3055 at 92f53c4c onto ef648e01, preserving contributor attribution and current main. Focused maintainer fixes reuse shared input ceilings, preserve late root context through outer bootstrap, bound frontmatter before immutable parsing, and assert real oversized input in the limits workflow.
plugin-hook-bootstrap (shell mode) and observe-runner probed a bare
`bash.exe`. On Windows that resolves to the first match on PATH, and
%SystemRoot%\System32 normally sits ahead of Git's bin directory, so the
pick was System32\bash.exe: the WSL launcher. The existing guard (require
a clean probe exit) only catches it when no distro is installed. With one
installed, the probe passes inside Linux, then the hook script is handed
over by a Windows path (C:\...) or a Git Bash path (/c/...), neither of
which exists in WSL. The hook exits 127 instead of running, and
observe-runner's continuous-learning observation fails on every call.
Resolve bash candidates on Windows to absolute paths from PATH, skipping
bash.exe in System32, SysWOW64, Sysnative, and the WindowsApps alias
directory, so a native bash later on PATH (Git's) is used. With none
available, the existing "no bash binary found" skip applies. An explicit
BASH override still wins, and other platforms keep bare names.
The shared resolver lives in scripts/lib/windows-bash.js with a
platform-independent test. A Windows integration test puts System32 ahead
of Git's bash and requires the .sh hook to actually run. Upstream's
bootstrap fails that scenario with exit 127 ("/bin/bash: C:Users...hook.sh:
No such file or directory"); this one prints the hook output.
Carry forward the final scoped delta of PR #3119 at 60e82f95. Keep required dependency closure and select only modules reachable from successful requests; refresh the unchanged 293-skill registry binding.
Carry forward PR #3115ef475c79 and 780dfb78 onto current main. Preserve the newer hook-consent transforms during install and repair. Main already includes the source import fix from #3167.