/** * Tests for scripts/hooks/block-no-verify.js via run-with-flags.js */ const assert = require('assert'); const path = require('path'); const fs = require('fs'); const os = require('os'); const vm = require('vm'); const hook = require('../../scripts/hooks/block-no-verify'); const { spawnSync } = require('child_process'); const runner = path.join(__dirname, '..', '..', 'scripts', 'hooks', 'run-with-flags.js'); function test(name, fn) { try { fn(); console.log(` \u2713 ${name}`); return true; } catch (error) { console.log(` \u2717 ${name}`); console.log(` Error: ${error.message}`); return false; } } function runHook(input) { const rawInput = typeof input === 'string' ? input : JSON.stringify(input); const result = hook.run(rawInput); return { code: result.exitCode, stdout: result.stdout || '', stderr: result.stderr || '' }; } let passed = 0; let failed = 0; console.log('\nblock-no-verify hook tests'); console.log('─'.repeat(50)); // --- Basic allow/block --- if (test('allows plain git commit', () => { const r = runHook({ tool_input: { command: 'git commit -m "hello"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('blocks --no-verify on git commit', () => { const r = runHook({ tool_input: { command: 'git commit --no-verify -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); assert.ok(r.stderr.includes('BLOCKED'), `stderr should contain BLOCKED: ${r.stderr}`); })) passed++; else failed++; if (test('blocks -n shorthand on git commit', () => { const r = runHook({ tool_input: { command: 'git commit -n -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); assert.ok(r.stderr.includes('BLOCKED'), `stderr should contain BLOCKED: ${r.stderr}`); })) passed++; else failed++; if (test('blocks core.hooksPath override', () => { const r = runHook({ tool_input: { command: 'git -c core.hooksPath=/dev/null commit -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); assert.ok(r.stderr.includes('core.hooksPath'), `stderr should mention core.hooksPath: ${r.stderr}`); })) passed++; else failed++; if (test('blocks quoted core.hooksPath override argument', () => { const r = runHook({ tool_input: { command: 'git -c "core.hooksPath=/dev/null" commit -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); assert.ok(r.stderr.includes('core.hooksPath'), `stderr should mention core.hooksPath: ${r.stderr}`); })) passed++; else failed++; // --- Chained command false positive prevention (Comment 2) --- if (test('does not false-positive on -n belonging to git log in a chain', () => { const r = runHook({ tool_input: { command: 'git log -n 10 && git commit -m "msg"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('does not false-positive on --no-verify in a prior non-git command', () => { const r = runHook({ tool_input: { command: 'echo --no-verify && git commit -m "msg"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('allows --no-verify discussed in a double-quoted commit message', () => { const r = runHook({ tool_input: { command: 'git commit -m "fix: --no-verify edge case"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('allows --no-verify discussed in a single-quoted commit message', () => { const r = runHook({ tool_input: { command: "git commit -m 'fix: --no-verify edge case'" } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('allows -n discussed in a quoted commit message', () => { const r = runHook({ tool_input: { command: 'git commit -m "Fixed -n bug in module"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('allows --no-verify after combined -am message option', () => { const r = runHook({ tool_input: { command: 'git commit -am "--no-verify"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('allows -n after combined -am message option', () => { const r = runHook({ tool_input: { command: 'git commit -am "-n"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; // --- Short options cluster, so -n need not lead --- if (test('blocks -n clustered after -a', () => { const r = runHook({ tool_input: { command: 'git commit -an -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); })) passed++; else failed++; if (test('blocks -n clustered after -s', () => { const r = runHook({ tool_input: { command: 'git commit -sn -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); })) passed++; else failed++; if (test('blocks -n clustered after -v', () => { const r = runHook({ tool_input: { command: 'git commit -vn -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); })) passed++; else failed++; if (test('allows -mn, where n is the inline message and not a flag', () => { const r = runHook({ tool_input: { command: 'git commit -mn' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('allows core.hooksPath discussed in a quoted commit message', () => { const r = runHook({ tool_input: { command: 'git commit -m "doc: explain core.hooksPath= setting"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('allows git bypass phrase discussed in a quoted commit message', () => { const r = runHook({ tool_input: { command: 'git commit -m "doc: explain git push --no-verify risk"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('still blocks --no-verify on the git commit part of a chain', () => { const r = runHook({ tool_input: { command: 'git log -n 5 && git commit --no-verify -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); })) passed++; else failed++; if (test('still blocks a real quoted --no-verify flag', () => { const r = runHook({ tool_input: { command: 'git commit "--no-verify" -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); assert.ok(r.stderr.includes('BLOCKED'), `stderr should contain BLOCKED: ${r.stderr}`); })) passed++; else failed++; if (test('still blocks bypass flags in later chained git commands', () => { const r = runHook({ tool_input: { command: 'git commit -m "msg" && git push --no-verify' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); assert.ok(r.stderr.includes('git push'), `stderr should mention git push: ${r.stderr}`); })) passed++; else failed++; // --- Subcommand detection (Comment 4) --- if (test('does not misclassify "commit" as subcommand when it is an argument to push', () => { // "git push origin commit" — "commit" is a refspec arg, not the subcommand const r = runHook({ tool_input: { command: 'git push origin commit' } }); // This should detect "push" as the subcommand, not "commit" // Either way it should not block since there's no --no-verify assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; // --- Blocks on push --no-verify --- if (test('blocks --no-verify on git push', () => { const r = runHook({ tool_input: { command: 'git push --no-verify' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); assert.ok(r.stderr.includes('git push'), `stderr should mention git push: ${r.stderr}`); })) passed++; else failed++; // --- Non-git commands pass through --- if (test('allows non-git commands', () => { const r = runHook({ tool_input: { command: 'npm test' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; // --- Plain text input (not JSON) --- if (test('handles plain text input', () => { const r = runHook('git commit -m "hello"'); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('blocks plain text input with --no-verify', () => { const r = runHook('git commit --no-verify -m "msg"'); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); })) passed++; else failed++; // --- Case-insensitivity of git config keys + -t template short option --- if (test('blocks case-variant core.hooksPath (lowercase)', () => { const r = runHook({ tool_input: { command: 'git -c core.hookspath=/dev/null commit -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); assert.ok(/core\.hookspath/i.test(r.stderr), `stderr should mention core.hooksPath: ${r.stderr}`); })) passed++; else failed++; if (test('blocks case-variant core.hooksPath (uppercase)', () => { const r = runHook({ tool_input: { command: 'git -c core.HOOKSPATH=/dev/null commit -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); })) passed++; else failed++; if (test('still allows -tn (n is the -t template path, not a flag)', () => { const r = runHook({ tool_input: { command: 'git commit -tn -m "msg"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; // --- Quoted/heredoc candidates: preserve blocking, prevent flag leakage --- const executingPayloads = [ // Quoted heredoc delimiter disables shell expansion but Python still consumes code; unsupported interpreter language remains conservative on a literal bypass phrase. ['hyphenated Python heredoc delimiter', 'python3 - <<\'PY-SCRIPT\'\nprint("git commit -n")\nPY-SCRIPT\nbash -n x.sh'], ['block double-quoted git executable', '"git" commit -n -m x'], ['block single-quoted git executable', "'git' commit -n -m x"], ['block git executable assembled with empty single quotes', "g''it commit -n -m x"], ['block git executable assembled with empty double quotes', 'g""it commit --no-verify -m x'], ['block git executable assembled from quoted prefix', "'g'it commit -n -m x"], ['block git executable assembled from quoted middle', "g'i't commit -n -m x"], ['block git executable assembled with an escape', 'g\\it commit -n -m x'], ['block double-quoted git plus exe suffix', '"git".exe commit -n -m x'], ['block single-quoted git plus exe suffix', "'git'.exe commit -n -m x"], ['block hooksPath after double-quoted git plus exe suffix', '"git".exe -c core.hooksPath=/tmp/no commit -m x'], ['block hooksPath after single-quoted git plus exe suffix', "'git'.exe -c core.hooksPath=/tmp/no commit -m x"], ['block double-quoted git with quote-assembled exe suffix', '"git".e""xe commit -n -m x'], ['block single-quoted git with quote-assembled exe suffix', "'git'.e''xe commit -n -m x"], ['block quoted git with escaped exe suffix', '"git".\\exe commit -n -m x'], ['block hooksPath after quote-assembled exe suffix', '"git".e""xe -c core.hooksPath=/tmp/no commit -m x'], ['quoted hash does not hide a later commit bypass', 'echo "#"; git commit -n -m x'], ['hash text in quotes does not hide a later commit bypass', 'echo "not # a comment" && git commit --no-verify -m x'], ['word-internal hash does not hide a later commit bypass', 'echo foo#bar; git commit -n -m x'], ['word-internal hash does not hide a later push bypass', 'printf %s foo#bar && git push --no-verify'], ['pipe echo data to bash', "echo 'git commit -n -m x' | bash"], ['pipe printf data to sh', "printf '%s\\n' 'git commit --no-verify -m x' | sh"], ['execute data through xargs and bash -c', "printf '%s\\n' 'git commit -n -m x' | xargs -I CMD bash -c CMD"], ['execute command substitution text through bash', "echo '$(git commit -n -m x)' | bash"], ['execute bash here-string', "bash <<< 'git commit -n -m x'"], ['execute sh here-string', "sh -s <<< 'git commit --no-verify -m x'"], ['block backtick command substitution', 'echo "`git commit -n -m x`"'], ['block substitution after quoted parenthesis', 'echo "$(printf \')\'; git commit -n -m x)"'], ['block substitution after case parenthesis', 'echo "$(case x in x) :;; esac; git commit -n -m x)"'], ['block bash --noprofile -c', "bash --noprofile -c 'git commit -n -m x'"], ['block bash -O extglob -c', "bash -O extglob -c 'git commit -n -m x'"], ['block bash -o pipefail -c', "bash -o pipefail -c 'git commit -n -m x'"], ['block bash -c after option terminator', "bash -c -- 'git commit -n -m x'"], ['block sh -c after option terminator', "sh -c -- 'git commit --no-verify -m x'"], ['block heredoc piped to bash', 'cat < { const r = runHook({ tool_input: { command } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; } const nonLeakingPayloads = [ // Inside double quotes a backslash before dot remains literal, so decoded executable is git\.exe, not git.exe. ['allows the distinct executable with a literal escaped dot', '"git""\\.exe" commit -n -m x'], // A complete echo operand is data; accepted role repair intentionally corrects the authored broad-blocking expectation. ['allows quoted echo data (corrected author expectation)', 'echo "git commit -n"'], ['python heredoc string with later bash -n', 'python3 - <<\'PY\'\nold="git add -A\\nif ! git diff --cached --quiet; then\\n git commit -q -m \\"vault sync"\nPY\nbash -n vault-sync.sh'], ['assignment string with later bash -n', 'old="git commit -q -m x"; bash -n x.sh'], ['plain commit followed by later-line bash -n', 'git commit -m x\nbash -n s.sh'], ['plain commit followed by grep -n', 'git commit -m x; grep -n foo f.txt'], ['JSON string followed by sed -n', 'printf \'%s\' \'{"cmd":"git commit -q -m \\"x\\""}\' | node x.js; sed -n 1p f'], ['non-shell heredoc after bash argument', "bash -c 'cat' < { const r = runHook({ tool_input: { command } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; } // --- Optional stuck values (-u, -S) and long-option prefixes --- if (test('allows -uno (n is the -u untracked-files mode, not a flag)', () => { const r = runHook({ tool_input: { command: 'git commit -uno -m "msg"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('allows -Sn (n is the -S key id, not a flag)', () => { const r = runHook({ tool_input: { command: 'git commit -Sn -m "msg"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; if (test('still blocks -nu (n comes before the optional-value flag)', () => { const r = runHook({ tool_input: { command: 'git commit -nu -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); })) passed++; else failed++; if (test('blocks --no-veri (git accepts unambiguous long-option prefixes)', () => { const r = runHook({ tool_input: { command: 'git commit --no-veri -m "msg"' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); })) passed++; else failed++; if (test('blocks --no-verif on git push', () => { const r = runHook({ tool_input: { command: 'git push --no-verif origin main' } }); assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`); })) passed++; else failed++; if (test('allows --no-verbose (not a prefix of --no-verify)', () => { const r = runHook({ tool_input: { command: 'git commit --no-verbose -m "msg"' } }); assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`); })) passed++; else failed++; // Finite literal role regressions: supplied command strings are never executed. for (const command of [ "git commit -m \"$(git push --no-verify)\"", "git commit -m \"$(git -c core.hooksPath=/dev/null push)\"", "git commit --message=\"$(git push --no-veri)\"", "git commit -m \"`git push --no-verify`\"", "echo '#'; git push --no-verify", "'bash' -c 'git push --no-verify'", "printf '%s' 'git commit --no-verify'; git push --no-verify", "printf '%s' 'git push --no-verify' | sh", "printf '%s' 'git push --no-verify' | sudo -u root bash", "git commit -m \"$(printf '%s' 'git push --no-verify' | sh)\"", "echo $(echo $(git push --no-verify))", "cat <(git push --no-verify)", "cat < { const result = runHook({ tool_input: { command } }); assert.strictEqual(result.code, 2, result.stderr); assert.deepStrictEqual(runHook({ tool_input: { command } }), result, 'Second call must not inherit lexical state'); })) passed++; else failed++; } for (const command of [ "printf '%s' 'git commit --no-verify'", "printf '%s' eval 'git commit --no-verify'", "echo 'git commit' --no-verify", "bash -c 'echo ok' 'git push --no-verify'", "'bash' -c 'printf %s safe' 'git push --no-verify'", "git commit -m --no-verify", "git commit -Skeyn -m x", "git commit -- --no-verify", "git push '--no-verify;literal'", "git push '--no-verify)literal'", "git commit -m '$(git push --no-verify)'", "printf '%s' '$(git push --no-verify)'", "echo \"\\$(git push --no-verify)\"", "echo \"\\`git push --no-verify\\`\"", "cat <<'EOF'\n$(git push --no-verify)\nEOF", "cat <<\\EOF\ngit push --no-verify\nEOF", "cat < --no-verify", "git commit -m safe 2> --no-verify", "printf '%s' 'git push --no-verify'; git push origin main", "git -C '/tmp/git push --no-verify' status", "echo 'git commit --no-verify' > log", "bash script.sh 'git push --no-verify'" ]) { if (test(`literal allowed: ${JSON.stringify(command)}`, () => { const result = runHook({ tool_input: { command } }); assert.strictEqual(result.code, 0, result.stderr); assert.deepStrictEqual(runHook({ tool_input: { command } }), result, 'Second call must not inherit lexical state'); })) passed++; else failed++; } if (test('bounded wide quoted data remains data', () => { assert.strictEqual(runHook(`printf '%s' ${"'git push --no-verify' ".repeat(2000)}`).code, 0); })) passed++; else failed++; if (test('deep nested substitutions fail closed within the work budget', () => { assert.strictEqual(runHook('echo ' + '$('.repeat(120) + 'git push --no-verify' + ')'.repeat(120)).code, 2); })) passed++; else failed++; for (const command of [ 'echo note{git push --no-verify}', 'printf %s note{git push --no-verify}', 'echo "$(printf %s case)"', "echo 'case x in x) git push --no-verify;; esac'", ]) { if (test(`literal role control: ${JSON.stringify(command)}`, () => { assert.strictEqual(runHook(command).code, 0); })) passed++; else failed++; } if (test('moderate nested execution identifies the actual Git bypass', () => { const result = runHook('echo ' + '$('.repeat(8) + 'git push --no-verify' + ')'.repeat(8)); assert.strictEqual(result.code, 2); assert.match(result.stderr, /git push/); })) passed++; else failed++; if (test('escaped backtick inside substitution does not hide a later command', () => { const result = runHook('echo "`printf %s \\`; git push --no-verify`"'); assert.strictEqual(result.code, 2); assert.match(result.stderr, /git push/); })) passed++; else failed++; // Review regressions: literal option roles and nested execution boundaries. for (const [expected, commands] of [ [2, [ "bash +x -c 'git push --no-verify'", "bash +o posix -c 'git push --no-verify'", "bash +o errexit -c 'git push --no-verify'", "bash +O extglob -c 'git commit -n'", "bash +xo posix -c 'git push --no-verify'", "bash +oO posix extglob -c 'git push --no-verify'", "bash -c +x 'git push --no-verify'", "bash -co posix 'git push --no-verify'", "bash +c 'git push --no-verify'", "bash +x -c -- 'git push --no-verify'", "bash +x -c - 'git push --no-verify'", "bash +unknown -c 'git push --no-verify'", "echo \"$(cat <<'EOF'\n)\nEOF\ngit push --no-verify\n)\"", "echo \"$(cat < { const result = runHook(command); assert.strictEqual(result.code, expected, result.stderr); if (expected === 2) assert.match(result.stderr, /git (push|commit)/, 'The literal bypass, not budget exhaustion, must be identified'); })) passed++; else failed++; } } // Nearby delimiter roles use the same lexer and must not become heredocs. for (const [expected, command] of [ [2, "echo \"$(cat <<\\\n EOF\n)\nEOF\ngit push --no-verify\n)\""], [0, "echo \"$(cat <<\\\n EOF\n)\ngit push --no-verify\nEOF\n)\""], [2, "echo \"$(cat << { const result = runHook(command); assert.strictEqual(result.code, expected, result.stderr); if (expected === 2) assert.match(result.stderr, /git push/); })) passed++; else failed++; } // Private VM instrumentation loads the exact source without changing the host // globals, module cache, production API or executing any supplied command. function countedClassification(command, quota) { const context = vm.createContext({}); vm.runInContext(` globalThis.copiedElements = 0; globalThis.copiedStateEntries = 0; const NativeMap = Map; const NativeSet = Set; globalThis.Map = class extends NativeMap { constructor(entries) { super(); if (entries) for (const [key, value] of entries) { globalThis.copiedStateEntries++; super.set(key, value); } } }; globalThis.Set = class extends NativeSet { constructor(entries) { super(); if (entries) for (const value of entries) { globalThis.copiedStateEntries++; super.add(value); } } }; const originalSlice = Array.prototype.slice; Array.prototype.slice = function(start = 0, end = this.length) { const a = start < 0 ? Math.max(0, this.length + start) : Math.min(this.length, start); const b = end < 0 ? Math.max(0, this.length + end) : Math.min(this.length, end); globalThis.copiedElements += Math.max(0, b - a); return originalSlice.call(this, start, end); }; `, context); const lexer = { exports: {} }; const hookModule = { exports: {} }; const hooks = path.join(__dirname, '../../scripts/hooks'); const load = (file, module, require) => vm.compileFunction(fs.readFileSync(file, 'utf8').replace(/^#![^\n]*\n/, ''), ['module', 'require'], { parsingContext: context, filename: file })(module, require); load(path.join(hooks, 'lib/shell-scan.js'), lexer, () => { throw new Error('Unexpected scanner dependency'); }); let spent = 0; let valueReads = 0; const instrumentedLexer = { ...lexer.exports, createBudget(length) { const budget = lexer.exports.createBudget(length); return { spend(amount = 1) { spent += amount; // Throw in the module's own realm so its fail-closed catch is exercised. if (quota !== undefined && spent > quota) vm.runInContext('throw new RangeError("Test work quota exceeded")', context); budget.spend(amount); } }; }, scanShell(text, budget) { const scan = lexer.exports.scanShell(text, budget); for (const command of scan.commands) { for (const word of command.words) { const value = word.value; Object.defineProperty(word, 'value', { get() { valueReads++; return value; } }); } } return scan; }, }; load(path.join(hooks, 'block-no-verify.js'), hookModule, name => { assert.strictEqual(name, './lib/shell-scan'); return instrumentedLexer; }); const result = hookModule.exports.run(command); return { result, copiedElements: context.copiedElements, copiedStateEntries: context.copiedStateEntries, spent, valueReads }; } for (const n of [64, 128]) { if (test(`opaque Git candidates avoid quadratic suffix copies at ${n}`, () => { const command = 'unknown ' + 'git '.repeat(n); const result = countedClassification(command); assert.strictEqual(result.result.exitCode, 0); assert.ok(result.copiedElements <= 2 * (n + 1), JSON.stringify(result)); assert.ok(result.valueReads <= 12 * (n + 1), JSON.stringify(result)); })) passed++; else failed++; if (test(`repeated global-option traversal spends the shared quota at ${n}`, () => { const result = countedClassification('unknown git ' + '-c git '.repeat(n), 3000); assert.strictEqual(result.result.exitCode, 2, JSON.stringify(result)); assert.match(result.result.stderr, /work budget/); assert.ok(result.spent >= 3000 && result.spent < 3100, JSON.stringify(result)); assert.ok(result.valueReads < 6000, JSON.stringify(result)); })) passed++; else failed++; } // Unquoted heredoc ending delimiters use logical lines; quoted ones do not. for (const quoted of [false, true]) { for (const stripTabs of [false, true]) { for (const nested of [false, true]) { for (const backslashes of [1, 2, 3, 4]) { const delimiter = quoted ? "'EOF'" : 'EOF'; const tab = stripTabs ? '\t' : ''; let command = `cat <<${stripTabs ? '-' : ''}${delimiter}\n${tab}EO${'\\'.repeat(backslashes)}\nF\ngit push --no-verify\n${tab}EOF\n`; if (nested) command = `echo "$( ${command})"`; const expected = !quoted && backslashes === 1 ? 2 : 0; if (test(`heredoc logical ending quoted=${quoted} tabs=${stripTabs} nested=${nested} escapes=${backslashes}`, () => { const result = runHook(command); assert.strictEqual(result.code, expected, result.stderr); if (expected === 2) assert.match(result.stderr, /git push/); })) passed++; else failed++; } } } } for (const [expected, command] of [ [2, 'cat < { const result = runHook(command); assert.strictEqual(result.code, expected, result.stderr); if (expected === 2) assert.match(result.stderr, /git push/); })) passed++; else failed++; } for (const option of ['-oerrexit', '+oerrexit', '-xoerrexit', '+xoerrexit', '-o errexit', '+o errexit', '-coerrexit']) { for (const [expected, code, tail] of [ [2, 'git push --no-verify', ''], [0, 'echo safe', " 'git push --no-verify'"], ]) { const command = `zsh ${option} -c '${code}'${tail}`; if (test(`zsh named option role ${expected}: ${command}`, () => { const result = runHook(command); assert.strictEqual(result.code, expected, result.stderr); if (expected === 2) assert.match(result.stderr, /git push/); })) passed++; else failed++; } } for (const [expected, command] of [ [2, "zsh -coerrexit 'git push --no-verify'"], [0, "zsh -coerrexit 'echo safe' 'git push --no-verify'"], [0, "zsh -oerrexit script.sh 'git push --no-verify'"], [0, "zsh +oerrexit -- script.sh 'git push --no-verify'"], [2, "bash +o errexit -c 'git push --no-verify'"], [0, "bash +o errexit -c 'echo safe' 'git push --no-verify'"], ]) { if (test(`shell-specific option control ${expected}: ${command}`, () => { assert.strictEqual(runHook(command).code, expected); })) passed++; else failed++; } // Named option arity is only modeled for Bash and the scoped zsh o grammar. // For other literal shell names these are opaque, not guessed script operands. for (const shell of ['sh', 'dash', 'ksh']) { for (const option of ['-oerrexit', '+oerrexit', '-o errexit', '+o errexit', '-Oextglob', '+Oextglob', '-O extglob', '+O extglob']) { for (const [expected, payload] of [[2, 'git push --no-verify'], [0, 'echo safe']]) { const command = `${shell} ${option} -c '${payload}'`; if (test(`opaque shell named option ${expected}: ${command}`, () => { const result = runHook(command); assert.strictEqual(result.code, expected, result.stderr); if (expected === 2) assert.match(result.stderr, /git push/); })) passed++; else failed++; } } for (const [expected, tail] of [ [2, "-oerrexit -c 'echo safe' 'git push --no-verify'"], [2, "-O extglob script.sh 'git push --no-verify'"], [0, "-c 'echo safe' 'git push --no-verify'"], [2, "-c 'git push --no-verify'"], [0, "script.sh 'git push --no-verify'"], [2, "-s <<'EOF'\ngit push --no-verify\nEOF"], [0, "-s <<'EOF'\necho safe\nEOF"], ]) { if (test(`opaque versus supported ${shell}: ${JSON.stringify(tail)}`, () => { // The first two are intentionally conservative refusals, including // potentially inert positional data; no execution semantics are claimed. const result = runHook(`${shell} ${tail}`); assert.strictEqual(result.code, expected, result.stderr); if (expected === 2) assert.match(result.stderr, /git push/); })) passed++; else failed++; } } // Review-followup witnesses remain inert strings passed only to the classifier. const reviewFollowupCases = [ [ "transformed executable pipeline", 2, "printf '%s' x | sed 's/x/git push --no-verify/' | bash" ], [ "transformed executable pipeline", 2, "printf '%s' x | sed 's/x/git commit -n/' | sh" ], [ "transformed executable pipeline", 2, "printf '%s' x | sed 's|x|GIT push --no-verify|' | env bash -s" ], [ "transformed executable pipeline", 2, "printf '%s' x | sed 's/x/git push --no-verify/' | tee file | bash" ], [ "transformed executable pipeline", 2, "echo \"$(printf '%s' x | sed 's/x/git push --no-verify/' | bash)\"" ], [ "transformed executable pipeline", 0, "printf '%s' x | sed 's/x/git push --no-verify/'" ], [ "transformed executable pipeline", 0, "printf '%s' x | sed 's/x/git push --no-verify/' | tee file" ], [ "transformed executable pipeline", 0, "printf '%s' x | sed 's/x/echo safe/' | bash" ], [ "transformed executable pipeline", 0, "printf '%s' x | sed 's/x/git status/' | bash" ], [ "transformed executable pipeline", 0, "printf '%s' x | sed 's/x/git push --no-verify/' | bash script.sh" ], [ "transformed executable pipeline", 0, "printf '%s' x | sed 's/x/git push --no-verify/' | bash -c 'echo safe'" ], [ "tee data versus executable sink", 0, "tee file <<'EOF'\ngit push --no-verify\nEOF" ], [ "tee data versus executable sink", 0, "tee -a file < { const result = runHook(command); assert.strictEqual(result.code, expected, result.stderr); if (expected === 2) assert.match(result.stderr, /BLOCKED/); })) passed++; else failed++; } // Literal shell-state propagation; witness text is never executed. Pipeline-last // and conditional state changes are conservative alternatives, not flow proofs. const stickyEnvironmentCases = Object.freeze([ [ "literal exported parameter", 2, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit -m x" ], [ "exported ordinary Git control", 0, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git status" ], [ "literal exported parameter", 2, "declare -x GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit -m x" ], [ "exported ordinary Git control", 0, "declare -x GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git status" ], [ "literal exported parameter", 2, "typeset -x GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit -m x" ], [ "exported ordinary Git control", 0, "typeset -x GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git status" ], [ "literal exported parameter", 2, "declare -gx GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit -m x" ], [ "exported ordinary Git control", 0, "declare -gx GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git status" ], [ "literal exported parameter", 2, "typeset -gx GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit -m x" ], [ "exported ordinary Git control", 0, "typeset -gx GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git status" ], [ "literal exported parameter", 2, "export -- GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit -m x" ], [ "exported ordinary Git control", 0, "export -- GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git status" ], [ "sticky export order", 2, "GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; export GIT_CONFIG_PARAMETERS; git commit" ], [ "sticky export order", 2, "export GIT_CONFIG_PARAMETERS; GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit" ], [ "sticky export order", 2, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"\ngit push" ], [ "sticky export order", 2, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; GIT_CONFIG_PARAMETERS=''; GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git am patches" ], [ "sticky export order", 2, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; readonly GIT_CONFIG_PARAMETERS; git merge main" ], [ "sticky export order", 2, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; command git rebase main" ], [ "sticky export order", 2, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; env -i git status; git commit" ], [ "sticky export order", 2, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; env -u GIT_CONFIG_PARAMETERS git status; git commit" ], [ "literal variable/export boundary", 0, "GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit" ], [ "literal variable/export boundary", 0, "declare GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit" ], [ "literal variable/export boundary", 0, "typeset GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit" ], [ "literal variable/export boundary", 0, "readonly GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit" ], [ "literal variable/export boundary", 0, "GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\" echo safe; git commit" ], [ "literal variable/export boundary", 0, "env GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\" echo safe; git commit" ], [ "literal variable/export boundary", 0, "command -v export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit" ], [ "literal variable/export boundary", 0, "export -p; git commit" ], [ "literal variable/export boundary", 0, "declare -xp GIT_CONFIG_PARAMETERS; git commit" ], [ "literal variable/export boundary", 0, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; GIT_CONFIG_PARAMETERS=; git commit" ], [ "literal variable/export boundary", 0, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; unset GIT_CONFIG_PARAMETERS; git commit" ], [ "literal variable/export boundary", 0, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; unset -v GIT_CONFIG_PARAMETERS; git commit" ], [ "literal variable/export boundary", 0, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; export -n GIT_CONFIG_PARAMETERS; git commit" ], [ "literal variable/export boundary", 0, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; declare +x GIT_CONFIG_PARAMETERS; git commit" ], [ "literal variable/export boundary", 0, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; env -i git commit" ], [ "literal variable/export boundary", 0, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; env -u GIT_CONFIG_PARAMETERS git commit" ], [ "literal variable/export boundary", 0, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; exec -c git commit" ], [ "sticky count/key/value", 2, "export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null; git commit" ], [ "sticky count/key/value", 2, "GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null; export GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0; git push" ], [ "sticky count/key/value", 2, "export GIT_CONFIG_COUNT=1; export GIT_CONFIG_KEY_0=core.hooksPath; export GIT_CONFIG_VALUE_0=/dev/null; git commit" ], [ "sticky count/key/value", 2, "export GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0; GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null; git commit" ], [ "count export controls", 0, "GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null; git commit" ], [ "count export controls", 0, "export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null; GIT_CONFIG_COUNT=0; git commit" ], [ "count export controls", 0, "export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null; unset GIT_CONFIG_COUNT; git commit" ], [ "count export controls", 0, "export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null; export -n GIT_CONFIG_VALUE_0; git commit" ], [ "count export controls", 0, "export GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.hooksPath GIT_CONFIG_VALUE_0=/dev/null; git status" ], [ "nested scope inherits shell state", 2, "(export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit)" ], [ "nested scope inherits shell state", 2, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; (git commit)" ], [ "nested scope inherits shell state", 2, "{ export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; }; git commit" ], [ "nested scope inherits shell state", 2, "{ export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit; } | cat" ], [ "nested scope inherits shell state", 2, "(export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; git commit) | cat" ], [ "nested scope inherits shell state", 2, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; printf \"%s\" \"$(git commit)\"" ], [ "nested scope inherits shell state", 2, "GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; printf \"%s\" \"$(export GIT_CONFIG_PARAMETERS; git commit)\"" ], [ "nested scope inherits shell state", 2, "export GIT_CONFIG_PARAMETERS=\"'core.hooksPath=/dev/null'\"; cat < { const result = runHook(command); assert.strictEqual(result.code, expected, result.stderr); if (expected === 2) assert.match(result.stderr, /core\.hooksPath/, 'Must identify the literal override, not exhaust the budget'); })) passed++; else failed++; } for (const n of [8, 12]) { if (test(`conditional environment alternatives charge copies within a shared quota at ${n}`, () => { const command = 'export GIT_CONFIG_COUNT=0; ' + 'true && GIT_CONFIG_COUNT=0; '.repeat(n) + 'git status'; const result = countedClassification(command, 3000); assert.strictEqual(result.result.exitCode, 2, JSON.stringify(result)); assert.match(result.result.stderr, /work budget/); assert.ok(result.spent >= 3000 && result.spent < 3100, JSON.stringify(result)); // Include fixed module-level Set construction as a constant allowance. assert.ok(result.copiedStateEntries <= result.spent + 128, JSON.stringify(result)); })) passed++; else failed++; } const pureOnly = process.argv.includes('--pure-only'); if (pureOnly) console.log('Pure classifier mode: 3 bounded Node routing checks omitted.'); else { const home = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-no-verify-')); try { for (const [name, input, disabled, code, direct] of [ ['raw stdin passes through direct hook', 'git status', false, 0, true], ['JSON bypass blocks through runner', JSON.stringify({ tool_input: { command: 'git push --no-verify' } }), false, 2], ['disabled hook is silent', 'git push --no-verify', true, 0], ]) { if (test(name, () => { const args = direct ? [path.join(__dirname, '../../scripts/hooks/block-no-verify.js')] : [runner, 'pre:bash:block-no-verify', 'scripts/hooks/block-no-verify.js', 'minimal,standard,strict']; const result = spawnSync(process.execPath, args, { input, encoding: 'utf8', timeout: 3000, env: { PATH: path.dirname(process.execPath), HOME: home, USERPROFILE: home, TMPDIR: home, TMP: home, TEMP: home, ECC_HOOK_PROFILE: 'standard', ECC_HOOK_CONFIG: path.join(home, 'absent.json'), ECC_DISABLED_HOOKS: disabled ? 'pre:bash:block-no-verify' : '' }, stdio: ['pipe', 'pipe', 'pipe'], }); assert.ifError(result.error); assert.strictEqual(result.status, code, result.stderr); if (code === 0) assert.strictEqual(result.stdout, direct ? input : ''); if (disabled) assert.strictEqual(result.stderr, ''); if (code === 2) assert.match(result.stderr, /BLOCKED/); })) passed++; else failed++; } } finally { fs.rmSync(home, { recursive: true, force: true }); } } // Contributor carry-forward regressions for #2858 and #2897. for (const [name, command, expected] of [ [ "blocks include.path config override", "git -c include.path=/tmp/evil.conf commit -m x", 2 ], [ "blocks quoted include.path config", "git \"-c\" \"include.path=/tmp/evil.conf\" push origin main", 2 ], [ "blocks inline include.path config", "git -cinclude.path=/tmp/evil.conf commit -m x", 2 ], [ "blocks include.path config-env", "git --config-env=include.path=EVIL commit -m x", 2 ], [ "blocks separated include.path config-env", "git --config-env include.path=EVIL commit -m x", 2 ], [ "blocks case-insensitive include.path", "git -c INCLUDE.PATH=/tmp/evil.conf commit -m x", 2 ], [ "blocks conditional include capability", "git -c includeIf.gitdir:**/.path=/tmp/evil.conf commit -m x", 2 ], [ "blocks counted include.path environment", "GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=include.path GIT_CONFIG_VALUE_0=/tmp/evil.conf git commit -m x", 2 ], [ "blocks encoded include.path parameters", "GIT_CONFIG_PARAMETERS=\"'include.path'='/tmp/evil.conf'\" git commit -m x", 2 ], [ "allows unrelated config value", "git -c user.name=ECC commit -m x", 0 ], [ "allows include setting outside protected command", "git -c include.path=/tmp/evil.conf status", 0 ], [ "blocks env long inline split-string", "env --split-string='git commit --no-verify -m x'", 2 ], [ "blocks env split-string after ignore-environment", "env -i --split-string='git commit --no-verify -m x'", 2 ], [ "blocks env separated long split-string", "env --split-string 'git commit --no-verify -m x'", 2 ], [ "blocks env attached short split-string", "env -S'git commit --no-verify -m x'", 2 ], [ "blocks appended argv after env split-string", "env --split-string='git commit' --no-verify -m x", 2 ], [ "allows quoted bypass text passed through env printf", "env --split-string=\"printf '%s' 'git commit --no-verify -m x'\"", 0 ] ]) { if (test(name, () => { const result = runHook({ tool_input: { command } }); assert.strictEqual(result.code, expected, result.stderr); })) passed++; else failed++; } for (const n of [100, 1000, 4000]) { for (const tail of ['git status', 'git commit --no-verify']) { if (test(`literal env split copies stay within the input quota at ${n}: ${tail}`, () => { const command = 'env ' + '-S -i '.repeat(n) + tail; const quota = 24 * (command.length + 1) + 4096; const result = countedClassification(command); assert.strictEqual(result.result.exitCode, 2, JSON.stringify(result)); assert.match(result.result.stderr, /work budget/); assert.ok(result.copiedElements <= quota, JSON.stringify(result)); assert.ok(result.copiedElements <= result.spent, JSON.stringify(result)); // The rejected charge may exceed the quota, but allocation must not run. assert.ok(result.spent >= quota && result.spent <= quota + 2 * (2 * n + 4) + 1, JSON.stringify(result)); })) passed++; else failed++; } } for (const n of [0, 1, 2, 4]) { if (test(`shallow literal env splits allow harmless argv at ${n}`, () => { const command = 'env ' + '-S -i '.repeat(n) + 'git status'; const result = countedClassification(command); assert.strictEqual(result.result.exitCode, 0, JSON.stringify(result)); assert.ok(result.copiedElements <= result.spent, JSON.stringify(result)); })) passed++; else failed++; } const quoteSplitPayload = payload => `'${payload.replace(/'/g, `'\\''`)}'`; const escapedSplitCases = [ ['unquoted escaped separators', 'git\\_commit\\_--no-verify', 2], ['single-quoted literal separators', "'git\\_commit\\_--no-verify'", 0], ['double-quoted escaped separators', '"git\\_commit\\_--no-verify"', 2], ['mixed quoted escaped separators', 'git\\_commit\\_"--no-verify"', 2], ['unsupported stop escape', 'printf ok\\c ignored', 2], ['unsupported double-quoted stop escape', 'printf "ok\\c" ignored', 2], ['single-quoted stop data', "printf '%s' 'ok\\c'", 0], ['single-quoted separator data', "printf '%s' 'git\\_commit\\_--no-verify'", 0], ['single-quoted newline format', "printf '%s\\n' 'git commit --no-verify'", 0], ['unsupported newline escape', 'printf ok\\n', 2], ['unsupported tab escape', 'printf ok\\t', 2], ['unsupported escaped backslash', 'printf ok\\\\_', 2], ['unsupported single-quoted backslash escape', "printf '%s' 'ok\\\\_'", 2], ['unsupported single-quoted apostrophe escape', "printf '%s' 'ok\\'data'", 2], ]; for (const [name, payload, expected] of escapedSplitCases) { for (const option of ['-S ', '-S', '--split-string=', '--split-string ']) { if (test(`env escape boundary ${name}: ${option}`, () => { const command = 'env ' + option + quoteSplitPayload(payload); for (const input of [command, JSON.stringify({ tool_input: { command } })]) { const result = runHook(input); assert.strictEqual(result.code, expected, result.stderr); if (expected === 2) assert.match(result.stderr, /Unsupported env split-string escape/); } })) passed++; else failed++; } } // --- GNU sed execution model (#3051 test intent) --- // sed is a text transformer: its script operand and file operands are data, // except the `e` command and the s///e flag, which run text through the shell. // Ported from the #3051 litmus set: quoted-as-data stays data, real execution // is blocked, and unmodeled invocations keep the conservative opaque default. const sedCases = [ // False positives fixed: non-executing sed operands are data. ['sed filename is data (the #3051 false positive)', "sed 's/x/y/' 'e git commit --no-verify'", 0], ['non-executing script containing a git command is data', "sed 's/x/git commit --no-verify/'", 0], ['file operand containing git is data', "sed 's/eee/zzz/' 'git commit --no-verify.txt'", 0], ['benign line-range script is data', "sed -n '1,3p' file.txt", 0], ['s///e replacement is shell source', "printf x | sed 's/x/git commit --no-verify/e'", 2], ['s///e replacement still blocked further downstream', "printf x | sed 's/x/git commit --no-verify/e' | cat", 2], ['e command operand is shell source', "sed -e 'e git commit --no-verify'", 2], ['bare e consumes piped source as shell code', "printf 'git commit --no-verify' | sed -e 'e'", 2], ['--expression= form is modeled', "sed --expression='s/x/git commit --no-verify/e'", 2], // Conservative defaults preserved: unmodeled invocations stay opaque. ['-f scripts are unreadable: opaque default', "sed -f script.sed 'git commit --no-verify'", 2], ['-i suffix ambiguity: opaque default', "sed -i 's/a/b/' 'git commit --no-verify'", 2], // Litmus anchors from the #3051 direction check (existing behavior pinned). ['quoted-as-data through echo stays allowed', 'echo "git commit --no-verify"', 0], ['direct --no-verify stays blocked', 'git commit --no-verify -m x', 2], ['mixed quoting smuggling a real flag stays blocked', "git commit '--no-verify' -m x", 2], // GNU sed collects -e scripts from any argv position (CodeRabbit finding on // this PR): an execution construct after positional operands still executes. ['later -e script after positional operands is scanned', "printf x | sed -e 's/x/y/' - -e 's/y/git commit --no-verify/e'", 2], ['later -e after a positional script operand is scanned', "sed 's/x/y/' -e 'e git commit --no-verify'", 2], ['later --expression= after positionals is scanned', "sed 's/x/y/' --expression='s/x/git commit --no-verify/e'", 2], ['-- ends options: later tokens are file operands', "sed -e 's/x/y/' -- 'git commit --no-verify'", 0], // With no script option before --, the first operand after -- is still the // script (Greptile P1 on this PR, GNU sed 4.9 verified). ['first operand after -- is still the script', "printf x | sed -- 's/x/git commit --no-verify/e'", 2], ['file operands after the -- script stay data', "sed -- 's/x/y/' 'git commit --no-verify'", 0], // GNU sed decodes escapes in exec-bearing fragments before execution // (Greptile P1 findings on this PR): decoding that surface faithfully is out // of scope, so escaped fragments are conservatively blocked instead. ['hex escape decoding bypass is conservatively blocked', "printf x | sed 's/x/\\x67it commit --no-verify/e'", 2], ['unknown escape decoding bypass is conservatively blocked', "printf x | sed 's/x/\\git commit --no-verify/e'", 2], ['escaped-newline continued e operand is conservatively blocked', "printf x | sed -e 'e git commit \\\n--no-verify'", 2], ['octal escape decoding bypass is conservatively blocked', "printf x | sed 's/x/\\147it commit --no-verify/e'", 2], ['backslash in a non-executing script stays data', "sed 's/a\\/b/c/' 'git commit --no-verify'", 0], // Labels end at `;` and following commands still execute (GNU sed 4.9 // verified: ':a;e cmd' runs cmd). Reading label operands to end of line // let an e command hide behind a label (CodeRabbit finding, landed via // #3495). The label reader must stop at `;` so the rest is scanned. ['e command after a label is scanned', "sed ':a;e git commit --no-verify'", 2], ['e command after a b branch is scanned', "printf x | sed 'ba;e git commit --no-verify'", 2], ['e command after a T branch is scanned', "printf x | sed 'TX;e git commit --no-verify'", 2], ['e command after a newline label is scanned', "sed ':a\ne git commit --no-verify'", 2], ['e command after a t branch is scanned', "printf x | sed 'tX;e git commit --no-verify'", 2], ['plain label without following commands stays data', "sed ':a' 'git commit --no-verify'", 0], ['b branch alone stays data', "sed 'bx' 'git commit --no-verify'", 0] ]; for (const [name, command, expected] of sedCases) { if (test(`sed model ${name}`, () => { for (const input of [command, JSON.stringify({ tool_input: { command } })]) { const result = runHook(input); assert.strictEqual(result.code, expected, `expected exit ${expected}, got ${result.code}: ${result.stderr}`); if (expected === 2) assert.match(result.stderr, /BLOCKED/); } })) passed++; else failed++; } console.log('─'.repeat(50)); console.log(`Passed: ${passed} Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);