fix(gateguard): gate force/path git checkout as destructive (#2158)

* fix(gateguard): gate force/path git checkout as destructive

The destructive-command gate's `checkout` handler only flagged
`git checkout -- <path>`. It missed `git checkout --force` / `-f <branch>`
and `git checkout .`, all of which discard uncommitted working-tree changes,
so they bypassed the gate (once the once-per-session routine-Bash gate is
satisfied, they ran with no challenge). The sibling `switch` handler already
covers these force forms; mirror it for `checkout`.

* test(gateguard): document Test 7b force-checkout case

---------

Co-authored-by: bymle <229636660+bymle@users.noreply.github.com>
This commit is contained in:
bymle
2026-06-07 13:26:08 +08:00
committed by GitHub
parent 680cc7153b
commit 0cb8907e14
2 changed files with 27 additions and 1 deletions

View File

@@ -301,6 +301,25 @@ function runTests() {
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('rollback'));
})) passed++; else failed++;
/**
* Test 7b: `git checkout -f <branch>` (force checkout) discards uncommitted
* working-tree changes, so it must be gated as destructive Bash.
*/
clearState();
if (test('denies git checkout -f as destructive Bash', () => {
const input = {
tool_name: 'Bash',
tool_input: { command: 'git checkout -f main' }
};
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', () => {