fix: show correct gateguard hook recovery id

This commit is contained in:
Affaan Mustafa
2026-04-30 11:08:52 -04:00
committed by Affaan Mustafa
parent 7c5452f4fa
commit bb40978e31
2 changed files with 28 additions and 6 deletions

View File

@@ -38,6 +38,8 @@ const READ_HEARTBEAT_MS = 60 * 1000;
const MAX_CHECKED_ENTRIES = 500; const MAX_CHECKED_ENTRIES = 500;
const MAX_SESSION_KEYS = 50; const MAX_SESSION_KEYS = 50;
const ROUTINE_BASH_SESSION_KEY = '__bash_session__'; const ROUTINE_BASH_SESSION_KEY = '__bash_session__';
const EDIT_WRITE_HOOK_ID = 'pre:edit-write:gateguard-fact-force';
const BASH_HOOK_ID = 'pre:bash:gateguard-fact-force';
const ECC_DISABLE_VALUES = new Set(['0', 'false', 'off', 'disabled', 'disable']); const ECC_DISABLE_VALUES = new Set(['0', 'false', 'off', 'disabled', 'disable']);
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; 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;
@@ -365,11 +367,12 @@ function routineBashMsg() {
].join('\n'); ].join('\n');
} }
function withRecoveryHint(message) { function withRecoveryHint(message, hookIds = [EDIT_WRITE_HOOK_ID]) {
const disableTargets = hookIds.map(hookId => `\`${hookId}\``).join(' or ');
return [ return [
message, message,
'', '',
'Recovery: if GateGuard is blocking setup or repair work, run this session with `ECC_GATEGUARD=off` or add `pre:edit-write:gateguard-fact-force` to `ECC_DISABLED_HOOKS`.' `Recovery: if GateGuard is blocking setup or repair work, run this session with \`ECC_GATEGUARD=off\` or add ${disableTargets} to \`ECC_DISABLED_HOOKS\`.`
].join('\n'); ].join('\n');
} }
@@ -377,12 +380,13 @@ function withRecoveryHint(message) {
function denyResult(reason, options = {}) { function denyResult(reason, options = {}) {
const includeRecoveryHint = options.includeRecoveryHint !== false; const includeRecoveryHint = options.includeRecoveryHint !== false;
const hookIds = Array.isArray(options.hookIds) && options.hookIds.length > 0 ? options.hookIds : [EDIT_WRITE_HOOK_ID];
return { return {
stdout: JSON.stringify({ stdout: JSON.stringify({
hookSpecificOutput: { hookSpecificOutput: {
hookEventName: 'PreToolUse', hookEventName: 'PreToolUse',
permissionDecision: 'deny', permissionDecision: 'deny',
permissionDecisionReason: includeRecoveryHint ? withRecoveryHint(reason) : reason permissionDecisionReason: includeRecoveryHint ? withRecoveryHint(reason, hookIds) : reason
} }
}), }),
exitCode: 0 exitCode: 0
@@ -471,7 +475,7 @@ function run(rawInput) {
if (!markChecked(ROUTINE_BASH_SESSION_KEY)) { if (!markChecked(ROUTINE_BASH_SESSION_KEY)) {
return allowWithStateWarning(); return allowWithStateWarning();
} }
return denyResult(routineBashMsg()); return denyResult(routineBashMsg(), { hookIds: [BASH_HOOK_ID] });
} }
return rawInput; // allow return rawInput; // allow

View File

@@ -471,7 +471,25 @@ function runTests() {
'denial reason should mention the existing hook-id disable control'); 'denial reason should mention the existing hook-id disable control');
})) passed++; else failed++; })) passed++; else failed++;
// --- Test 14: destructive Bash denials do not advertise the recovery escape hatch --- // --- Test 14: routine Bash denial messages show the Bash hook escape hatch ---
clearState();
if (test('routine Bash denials include Bash hook disable id', () => {
const input = {
tool_name: 'Bash',
tool_input: { command: 'npm test' }
};
const result = runBashHook(input);
const output = parseOutput(result.stdout);
const reason = output.hookSpecificOutput.permissionDecisionReason;
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
assert.ok(reason.includes('pre:bash:gateguard-fact-force'),
'routine Bash denial should show the Bash hook ID');
assert.ok(!reason.includes('pre:edit-write:gateguard-fact-force'),
'routine Bash denial should not show the Edit/Write hook ID as the targeted disable');
})) passed++; else failed++;
// --- Test 15: destructive Bash denials do not advertise the recovery escape hatch ---
clearState(); clearState();
if (test('destructive Bash denials omit recovery escape hatch', () => { if (test('destructive Bash denials omit recovery escape hatch', () => {
const input = { const input = {
@@ -487,7 +505,7 @@ function runTests() {
'destructive gate should not advertise disabling GateGuard'); 'destructive gate should not advertise disabling GateGuard');
})) passed++; else failed++; })) passed++; else failed++;
// --- Test 15: MultiEdit gates first unchecked file --- // --- Test 16: MultiEdit gates first unchecked file ---
clearState(); clearState();
if (test('denies first MultiEdit with unchecked file', () => { if (test('denies first MultiEdit with unchecked file', () => {
const input = { const input = {