gateguard

Fact-forcing gate that blocks Edit/Write/Bash (including MultiEdit) and demands concrete investigation (importers, data schemas, user instruction) before allowing the action. Measurably improves output quality by +2.25 points vs ungated agents.

By affaan-m · 2,823 installs

npx skills add affaan-m/ecc --skill gateguard

Source repository · Upstream listing

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: 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 / MultiEdit Gate (first edit per file) MultiEdit is handled identically — each file in the batch is gated individually; entries without their own file path are gated as the call's file path . NotebookEdit is gated as an Edit of its notebook path : same class, checked key, credit and cap rules, the full questions (the cell change is not read), and never a comment only pass. The questions depend on the target's class (see [Questions by target class]( questions by target class)). For code targets the Edit gate asks: These four are the full set, asked whenever the change cannot be read with confidence. When it can (see [Questions fit the change]( questions fit the change)), an edit that touches no public declaration asks instead for the call sites in the file or its module that rely on the changed behaviour, and item 3 is dropped when the change handles no data. Write Gate (first new file creation) For code targets the Write gate asks: Item 3 is dropped when the new content handles no data. Questions fit the change For code targets the gate reads the change it is about to allow (Edit old string / new string , every MultiEdit entry for the file, Write content ) and decides three things: Public surface: a line on either side declares or exposes a public name. JS/TS: export , exports , public , declare ( .d.ts files always); Python: def / class not starting with , dunders, all , a module level public assignment such as BASE URL = ... ( init .py always); Go: capitalised func / type / var / const names, package , lines starting with a capital; Rust: pub , impl , trait , extern , [macro export] , [derive] , [repr] , [no mangle] ; shell: function definitions, export , declare x ; PowerShell: function , filter , param , Export ModuleMember , [CmdletBinding . A member of an exported container counts too, judged from the file: lines inside an exported interface, enum, type, object or export list, member declarations of an exported class, all entries, public class attributes and dataclass fields, and members of a pub enum, struct, trait or use list. Java, Kotlin, C , C, C++ and batch files, and every Write, always count as touching it. Touching it keeps questions 1 and 2; otherwise both are replaced by "List the call sites in this file or its module that rely on the changed behaviour". Data: words for file I/O, formats ( json , csv , yaml , …), serialisation, encodings, schemas, SQL, data stores, browser storage, dates and clock reads on either side; in shell scripts also redirection to a file, curl , wget , jq and PowerShell file and web cmdlets. Without them the data question is dropped. With GATEGUARD EXTERNAL CONTRACT QUESTION on, a change that keeps the data question is also asked what outside the repository decides its format or semantics. Trivial: see [Comment and whitespace only edits]( comment and whitespace only edits). Supported extensions: .js .mjs .cjs .jsx .ts .tsx .mts .cts .py .pyi .go .rs .java .kt .kts .cs .c .h .cc .cpp .cxx .hpp .hh .hxx .sh .bash .zsh .ps1 .psm1 .bat .cmd . Any other extension, a missing or non string field, more than 64 MultiEdit entries, a side over 64 KiB, or 256 KiB in total means the full questions, word for word: the original four (importers, public API, data schemas, the user's instruction), or five with GATEGUARD EXTERNAL CONTRACT QUESTION on. Sensitive and hard linked targets and NotebookEdit calls always get the full questions. Other classes keep their class questions. Each question has a stable id ( importers , public api , local callers , callers , no duplicate , data schema , external contract when opted in, quote instruction , and one per class question), listed in docs/gateguard/change profile.md . Questions by target class The code questions carry no signal for a SKILL.md , a README, a test, or a YAML file, so the first touch gate classifies the target first. Classification uses the target's path relative to the project root ( CLAUDE PROJECT DIR , then the payload cwd , then the process directory), so a project that lives under, say, ~/work/tests/ does not turn every file into a test. A leading .claude/worktrees/<name / is stripped, since a worktree mirrors the project, but only when <root /.claude/worktrees/<name /.git exists; otherwise the path stays under .claude . On Windows style paths (and any segment containing ::$ ), trailing dots/spaces and a :stream suffix are dropped first, so CLAUDE.md. and CLAUDE.md::$DATA classify as CLAUDE.md . Targets outside the root are classified by their absolute path. The path is lowercased and / separated, and the first match wins: 1. instruction — basename CLAUDE.md , AGENTS.md , AGENT.md , GEMINI.md , SKILL.md , copilot instructions.md , .cursorrules , or .windsurfrules ; any .mdc file; a .md / .mdx / .txt file under a .claude , agents , commands , skills , rules , hooks , .cursor , .codex , or .opencode directory; or a instructions .md file under .github/ 2. test — .test. , .spec. , test .py , test.py , test.go , or anything under a tests , test , or tests directory 3. prose — .md , .mdx , .txt , .rst , .adoc 4. config — .json , .jsonc , .yaml , .yml , .toml , .ini , or a .env / .env. file that does not end in a code extension ( .env.local is config; .env.example.ts and .envrc are code) 5. code — everything else (a .js under hooks/ or skills/ is code) Every class ends with "Quote the user's current instruction verbatim", keeps the batch sibling warning, and uses the same questions for Edit, MultiEdit, and Write unless noted. Items marked (search) carry the suffix "(search the tree — Glob/Grep, or find/grep via Bash)". Class Questions instruction Name the harness/loader that reads this file (Claude Code, Codex, Cursor, OpenCode, …) and when it loads it · Describe what agent behaviour changes as a result · Confirm no existing instruction, skill, or agent file already covers this (search) test Name what behaviour is under test and which module/function it exercises · Name the existing test file(s) covering this module, or confirm none exist (search) prose (Write) Name any existing doc this supersedes or duplicates (search) · State where it will be linked or referenced from · Explain why a new file rather than editing an existing one prose (Edit) List other docs or code that reference the section being changed (search) · State what the change corrects or adds config Name which process/tool reads this file and when · Describe the effect of the change · Confirm no secrets or credentials are being written in plain text Condensed denials (after GATEGUARD FACT FORCE FULL DENIALS ) carry a one line hint for the same class. For code targets the hint names the same questions as the full denial, one short phrase each; an Edit whose change cannot be read keeps the fixed hint (importers/callers, affected API, data schemas if any) when the external contract question is off. Opting in adds its external contract phrase. Besides the message text, the class only decides [sibling collapse]( same turn sibling creation collapse): which files may collapse, and that a sibling must share the first file's class. Destructive Bash Gate (every destructive command) Triggers on: rm rf , git reset hard , git push force , drop table , etc. Routine Bash Gate (once per session) Read only introspection does not use this gate up: until it fires, a command whose every ; , , && or segment is ls , pwd , cat , head , tail , wc , grep , rg , find , fd , tree , or git status / log / diff / show / ls files / rev parse / branch with read only options (in PowerShell: Get ChildItem , Get Content , Select String , Get Location , their aliases, rg , git ) passes and is counted in routine readonly passes . The first other command is still gated. Any redirection, tee , substitution, backtick, variable, environment assignment, wrapper ( env , sudo , xargs , sh c ), unknown command, find exec / delete , fd x , rg pre / z , tree o , git global option or write/execute option, or PowerShell $ , @ , ( , { or % makes the command ordinary. Destructive detection runs first, unchanged. Session state expires after 8 idle hours when it is keyed by a session id or transcript path, and after 30 idle minutes when it falls back to the project directory. Parallel Batches and Partial Application The first touch gate evaluates each tool call independently. When several edits to a file that has not been touched yet are sent in one parallel batch, the first call is denied and the denial marks the file as checked, so the sibling edits in that batch are applied. Nothing is rolled back: the file can end up holding the sibling edits without the denied one. The denial message names the file and warns that batch siblings may already have been applied. Treat it literally: Send dependent edits to a not yet touched file sequentially, not in a parallel batch. A definition and its first use, or an import and its call site, must not ride in the same batch. After a first touch denial, present the facts, retry the denied edit, and re read the file before building on anything else from the batch. A batch wide lock is not possible: hooks see tool calls one at a time, so the gate cannot know which calls arrived together. What Counts as Already Done A first touch denial is meant to force investigation. When the transcript shows the investigation already happened, or the answers would repeat the previous denial word for word, the gate lets the call through with a note instead of a denial. Each rule below falls back to the normal denial when anything is ambiguous. Order of checks For each Edit/Write/NotebookEdit target (and each MultiEdit entry) the gate decides in this order; the first rule that applies wins: 1. Exempt ( GATEGUARD EXEMPT GLOBS , Claude settings files) — allowed. Operator exemptions are authoritative: they win over every later rule, including the sensitive and hard link rules, so a glob that covers src/auth/ or a hard linked file turns its check off. Exempt only paths that may be edited without the check. 2. Subagent call — allowed (the parent session was already gated), except a [sensitive]( sensitive targets) or [hard linked]( hard linked targets) target: denied once per path unless the parent already gated it. A subagent's retry does not unlock the path fo