B-05: ordinary arguments between Git subcommands and flags bypass matching
Summary: In the installed ECC release, an ordinary argument or option between a dangerous Git subcommand and its flag defeats the matcher because the flag must immediately follow the subcommand.
Affected version: 2.0.0-rc.1, commit 841beea45cb2.
Environment
The observation is against the installed copy of gateguard-fact-force.js, which is 487 lines. The plugin version is 2.0.0-rc.1 at commit 841beea45cb2.
Reproduction
Save as repro.js and run with node repro.js. Replace <plugin> with the installed
plugin directory.
const os = require('os'), fs = require('fs'), path = require('path');
// Isolate hook state so the repro cannot touch a real ~/.gateguard directory.
process.env.HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'ggrepro-'));
// The module exports exactly one function: run(rawInput).
const { run } = require('<plugin>/scripts/hooks/gateguard-fact-force.js');
// A deny is signalled by run() returning something OTHER than the object passed in.
function decide(command) {
const input = { tool_name: 'Bash', tool_input: { command }, session_id: 'repro' };
return run(input) === input ? 'ALLOWED' : 'DENIED';
}
decide('printf ready'); // prime — see note below
for (const command of [
'git reset --quiet --hard HEAD',
'git checkout -q -- src/f.py',
'git clean -d -f',
'git push origin --force main',
'git reset --hard HEAD', // control: still denied
]) console.log(decide(command).padEnd(8), command);
Two notes that otherwise make this confusing to reproduce:
tool_name is required. Without it, run() falls through every branch and returns
the input unchanged, so every command reads as ALLOWED and the defect looks absent.
- Prime the session. The first Bash command of a session trips a separate routine
gate, which masks the destructive arm on whichever command happens to be first.
Observed results
| Command |
Installed decision |
git reset --quiet --hard HEAD |
ALLOWED |
git checkout -q -- src/f.py |
ALLOWED |
git clean -d -f |
ALLOWED |
git push origin --force main |
ALLOWED |
git reset --hard HEAD (control) |
DENIED |
git clean -f (control) |
DENIED |
The two controls matter: the matcher is not broken in general, only adjacency-dependent.
Each pair differs solely by an ordinary flag between the subcommand and the dangerous flag.
Impact
These spellings can discard uncommitted work, delete untracked files, or force-update a remote branch. git clean -d -f is especially notable because it is a command spelling people type by habit.
Suggested fix
Locate the Git subcommand structurally, then accept ordinary interstitial arguments before evaluating the dangerous flag. Upstream main already contains a structural findGitSubcommand() parser that addresses this failure mode. Please cut a release containing that parser and add regression tests for these cases.
B-05: ordinary arguments between Git subcommands and flags bypass matching
Summary: In the installed ECC release, an ordinary argument or option between a dangerous Git subcommand and its flag defeats the matcher because the flag must immediately follow the subcommand.
Affected version:
2.0.0-rc.1, commit841beea45cb2.Environment
The observation is against the installed copy of
gateguard-fact-force.js, which is 487 lines. The plugin version is2.0.0-rc.1at commit841beea45cb2.Reproduction
Save as
repro.jsand run withnode repro.js. Replace<plugin>with the installedplugin directory.
Two notes that otherwise make this confusing to reproduce:
tool_nameis required. Without it,run()falls through every branch and returnsthe input unchanged, so every command reads as ALLOWED and the defect looks absent.
gate, which masks the destructive arm on whichever command happens to be first.
Observed results
git reset --quiet --hard HEADgit checkout -q -- src/f.pygit clean -d -fgit push origin --force maingit reset --hard HEAD(control)git clean -f(control)The two controls matter: the matcher is not broken in general, only adjacency-dependent.
Each pair differs solely by an ordinary flag between the subcommand and the dangerous flag.
Impact
These spellings can discard uncommitted work, delete untracked files, or force-update a remote branch.
git clean -d -fis especially notable because it is a command spelling people type by habit.Suggested fix
Locate the Git subcommand structurally, then accept ordinary interstitial arguments before evaluating the dangerous flag. Upstream
mainalready contains a structuralfindGitSubcommand()parser that addresses this failure mode. Please cut a release containing that parser and add regression tests for these cases.