mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-06-11 02:33:10 +08:00
feat(gateguard): add env knobs for routine bash gate + extra destructive patterns (#2161)
* feat(gateguard): add env knobs for routine bash gate + extra destructive patterns The JS port of gateguard-fact-force has two bash gates: a destructive gate (rm -rf, drop table, git push --force, etc.) that operators want to keep, and a once-per-session routine gate that fires on the very first bash invocation regardless of intent. Operators on hosts where the routine gate is friction without signal (Cursor, OpenCode, etc.) have been maintaining local patches that get clobbered on every plugin update; the Python upstream gateguard-ai already exposes equivalent config via .gateguard.yml. Adds two env vars, both off-by-default so existing behavior is preserved: - GATEGUARD_BASH_ROUTINE_DISABLED — truthy values (1, true, on, yes, enabled) skip the routine bash gate. Destructive gate is unaffected. - GATEGUARD_BASH_EXTRA_DESTRUCTIVE — regex source string for additional destructive patterns. Matches against the same quote-stripped, subshell-flattened command the built-in DESTRUCTIVE_SQL_DD regex sees, so a custom phrase inside $(...) or backticks is also caught. A malformed regex is logged once to stderr and treated as not configured rather than crashing the hook (hooks must never block tool execution unexpectedly). Twelve new tests pin both env vars (truthy aliases, falsy values, unset baseline, destructive-gate-still-fires, alternation members, malformed regex degrades safely, custom phrase inside command substitution). Existing 2619/2619 tests still pass; eslint clean. Fixes #2078 * fix(gateguard): reset extra-destructive warn-once gate when env value changes Both reviewers (CodeRabbit + cubic) flagged that extraDestructiveWarnLogged was never reset when GATEGUARD_BASH_EXTRA_DESTRUCTIVE flipped from one invalid regex to a different invalid regex. The sticky boolean meant a long-running process saw bad-pattern-a's warning then silently swallowed bad-pattern-b's parse failure. Fix: clear extraDestructiveWarnLogged whenever the cache key changes (i.e. before the regex compile attempt). The warn-once-per-distinct- pattern invariant now matches the per-key cache invariant. Adds a same-process regression test via loadDirectHook() that spies on process.stderr.write and asserts: same bad pattern warns once across multiple invocations; switching to a different bad pattern emits a second warning; switching to a valid regex emits zero warnings.
This commit is contained in:
@@ -46,6 +46,7 @@ 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_ENABLE_VALUES = new Set(['1', 'true', 'on', 'enabled', 'enable', 'yes']);
|
||||
|
||||
// SQL-keyword + dd patterns stay as a single regex — they are stable
|
||||
// phrases without shell-flag ordering concerns. Quoted strings are
|
||||
@@ -53,6 +54,54 @@ const ECC_DISABLE_VALUES = new Set(['0', 'false', 'off', 'disabled', 'disable'])
|
||||
// "drop table" no longer triggers a false positive.
|
||||
const DESTRUCTIVE_SQL_DD = /\b(drop\s+table|delete\s+from|truncate|dd\s+if=)\b/i;
|
||||
|
||||
// Operator-supplied additional destructive patterns. Lazily compiled from
|
||||
// `GATEGUARD_BASH_EXTRA_DESTRUCTIVE` (regex source) on first use, then
|
||||
// memoized keyed by the env-var value so a test or long-running process
|
||||
// that flips the env between calls re-reads it without paying for a
|
||||
// recompile on every invocation. A malformed regex is treated as
|
||||
// "not configured" (the gate falls back to the built-in patterns) and
|
||||
// the parse failure is logged once via `[gateguard-fact-force]` to
|
||||
// stderr — hooks must never crash tool execution because of operator
|
||||
// config errors.
|
||||
let extraDestructiveCacheKey = null;
|
||||
let extraDestructiveCacheRegex = null;
|
||||
let extraDestructiveWarnLogged = false;
|
||||
function getExtraDestructiveRegex() {
|
||||
const raw = process.env.GATEGUARD_BASH_EXTRA_DESTRUCTIVE || '';
|
||||
if (!raw) {
|
||||
extraDestructiveCacheKey = '';
|
||||
extraDestructiveCacheRegex = null;
|
||||
return null;
|
||||
}
|
||||
if (raw === extraDestructiveCacheKey) {
|
||||
return extraDestructiveCacheRegex;
|
||||
}
|
||||
// The env value just changed; reset the once-per-pattern warning gate
|
||||
// so a subsequent *different* invalid regex is also reported once. The
|
||||
// previous shape kept the flag sticky and silently swallowed the
|
||||
// second bad pattern in a long-running process.
|
||||
extraDestructiveCacheKey = raw;
|
||||
extraDestructiveWarnLogged = false;
|
||||
try {
|
||||
extraDestructiveCacheRegex = new RegExp(raw, 'i');
|
||||
} catch (err) {
|
||||
extraDestructiveCacheRegex = null;
|
||||
if (!extraDestructiveWarnLogged) {
|
||||
try {
|
||||
process.stderr.write(
|
||||
`[gateguard-fact-force] ignoring invalid GATEGUARD_BASH_EXTRA_DESTRUCTIVE regex: ${err.message}\n`
|
||||
);
|
||||
} catch (_) { /* stderr write failure is non-fatal */ }
|
||||
extraDestructiveWarnLogged = true;
|
||||
}
|
||||
}
|
||||
return extraDestructiveCacheRegex;
|
||||
}
|
||||
|
||||
function isRoutineBashGateDisabled() {
|
||||
return ECC_ENABLE_VALUES.has(normalizeEnvValue(process.env.GATEGUARD_BASH_ROUTINE_DISABLED));
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the contents of single- and double-quoted strings so phrases
|
||||
* mentioned inside a commit message or echoed argument do not trigger
|
||||
@@ -414,9 +463,17 @@ function isDestructiveBash(command) {
|
||||
const flattened = explodeSubshells(stripQuotedStrings(raw));
|
||||
if (DESTRUCTIVE_SQL_DD.test(flattened)) return true;
|
||||
|
||||
// Operator-supplied additional destructive patterns. Same scope as the
|
||||
// built-in SQL/dd regex: matched against the quote-stripped, subshell-
|
||||
// exploded command so a phrase inside `$(...)` or backticks is caught.
|
||||
const extra = getExtraDestructiveRegex();
|
||||
if (extra && extra.test(flattened)) return true;
|
||||
|
||||
const segments = collectExecutableBodies(raw).flatMap(splitCommandSegments);
|
||||
for (const segment of segments) {
|
||||
if (DESTRUCTIVE_SQL_DD.test(stripQuotedStrings(segment))) return true;
|
||||
const stripped = stripQuotedStrings(segment);
|
||||
if (DESTRUCTIVE_SQL_DD.test(stripped)) return true;
|
||||
if (extra && extra.test(stripped)) return true;
|
||||
const tokens = tokenize(segment);
|
||||
if (isDestructiveRm(tokens)) return true;
|
||||
if (isDestructiveGit(tokens)) return true;
|
||||
@@ -883,6 +940,14 @@ function run(rawInput) {
|
||||
return rawInput; // allow retry after facts presented
|
||||
}
|
||||
|
||||
// Operator opt-out: skip the routine-bash gate entirely. The destructive
|
||||
// gate above still fires. This is the documented escape hatch for hosts
|
||||
// (Cursor, OpenCode, etc.) where the once-per-session routine gate is
|
||||
// friction without signal.
|
||||
if (isRoutineBashGateDisabled()) {
|
||||
return rawInput; // routine gate opted out via env
|
||||
}
|
||||
|
||||
if (!isChecked(ROUTINE_BASH_SESSION_KEY)) {
|
||||
if (!markChecked(ROUTINE_BASH_SESSION_KEY)) {
|
||||
return allowWithStateWarning();
|
||||
|
||||
Reference in New Issue
Block a user