mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-14 07:13:35 +08:00
fix(.cursor/hooks): route block-no-verify through local hook to fix message-body false positives (#2107) (#2177)
Cursor hooks still called `npx block-no-verify@1.1.2`, the broken external package whose matcher over-matches: it blocks legitimate `git commit` whenever `--no-verify` (or `no-verify`) appears anywhere in the command string, including inside the commit message body. The Claude Code surface already routes through the in-repo `scripts/hooks/block-no-verify.js`, which performs flag-position-aware tokenisation and passes 25 regression tests covering every false-positive case from #2107. Add a thin Cursor wrapper (`before-shell-execution-block-no-verify.js`) that reads Cursor stdin, transforms to the Claude Code `tool_input.command` shape, delegates to the local hook's exported `run()`, and forwards exit code and stderr. Update `.cursor/hooks.json` to call the wrapper instead of the npx package. New 14-case test file pins the false-positive cases from the issue plus the still-blocked real bypass attempts. Fixes #2107
This commit is contained in:
parent
8dc43e5f60
commit
d8a84b5f7b
@ -17,7 +17,7 @@
|
|||||||
],
|
],
|
||||||
"beforeShellExecution": [
|
"beforeShellExecution": [
|
||||||
{
|
{
|
||||||
"command": "npx block-no-verify@1.1.2",
|
"command": "node .cursor/hooks/before-shell-execution-block-no-verify.js",
|
||||||
"event": "beforeShellExecution",
|
"event": "beforeShellExecution",
|
||||||
"description": "Block git hook-bypass flag to protect pre-commit, commit-msg, and pre-push hooks from being skipped"
|
"description": "Block git hook-bypass flag to protect pre-commit, commit-msg, and pre-push hooks from being skipped"
|
||||||
},
|
},
|
||||||
|
|||||||
63
.cursor/hooks/before-shell-execution-block-no-verify.js
Normal file
63
.cursor/hooks/before-shell-execution-block-no-verify.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Cursor wrapper for block-no-verify.
|
||||||
|
*
|
||||||
|
* Cursor hooks previously called `npx block-no-verify@1.1.2`, an external
|
||||||
|
* package whose matcher over-matches: it blocks legitimate `git commit`
|
||||||
|
* whenever the literal string `--no-verify` (or `no-verify`) appears
|
||||||
|
* anywhere in the command string, including inside the commit message
|
||||||
|
* body. See issue #2107.
|
||||||
|
*
|
||||||
|
* The Claude Code surface already routes through the local, in-repo hook
|
||||||
|
* `scripts/hooks/block-no-verify.js`, which performs flag-position-aware
|
||||||
|
* tokenisation (skipping the value of `-m`, `-F`, `-am "..."`, etc.) and
|
||||||
|
* passes 25 regression tests covering every false-positive case.
|
||||||
|
*
|
||||||
|
* This wrapper gives Cursor the same matcher: read Cursor stdin, transform
|
||||||
|
* to the Claude Code `tool_input.command` shape the local hook understands,
|
||||||
|
* delegate to its exported `run()`, then forward the exit code and stderr.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const { readStdin, hookEnabled } = require('./adapter');
|
||||||
|
const { run } = require('../../scripts/hooks/block-no-verify');
|
||||||
|
|
||||||
|
readStdin()
|
||||||
|
.then(raw => {
|
||||||
|
if (!hookEnabled('pre:bash:block-no-verify', ['minimal', 'standard', 'strict'])) {
|
||||||
|
process.stdout.write(raw);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let command = '';
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw || '{}');
|
||||||
|
command = String(parsed.command || parsed.args?.command || '');
|
||||||
|
} catch {
|
||||||
|
command = String(raw || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Local hook accepts either the raw command string or a Claude-Code
|
||||||
|
// shaped `{ tool_input: { command } }` JSON. Pass the Claude shape so
|
||||||
|
// the JSON branch in extractCommand() is exercised the same way the
|
||||||
|
// Claude Code surface exercises it — keeps the two surfaces on the
|
||||||
|
// same code path.
|
||||||
|
const claudeInput = JSON.stringify({ tool_input: { command } });
|
||||||
|
const result = run(claudeInput);
|
||||||
|
|
||||||
|
if (result && result.exitCode === 2) {
|
||||||
|
if (result.stderr) {
|
||||||
|
process.stderr.write(String(result.stderr) + '\n');
|
||||||
|
}
|
||||||
|
process.exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.stdout.write(raw);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// Per repo rule: hooks must exit 0 on non-critical errors and never
|
||||||
|
// unexpectedly block tool execution. A parse / transport error here
|
||||||
|
// is non-critical — fall through.
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
147
tests/hooks/cursor-block-no-verify.test.js
Normal file
147
tests/hooks/cursor-block-no-verify.test.js
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* Tests for .cursor/hooks/before-shell-execution-block-no-verify.js
|
||||||
|
*
|
||||||
|
* Issue #2107: previously .cursor/hooks.json wired `npx block-no-verify@1.1.2`,
|
||||||
|
* which over-matches and blocks legitimate commits whose message body
|
||||||
|
* mentions `--no-verify` or `-n`. The wrapper added in this PR delegates
|
||||||
|
* to the local scripts/hooks/block-no-verify.js so Cursor users get the
|
||||||
|
* same flag-position-aware matcher Claude Code already uses.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const path = require('path');
|
||||||
|
const { spawnSync } = require('child_process');
|
||||||
|
|
||||||
|
const wrapper = path.join(
|
||||||
|
__dirname, '..', '..',
|
||||||
|
'.cursor', 'hooks', 'before-shell-execution-block-no-verify.js'
|
||||||
|
);
|
||||||
|
|
||||||
|
function runWrapper(input, env = {}) {
|
||||||
|
const rawInput = typeof input === 'string' ? input : JSON.stringify(input);
|
||||||
|
const result = spawnSync('node', [wrapper], {
|
||||||
|
input: rawInput,
|
||||||
|
encoding: 'utf8',
|
||||||
|
env: { ...process.env, ECC_HOOK_PROFILE: 'standard', ...env },
|
||||||
|
timeout: 15000,
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe'],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: Number.isInteger(result.status) ? result.status : 1,
|
||||||
|
stdout: result.stdout || '',
|
||||||
|
stderr: result.stderr || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function test(name, fn) {
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
console.log(` ✓ ${name}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(` ✗ ${name}`);
|
||||||
|
console.log(` Error: ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let passed = 0;
|
||||||
|
let failed = 0;
|
||||||
|
|
||||||
|
console.log('\ncursor block-no-verify wrapper tests');
|
||||||
|
console.log('─'.repeat(50));
|
||||||
|
|
||||||
|
// --- Cursor input shapes ---
|
||||||
|
|
||||||
|
if (test('reads Cursor top-level command field', () => {
|
||||||
|
const r = runWrapper({ command: 'git commit -m "hello"' });
|
||||||
|
assert.strictEqual(r.code, 0, `expected 0, got ${r.code}: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('reads Cursor args.command field', () => {
|
||||||
|
const r = runWrapper({ args: { command: 'git commit -m "hello"' } });
|
||||||
|
assert.strictEqual(r.code, 0, `expected 0, got ${r.code}: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Issue #2107 false positives now allowed ---
|
||||||
|
|
||||||
|
if (test('#2107: allows --no-verify mentioned in double-quoted message body', () => {
|
||||||
|
const r = runWrapper({ command: 'git commit -m "docs: explain why we never pass --no-verify"' });
|
||||||
|
assert.strictEqual(r.code, 0, `expected 0, got ${r.code}: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('#2107: allows --no-verify mentioned in single-quoted message body', () => {
|
||||||
|
const r = runWrapper({ command: "git commit -m 'docs: discuss --no-verify risk'" });
|
||||||
|
assert.strictEqual(r.code, 0, `expected 0, got ${r.code}: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('#2107: allows -n mentioned in quoted message body', () => {
|
||||||
|
const r = runWrapper({ command: 'git commit -m "fix: handle -n flag in parser"' });
|
||||||
|
assert.strictEqual(r.code, 0, `expected 0, got ${r.code}: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('#2107: allows commit message containing the literal string "block-no-verify"', () => {
|
||||||
|
const r = runWrapper({ command: 'git commit -m "feat: add block-no-verify hook"' });
|
||||||
|
assert.strictEqual(r.code, 0, `expected 0, got ${r.code}: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Real bypass attempts still blocked ---
|
||||||
|
|
||||||
|
if (test('still blocks real --no-verify flag', () => {
|
||||||
|
const r = runWrapper({ command: 'git commit --no-verify -m "msg"' });
|
||||||
|
assert.strictEqual(r.code, 2, `expected 2, got ${r.code}`);
|
||||||
|
assert.ok(r.stderr.includes('BLOCKED'), `stderr should contain BLOCKED: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('still blocks -n shorthand on git commit', () => {
|
||||||
|
const r = runWrapper({ command: 'git commit -n -m "msg"' });
|
||||||
|
assert.strictEqual(r.code, 2, `expected 2, got ${r.code}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('still blocks core.hooksPath override', () => {
|
||||||
|
const r = runWrapper({ command: 'git -c core.hooksPath=/dev/null commit -m "msg"' });
|
||||||
|
assert.strictEqual(r.code, 2, `expected 2, got ${r.code}`);
|
||||||
|
assert.ok(r.stderr.includes('core.hooksPath'), `stderr should mention core.hooksPath: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('still blocks --no-verify on git push', () => {
|
||||||
|
const r = runWrapper({ command: 'git push --no-verify' });
|
||||||
|
assert.strictEqual(r.code, 2, `expected 2, got ${r.code}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Pass-through cases ---
|
||||||
|
|
||||||
|
if (test('allows non-git commands', () => {
|
||||||
|
const r = runWrapper({ command: 'npm test' });
|
||||||
|
assert.strictEqual(r.code, 0, `expected 0, got ${r.code}: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('handles empty stdin gracefully', () => {
|
||||||
|
const r = runWrapper('');
|
||||||
|
assert.strictEqual(r.code, 0, `expected 0, got ${r.code}: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
if (test('handles malformed JSON gracefully (treats raw as command string)', () => {
|
||||||
|
const r = runWrapper('git commit -m "hello"');
|
||||||
|
assert.strictEqual(r.code, 0, `expected 0, got ${r.code}: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Disable via ECC_DISABLED_HOOKS ---
|
||||||
|
|
||||||
|
if (test('respects ECC_DISABLED_HOOKS=pre:bash:block-no-verify', () => {
|
||||||
|
const r = runWrapper(
|
||||||
|
{ command: 'git commit --no-verify -m "msg"' },
|
||||||
|
{ ECC_DISABLED_HOOKS: 'pre:bash:block-no-verify' }
|
||||||
|
);
|
||||||
|
// When the hook is disabled, the wrapper should pass through (exit 0)
|
||||||
|
// even on a real bypass attempt.
|
||||||
|
assert.strictEqual(r.code, 0, `expected 0 when disabled, got ${r.code}: ${r.stderr}`);
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
console.log('─'.repeat(50));
|
||||||
|
console.log(`Passed: ${passed} Failed: ${failed}`);
|
||||||
|
|
||||||
|
process.exit(failed > 0 ? 1 : 0);
|
||||||
Loading…
x
Reference in New Issue
Block a user