mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-14 13:53:29 +08:00
feat(hooks,skills): add gateguard fact-forcing pre-action gate
A PreToolUse hook that forces Claude to investigate before editing.
Instead of self-evaluation ("are you sure?"), it demands concrete facts:
importers, public API, data schemas, user instruction.
A/B tested: +2.25 quality points (9.0 vs 6.75) across two independent tasks.
- scripts/hooks/gateguard-fact-force.js — standalone Node.js hook
- skills/gateguard/SKILL.md — skill documentation
- hooks/hooks.json — PreToolUse entries for Edit|Write and Bash
Full package with config: pip install gateguard-ai
Repo: https://github.com/zunoworks/gateguard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -126,6 +126,30 @@
|
|||||||
],
|
],
|
||||||
"description": "Check MCP server health before MCP tool execution and block unhealthy MCP calls",
|
"description": "Check MCP server health before MCP tool execution and block unhealthy MCP calls",
|
||||||
"id": "pre:mcp-health-check"
|
"id": "pre:mcp-health-check"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Edit|Write",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/gateguard-fact-force.js\"",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Fact-forcing gate: block first Edit/Write per file and demand investigation (importers, data schemas, user instruction) before allowing",
|
||||||
|
"id": "pre:edit-write:gateguard-fact-force"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/hooks/gateguard-fact-force.js\"",
|
||||||
|
"timeout": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Fact-forcing gate: block destructive Bash commands and demand rollback plan; quote user instruction on first Bash per session",
|
||||||
|
"id": "pre:bash:gateguard-fact-force"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"PreCompact": [
|
"PreCompact": [
|
||||||
|
|||||||
216
scripts/hooks/gateguard-fact-force.js
Normal file
216
scripts/hooks/gateguard-fact-force.js
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* PreToolUse Hook: GateGuard Fact-Forcing Gate
|
||||||
|
*
|
||||||
|
* Forces Claude to investigate before editing files or running commands.
|
||||||
|
* Instead of asking "are you sure?" (which LLMs always answer "yes"),
|
||||||
|
* this hook demands concrete facts: importers, public API, data schemas.
|
||||||
|
*
|
||||||
|
* The act of investigation creates awareness that self-evaluation never did.
|
||||||
|
*
|
||||||
|
* Gates:
|
||||||
|
* - Edit/Write: list importers, affected API, verify data schemas, quote instruction
|
||||||
|
* - Bash (destructive): list targets, rollback plan, quote instruction
|
||||||
|
* - Bash (routine): quote current instruction (once per session)
|
||||||
|
*
|
||||||
|
* Exit codes:
|
||||||
|
* 0 - Allow (gate already passed for this target)
|
||||||
|
* 2 - Block (force investigation first)
|
||||||
|
*
|
||||||
|
* Cross-platform (Windows, macOS, Linux).
|
||||||
|
*
|
||||||
|
* Full package with config support: pip install gateguard-ai
|
||||||
|
* Repo: https://github.com/zunoworks/gateguard
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const MAX_STDIN = 1024 * 1024;
|
||||||
|
|
||||||
|
// Session state file for tracking which files have been gated
|
||||||
|
const STATE_DIR = path.join(process.env.HOME || process.env.USERPROFILE || '/tmp', '.gateguard');
|
||||||
|
const STATE_FILE = path.join(STATE_DIR, '.session_state.json');
|
||||||
|
|
||||||
|
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|dd\s+if=)\b/i;
|
||||||
|
|
||||||
|
// --- State management ---
|
||||||
|
|
||||||
|
function loadState() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(STATE_FILE)) {
|
||||||
|
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
|
||||||
|
}
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
return { checked: [], read_files: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveState(state) {
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(STATE_DIR, { recursive: true });
|
||||||
|
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), 'utf8');
|
||||||
|
} catch (_) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function markChecked(key) {
|
||||||
|
const state = loadState();
|
||||||
|
if (!state.checked.includes(key)) {
|
||||||
|
state.checked.push(key);
|
||||||
|
saveState(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isChecked(key) {
|
||||||
|
const state = loadState();
|
||||||
|
return state.checked.includes(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Sanitize file path against injection ---
|
||||||
|
|
||||||
|
function sanitizePath(filePath) {
|
||||||
|
return filePath.replace(/[\n\r]/g, ' ').trim().slice(0, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Gate messages ---
|
||||||
|
|
||||||
|
function editGateMsg(filePath) {
|
||||||
|
const safe = sanitizePath(filePath);
|
||||||
|
return [
|
||||||
|
'[Fact-Forcing Gate]',
|
||||||
|
'',
|
||||||
|
`Before editing ${safe}, present these facts:`,
|
||||||
|
'',
|
||||||
|
'1. List ALL files that import/require this file (use Grep)',
|
||||||
|
'2. List the public functions/classes affected by this change',
|
||||||
|
'3. If this file reads/writes data files, cat one real record and show actual field names, structure, and date format',
|
||||||
|
'4. Quote the user\'s current instruction verbatim',
|
||||||
|
'',
|
||||||
|
'Present the facts, then retry the same operation.'
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeGateMsg(filePath) {
|
||||||
|
const safe = sanitizePath(filePath);
|
||||||
|
return [
|
||||||
|
'[Fact-Forcing Gate]',
|
||||||
|
'',
|
||||||
|
`Before creating ${safe}, present these facts:`,
|
||||||
|
'',
|
||||||
|
'1. Name the file(s) and line(s) that will call this new file',
|
||||||
|
'2. Confirm no existing file serves the same purpose (use Glob)',
|
||||||
|
'3. If this file reads/writes data files, cat one real record and show actual field names, structure, and date format',
|
||||||
|
'4. Quote the user\'s current instruction verbatim',
|
||||||
|
'',
|
||||||
|
'Present the facts, then retry the same operation.'
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function destructiveBashMsg() {
|
||||||
|
return [
|
||||||
|
'[Fact-Forcing Gate]',
|
||||||
|
'',
|
||||||
|
'Destructive command detected. Before running, present:',
|
||||||
|
'',
|
||||||
|
'1. List all files/data this command will modify or delete',
|
||||||
|
'2. Write a one-line rollback procedure',
|
||||||
|
'3. Quote the user\'s current instruction verbatim',
|
||||||
|
'',
|
||||||
|
'Present the facts, then retry the same operation.'
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function routineBashMsg() {
|
||||||
|
return [
|
||||||
|
'[Fact-Forcing Gate]',
|
||||||
|
'',
|
||||||
|
'Quote the user\'s current instruction verbatim.',
|
||||||
|
'Then retry the same operation.'
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Output helpers ---
|
||||||
|
|
||||||
|
function deny(reason) {
|
||||||
|
const output = {
|
||||||
|
hookSpecificOutput: {
|
||||||
|
hookEventName: 'PreToolUse',
|
||||||
|
permissionDecision: 'deny',
|
||||||
|
permissionDecisionReason: reason
|
||||||
|
}
|
||||||
|
};
|
||||||
|
process.stdout.write(JSON.stringify(output));
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function allow() {
|
||||||
|
// Output nothing = allow
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Main ---
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
let raw = '';
|
||||||
|
try {
|
||||||
|
raw = fs.readFileSync(0, 'utf8').slice(0, MAX_STDIN);
|
||||||
|
} catch (_) {
|
||||||
|
allow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(raw);
|
||||||
|
} catch (_) {
|
||||||
|
allow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolName = data.tool_name || '';
|
||||||
|
const toolInput = data.tool_input || {};
|
||||||
|
|
||||||
|
if (toolName === 'Edit' || toolName === 'Write') {
|
||||||
|
const filePath = toolInput.file_path || '';
|
||||||
|
if (!filePath) {
|
||||||
|
allow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gate: first action per file
|
||||||
|
if (!isChecked(filePath)) {
|
||||||
|
markChecked(filePath);
|
||||||
|
const msg = toolName === 'Edit' ? editGateMsg(filePath) : writeGateMsg(filePath);
|
||||||
|
deny(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
allow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toolName === 'Bash') {
|
||||||
|
const command = toolInput.command || '';
|
||||||
|
|
||||||
|
// Destructive commands: always gate
|
||||||
|
if (DESTRUCTIVE_BASH.test(command)) {
|
||||||
|
deny(destructiveBashMsg());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Routine bash: once per session
|
||||||
|
if (!isChecked('__bash_session__')) {
|
||||||
|
markChecked('__bash_session__');
|
||||||
|
deny(routineBashMsg());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
allow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
allow();
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
117
skills/gateguard/SKILL.md
Normal file
117
skills/gateguard/SKILL.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
---
|
||||||
|
name: gateguard
|
||||||
|
description: Fact-forcing gate that blocks Edit/Write/Bash and demands concrete investigation (importers, data schemas, user instruction) before allowing the action. Measurably improves output quality by +2.25 points vs ungated agents.
|
||||||
|
origin: community
|
||||||
|
---
|
||||||
|
|
||||||
|
# GateGuard — Fact-Forcing Pre-Action Gate
|
||||||
|
|
||||||
|
A PreToolUse hook that forces Claude to investigate before editing. Instead of self-evaluation ("are you sure?"), it demands concrete facts. The act of investigation creates awareness that self-evaluation never did.
|
||||||
|
|
||||||
|
## When to Activate
|
||||||
|
|
||||||
|
- Working on any codebase where file edits affect multiple modules
|
||||||
|
- Projects with data files that have specific schemas or date formats
|
||||||
|
- Teams where AI-generated code must match existing patterns
|
||||||
|
- Any workflow where Claude tends to guess instead of investigating
|
||||||
|
|
||||||
|
## Core Concept
|
||||||
|
|
||||||
|
LLM self-evaluation doesn't work. Ask "did you violate any policies?" and the answer is always "no." This is verified experimentally.
|
||||||
|
|
||||||
|
But asking "list every file that imports this module" forces the LLM to run Grep and Read. The investigation itself creates context that changes the output.
|
||||||
|
|
||||||
|
**Three-stage gate:**
|
||||||
|
|
||||||
|
```
|
||||||
|
1. DENY — block the first Edit/Write/Bash attempt
|
||||||
|
2. FORCE — tell the model exactly which facts to gather
|
||||||
|
3. ALLOW — permit retry after facts are presented
|
||||||
|
```
|
||||||
|
|
||||||
|
No competitor does all three. Most stop at deny.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Two independent A/B tests, identical agents, same task:
|
||||||
|
|
||||||
|
| Task | Gated | Ungated | Gap |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| Analytics module | 8.0/10 | 6.5/10 | +1.5 |
|
||||||
|
| Webhook validator | 10.0/10 | 7.0/10 | +3.0 |
|
||||||
|
| **Average** | **9.0** | **6.75** | **+2.25** |
|
||||||
|
|
||||||
|
Both agents produce code that runs and passes tests. The difference is design depth.
|
||||||
|
|
||||||
|
## Gate Types
|
||||||
|
|
||||||
|
### Edit Gate (first edit per file)
|
||||||
|
|
||||||
|
```
|
||||||
|
Before editing {file_path}, present these facts:
|
||||||
|
|
||||||
|
1. List ALL files that import/require this file (use Grep)
|
||||||
|
2. List the public functions/classes affected by this change
|
||||||
|
3. If this file reads/writes data files, cat one real record
|
||||||
|
and show actual field names, structure, and date format
|
||||||
|
4. Quote the user's current instruction verbatim
|
||||||
|
```
|
||||||
|
|
||||||
|
### Write Gate (first new file creation)
|
||||||
|
|
||||||
|
```
|
||||||
|
Before creating {file_path}, present these facts:
|
||||||
|
|
||||||
|
1. Name the file(s) and line(s) that will call this new file
|
||||||
|
2. Confirm no existing file serves the same purpose (use Glob)
|
||||||
|
3. If this file reads/writes data files, cat one real record
|
||||||
|
4. Quote the user's current instruction verbatim
|
||||||
|
```
|
||||||
|
|
||||||
|
### Destructive Bash Gate (every destructive command)
|
||||||
|
|
||||||
|
Triggers on: `rm -rf`, `git reset --hard`, `git push --force`, `drop table`, etc.
|
||||||
|
|
||||||
|
```
|
||||||
|
1. List all files/data this command will modify or delete
|
||||||
|
2. Write a one-line rollback procedure
|
||||||
|
3. Quote the user's current instruction verbatim
|
||||||
|
```
|
||||||
|
|
||||||
|
### Routine Bash Gate (once per session)
|
||||||
|
|
||||||
|
```
|
||||||
|
Quote the user's current instruction verbatim.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Option A: Use the ECC hook (zero install)
|
||||||
|
|
||||||
|
The hook at `scripts/hooks/gateguard-fact-force.js` is included in this plugin. Enable it via hooks.json.
|
||||||
|
|
||||||
|
### Option B: Full package with config
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install gateguard-ai
|
||||||
|
gateguard init
|
||||||
|
```
|
||||||
|
|
||||||
|
This adds `.gateguard.yml` for per-project configuration (custom messages, ignore paths, gate toggles).
|
||||||
|
|
||||||
|
## Anti-Patterns
|
||||||
|
|
||||||
|
- **Don't use self-evaluation instead.** "Are you sure?" always gets "yes." This is experimentally verified.
|
||||||
|
- **Don't skip the data schema check.** Both A/B test agents assumed ISO-8601 dates when real data used `%Y/%m/%d %H:%M`. Checking one real record prevents this entire class of bugs.
|
||||||
|
- **Don't gate every single Bash command.** Routine bash gates once per session. Destructive bash gates every time. This balance avoids slowdown while catching real risks.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
- Let the gate fire naturally. Don't try to pre-answer the gate questions — the investigation itself is what improves quality.
|
||||||
|
- Customize gate messages for your domain. If your project has specific conventions, add them to the gate prompts.
|
||||||
|
- Use `.gateguard.yml` to ignore paths like `.venv/`, `node_modules/`, `.git/`.
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- `safety-guard` — Runtime safety checks (complementary, not overlapping)
|
||||||
|
- `code-reviewer` — Post-edit review (GateGuard is pre-edit investigation)
|
||||||
Reference in New Issue
Block a user