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