mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-01 14:33:31 +08:00
fix: add gateguard recovery escape hatch
This commit is contained in:
committed by
Affaan Mustafa
parent
4c8499d509
commit
cfe770a735
@@ -99,6 +99,9 @@ export ECC_HOOK_PROFILE=standard
|
|||||||
# Disable specific hook IDs (comma-separated)
|
# Disable specific hook IDs (comma-separated)
|
||||||
export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck"
|
export ECC_DISABLED_HOOKS="pre:bash:tmux-reminder,post:edit:typecheck"
|
||||||
|
|
||||||
|
# Disable only GateGuard during setup or recovery
|
||||||
|
export ECC_GATEGUARD=off
|
||||||
|
|
||||||
# Cap SessionStart additional context (default: 8000 chars)
|
# Cap SessionStart additional context (default: 8000 chars)
|
||||||
export ECC_SESSION_START_MAX_CHARS=4000
|
export ECC_SESSION_START_MAX_CHARS=4000
|
||||||
|
|
||||||
|
|||||||
@@ -38,11 +38,25 @@ 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 LEGACY_DISABLE_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
||||||
|
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;
|
||||||
|
|
||||||
// --- State management (per-session, atomic writes, bounded) ---
|
// --- State management (per-session, atomic writes, bounded) ---
|
||||||
|
|
||||||
|
function normalizeEnvValue(value) {
|
||||||
|
return String(value || '').trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGateGuardDisabled() {
|
||||||
|
if (LEGACY_DISABLE_VALUES.has(normalizeEnvValue(process.env.GATEGUARD_DISABLED))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ECC_DISABLE_VALUES.has(normalizeEnvValue(process.env.ECC_GATEGUARD));
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeSessionKey(value) {
|
function sanitizeSessionKey(value) {
|
||||||
const raw = String(value || '').trim();
|
const raw = String(value || '').trim();
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
@@ -352,6 +366,14 @@ function routineBashMsg() {
|
|||||||
].join('\n');
|
].join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function withRecoveryHint(message) {
|
||||||
|
return [
|
||||||
|
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`.'
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
// --- Deny helper ---
|
// --- Deny helper ---
|
||||||
|
|
||||||
function denyResult(reason) {
|
function denyResult(reason) {
|
||||||
@@ -360,7 +382,7 @@ function denyResult(reason) {
|
|||||||
hookSpecificOutput: {
|
hookSpecificOutput: {
|
||||||
hookEventName: 'PreToolUse',
|
hookEventName: 'PreToolUse',
|
||||||
permissionDecision: 'deny',
|
permissionDecision: 'deny',
|
||||||
permissionDecisionReason: reason
|
permissionDecisionReason: withRecoveryHint(reason)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
exitCode: 0
|
exitCode: 0
|
||||||
@@ -383,6 +405,11 @@ function run(rawInput) {
|
|||||||
} catch (_) {
|
} catch (_) {
|
||||||
return rawInput; // allow on parse error
|
return rawInput; // allow on parse error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isGateGuardDisabled()) {
|
||||||
|
return rawInput;
|
||||||
|
}
|
||||||
|
|
||||||
activeStateFile = null;
|
activeStateFile = null;
|
||||||
getStateFile(data);
|
getStateFile(data);
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,10 @@ Triggers on: `rm -rf`, `git reset --hard`, `git push --force`, `drop table`, etc
|
|||||||
|
|
||||||
The hook at `scripts/hooks/gateguard-fact-force.js` is included in this plugin. Enable it via hooks.json.
|
The hook at `scripts/hooks/gateguard-fact-force.js` is included in this plugin. Enable it via hooks.json.
|
||||||
|
|
||||||
|
If GateGuard blocks setup or repair work, start the session with
|
||||||
|
`ECC_GATEGUARD=off`. For hook-level control, keep using
|
||||||
|
`ECC_DISABLED_HOOKS` with the GateGuard hook ID.
|
||||||
|
|
||||||
### Option B: Full package with config
|
### Option B: Full package with config
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -408,7 +408,56 @@ function runTests() {
|
|||||||
}
|
}
|
||||||
})) passed++; else failed++;
|
})) passed++; else failed++;
|
||||||
|
|
||||||
// --- Test 10: MultiEdit gates first unchecked file ---
|
// --- Test 10: respects direct GateGuard env disable for recovery sessions ---
|
||||||
|
clearState();
|
||||||
|
if (test('respects ECC_GATEGUARD=off without writing gate state', () => {
|
||||||
|
const input = {
|
||||||
|
tool_name: 'Write',
|
||||||
|
tool_input: { file_path: '/src/env-disabled.js', content: 'export const ok = true;' }
|
||||||
|
};
|
||||||
|
const result = runHook(input, { ECC_GATEGUARD: 'off' });
|
||||||
|
const output = parseOutput(result.stdout);
|
||||||
|
|
||||||
|
assert.ok(output, 'should produce valid JSON output');
|
||||||
|
assert.strictEqual(output.tool_name, 'Write', 'disabled gate should pass through raw input');
|
||||||
|
assert.ok(!output.hookSpecificOutput, 'disabled gate should not deny the operation');
|
||||||
|
assert.ok(!fs.existsSync(stateFile), 'disabled gate should not create or mutate gate state');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Test 11: respects legacy GATEGUARD_DISABLED env disable ---
|
||||||
|
clearState();
|
||||||
|
if (test('respects GATEGUARD_DISABLED=1 for Bash recovery', () => {
|
||||||
|
const input = {
|
||||||
|
tool_name: 'Bash',
|
||||||
|
tool_input: { command: 'npm test' }
|
||||||
|
};
|
||||||
|
const result = runBashHook(input, { GATEGUARD_DISABLED: '1' });
|
||||||
|
const output = parseOutput(result.stdout);
|
||||||
|
|
||||||
|
assert.ok(output, 'should produce valid JSON output');
|
||||||
|
assert.strictEqual(output.tool_name, 'Bash', 'disabled gate should pass Bash through raw input');
|
||||||
|
assert.ok(!output.hookSpecificOutput, 'disabled gate should not deny Bash');
|
||||||
|
assert.ok(!fs.existsSync(stateFile), 'disabled gate should not create or mutate gate state');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Test 12: denial messages show an escape hatch ---
|
||||||
|
clearState();
|
||||||
|
if (test('denial messages include direct recovery escape hatch', () => {
|
||||||
|
const input = {
|
||||||
|
tool_name: 'Write',
|
||||||
|
tool_input: { file_path: '/src/recovery-hint.js', content: 'export const ok = true;' }
|
||||||
|
};
|
||||||
|
const result = runHook(input);
|
||||||
|
const output = parseOutput(result.stdout);
|
||||||
|
|
||||||
|
assert.strictEqual(output.hookSpecificOutput.permissionDecision, 'deny');
|
||||||
|
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('ECC_GATEGUARD=off'),
|
||||||
|
'denial reason should show the direct recovery env toggle');
|
||||||
|
assert.ok(output.hookSpecificOutput.permissionDecisionReason.includes('ECC_DISABLED_HOOKS'),
|
||||||
|
'denial reason should mention the existing hook-id disable control');
|
||||||
|
})) passed++; else failed++;
|
||||||
|
|
||||||
|
// --- Test 13: 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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user