fix: refine gateguard destructive git detection

This commit is contained in:
Affaan Mustafa 2026-04-29 22:30:57 -04:00 committed by Affaan Mustafa
parent 17aafc4506
commit 1188aeafc4
2 changed files with 57 additions and 1 deletions

View File

@ -39,7 +39,7 @@ const MAX_CHECKED_ENTRIES = 500;
const MAX_SESSION_KEYS = 50;
const ROUTINE_BASH_SESSION_KEY = '__bash_session__';
const DESTRUCTIVE_BASH = /\b(rm\s+-rf|git\s+reset\s+--hard|git\s+checkout\s+--|git\s+clean\s+-f|drop\s+table|delete\s+from|truncate|git\s+push\s+--force|dd\s+if=)\b/i;
const DESTRUCTIVE_BASH = /\b(rm\s+-rf|git\s+reset\s+--hard|git\s+checkout\s+--|git\s+clean\s+-f|drop\s+table|delete\s+from|truncate|git\s+push\s+--force(?!-with-lease)|git\s+commit\s+--amend|dd\s+if=)\b/i;
// --- State management (per-session, atomic writes, bounded) ---

View File

@ -223,6 +223,62 @@ function runTests() {
// --- Test 5: denies first routine Bash, allows second ---
clearState();
if (test('allows safe git push --force-with-lease without destructive gate', () => {
writeState({
checked: ['__bash_session__'],
last_active: Date.now()
});
const input = {
tool_name: 'Bash',
tool_input: { command: 'git push --force-with-lease origin feature-branch' }
};
const result = runBashHook(input);
assert.strictEqual(result.code, 0, 'exit code should be 0');
const output = parseOutput(result.stdout);
assert.ok(output, 'should produce valid JSON output');
if (output.hookSpecificOutput) {
assert.notStrictEqual(output.hookSpecificOutput.permissionDecision, 'deny',
'safe lease-protected force push should not be denied');
} else {
assert.strictEqual(output.tool_name, 'Bash', 'pass-through should preserve input');
}
})) passed++; else failed++;
// --- Test 6: gates amend as destructive Bash ---
clearState();
if (test('denies git commit --amend as destructive Bash', () => {
const input = {
tool_name: 'Bash',
tool_input: { command: 'git commit --amend --no-edit' }
};
const result = runBashHook(input);
assert.strictEqual(result.code, 0, 'exit code should be 0');
const output = parseOutput(result.stdout);
assert.ok(output, 'should produce JSON output');
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('Destructive'));
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('rollback'));
})) passed++; else failed++;
// --- Test 7: still gates plain force push as destructive Bash ---
clearState();
if (test('denies plain git push --force as destructive Bash', () => {
const input = {
tool_name: 'Bash',
tool_input: { command: 'git push --force origin feature-branch' }
};
const result = runBashHook(input);
assert.strictEqual(result.code, 0, 'exit code should be 0');
const output = parseOutput(result.stdout);
assert.ok(output, 'should produce JSON output');
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('Destructive'));
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('rollback'));
})) passed++; else failed++;
// --- Test 8: denies first routine Bash, allows second ---
clearState();
if (test('denies first routine Bash, allows second', () => {
const input = {
tool_name: 'Bash',