Skip to content

Commit 4f37387

Browse files
fix(hooks): block-no-verify handles stuck optional values and long-option prefixes (#3073)
Two cases the word-level rewrite still got wrong. Short options that take an optional stuck value (-u[mode], -S[keyid]) end the cluster scan, so git commit -uno and -Sn are allowed while -nu stays blocked. Git accepts any unambiguous long-option prefix, so --no-veri and --no-verif on commit, push, merge and rebase are now blocked; --no-verbose stays allowed. Quoted data such as -m "--no-verify" is still treated as data. Independent exact-head review probed 34 commands in-process and against real git with no bypass and no false positive; hook test 35/35, eslint clean, CI 44/44 at the head.
1 parent 95b9fe1 commit 4f37387

2 files changed

Lines changed: 48 additions & 1 deletion

File tree

scripts/hooks/block-no-verify.js

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ const COMMIT_OPTIONS_WITH_INLINE_VALUE = [
7878
// must stop at this character — anything after it is the inline value,
7979
// not another flag.
8080
const COMMIT_SHORT_OPTIONS_WITH_VALUE = new Set(['m', 'F', 'C', 'c', 't']);
81+
// Short options whose value is OPTIONAL and must be stuck to the flag
82+
// (`-uno`, `-S<keyid>`). The rest of the cluster is that value, so an `n`
83+
// after them is not the -n flag: `git commit -uno` means --untracked-files=no.
84+
const COMMIT_SHORT_OPTIONS_WITH_OPTIONAL_VALUE = new Set(['u', 'S']);
8185

8286
function tokenizeShellWords(input, start = 0, end = input.length) {
8387
const tokens = [];
@@ -264,6 +268,7 @@ function isCommitNoVerifyShortFlag(value) {
264268
const option = options.charAt(i);
265269
if (option === 'n') return true;
266270
if (COMMIT_SHORT_OPTIONS_WITH_VALUE.has(option)) return false;
271+
if (COMMIT_SHORT_OPTIONS_WITH_OPTIONAL_VALUE.has(option)) return false;
267272
}
268273

269274
return false;
@@ -388,6 +393,16 @@ function detectGitCommand(input, start = 0) {
388393
return null;
389394
}
390395

396+
/**
397+
* git's option parser accepts any unambiguous prefix of a long option, so
398+
* `--no-veri` and `--no-verif` run as --no-verify. Shorter prefixes such as
399+
* `--no-ver` are ambiguous with --no-verbose and git rejects them itself, so
400+
* refusing every prefix from `--no-v` up blocks nothing that would have run.
401+
*/
402+
function isNoVerifyLongFlag(value) {
403+
return value.length >= '--no-v'.length && '--no-verify'.startsWith(value);
404+
}
405+
391406
/**
392407
* Check if the input contains a --no-verify flag for a specific git command.
393408
* Only inspects the portion of the input starting at `offset` (the position
@@ -422,7 +437,7 @@ function hasNoVerifyFlag(input, command, offset) {
422437
}
423438
}
424439

425-
if (value === '--no-verify') return true;
440+
if (isNoVerifyLongFlag(value)) return true;
426441

427442
// For commit, -n is shorthand for --no-verify.
428443
if (command === 'commit' && isCommitNoVerifyShortFlag(value)) {

tests/hooks/block-no-verify.test.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,38 @@ if (test('still allows -tn (n is the -t template path, not a flag)', () => {
219219
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`);
220220
})) passed++; else failed++;
221221

222+
// --- Optional stuck values (-u, -S) and long-option prefixes ---
223+
224+
if (test('allows -uno (n is the -u untracked-files mode, not a flag)', () => {
225+
const r = runHook({ tool_input: { command: 'git commit -uno -m "msg"' } });
226+
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`);
227+
})) passed++; else failed++;
228+
229+
if (test('allows -Sn (n is the -S key id, not a flag)', () => {
230+
const r = runHook({ tool_input: { command: 'git commit -Sn -m "msg"' } });
231+
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`);
232+
})) passed++; else failed++;
233+
234+
if (test('still blocks -nu (n comes before the optional-value flag)', () => {
235+
const r = runHook({ tool_input: { command: 'git commit -nu -m "msg"' } });
236+
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
237+
})) passed++; else failed++;
238+
239+
if (test('blocks --no-veri (git accepts unambiguous long-option prefixes)', () => {
240+
const r = runHook({ tool_input: { command: 'git commit --no-veri -m "msg"' } });
241+
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
242+
})) passed++; else failed++;
243+
244+
if (test('blocks --no-verif on git push', () => {
245+
const r = runHook({ tool_input: { command: 'git push --no-verif origin main' } });
246+
assert.strictEqual(r.code, 2, `expected exit 2, got ${r.code}`);
247+
})) passed++; else failed++;
248+
249+
if (test('allows --no-verbose (not a prefix of --no-verify)', () => {
250+
const r = runHook({ tool_input: { command: 'git commit --no-verbose -m "msg"' } });
251+
assert.strictEqual(r.code, 0, `expected exit 0, got ${r.code}: ${r.stderr}`);
252+
})) passed++; else failed++;
253+
222254
console.log('─'.repeat(50));
223255
console.log(`Passed: ${passed} Failed: ${failed}`);
224256

0 commit comments

Comments
 (0)