Web・iOS・Androidの画面を、読み上げやキーボード操作に対応させ、ラベル、配色、操作対象の大きさなどをWCAG 2.2に沿って設計・点検するスキル。
- アイコンボタンの説明を付けたいとき
- キーボード操作とモーダルの点検
- コントラストや操作対象の大きさの確認
Claudeの初回編集やファイル作成をいったん止め、呼び出し元・データ構造・依頼内容の確認を促すフックを設定し、変更前の調査を組み込むスキル。
PreToolUse fact-forcing gate that denies the first Edit/Write/Bash (including MultiEdit and NotebookEdit) attempt until the agent presents concrete facts (importers, data schemas, verbatim user instruction), then allows retry; A/B-tested at +2.25 quality points. Use when enabling or configuring the GateGuard hook, exempting paths via env vars, or handling first-touch denials.
Claudeがコードを変更する前に調査を促すPreToolUseフック、つまりツール実行前の確認処理を設定します。ファイルごとの初回編集や新規作成をいったん拒否し、呼び出し元、変更する公開関数、データの項目や日付形式、利用者の依頼文を提示させてから再試行へ進めます。破壊的なコマンドでは、変更・削除対象と復旧手順の確認を促します。
複数のモジュールに影響する変更や、決まったデータ形式を扱うプロジェクトに向いています。環境変数でテストや生成ファイルを対象外にしたり、拒否メッセージの詳しさや回数を調整したりする設定も扱います。
付属フックはhooks.jsonで有効にします。別の導入方法としてpip install gateguard-aiとgateguard initも示されています。並列編集では一部だけ適用される場合があるため、拒否後は対象ファイルを読み直して状態を確認します。
この紹介文は、公開されている SKILL.md をもとに AI(Claude Haiku)が作成しました。正確な仕様は下の原文を確認してください。
インストールする前に、エージェントに与えられる指示の中身を確認できます。
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.
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.
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.
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). For code targets the Edit gate asks:
Before editing {file_path}, present these facts:
1. List ALL files that import/require this file (search the tree — Glob/Grep, or find/grep via Bash)
2. List the public functions/classes affected by this change
3. If this file reads/writes data files, show field names, structure,
and date format (use redacted or synthetic values, not raw production data)
4. Quote the user's current instruction verbatim
These four are the full set, asked whenever the change cannot be read with confidence. When it can (see 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.
For code targets the Write gate asks:
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 (search the tree — Glob/Grep, or find/grep via Bash)
3. If this file reads/writes data files, show field names, structure,
and date format (use redacted or synthetic values, not raw production data)
4. Quote the user's current instruction verbatim
Item 3 is dropped when the new content handles no data.
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:
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".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.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.
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:
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/*.test.*, *.spec.*, test_*.py, *_test.py, *_test.go,
or anything under a tests, test, or __tests__ directory.md, .mdx, .txt, .rst, .adoc.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).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: which files
may collapse, and that a sibling must share the first file's class.
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
1. The current user request in one sentence
2. What this specific command verifies or produces
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.
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:
A batch-wide lock is not possible: hooks see tool calls one at a time, so the gate cannot know which calls arrived together.
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.
For each Edit/Write/NotebookEdit target (and each MultiEdit entry) the gate decides in this order; the first rule that applies wins:
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.GATEGUARD_FACT_FORCE_MAX_DENIALS, opt-in) — passes
through once the session's denials have reached the cap.fact_force_denials.Credit, trivial and sibling allows never consume the denial cap.
Some files are too costly to change without the full check, so prior-search credit, the comment or whitespace-only pass, the change profile, sibling collapse, and the denial cap never apply to them: they are denied on first touch exactly as without these rules, and the denial counts as usual. The denial carries one extra line: "Sensitive target: prior-search credit, sibling collapse, and the denial cap do not apply." A target is sensitive, judged on the same lowercased project-relative path as its class and on its real (symlink-resolved) location, when either matches:
.env, .netrc, or .pgpass, starts with .env., ends
in .pem, .key, .p12, or .pfx, or starts with id_rsa, id_ed25519,
id_ecdsa, id_dsa, credentials, or secrets.;auth, authn, authz, security,
secrets, payment, payments, billing, or migrations (whole
segments only: src/author.py, docs/authoring.md, and
lib/paymentutils.py are ordinary);.github/workflows/.The real location is the file's own realpath when it exists, else the
realpath of its nearest existing directory plus the rest of the path, judged
relative to the realpath of the project root, so src/tools -> ../auth makes
src/tools/login.py sensitive. A real location outside the project is judged
on its absolute path. Any error while deciding (a dangling symlink, a file
where a directory should be, a permission error) counts as sensitive.
Destructive and routine shell gates are unaffected.
A file with more than one hard link (nlink > 1, or a symlink to such a file)
is also reachable under another name, which may be sensitive or in another
class, so the path being edited says little about what changes. It gets the
same treatment as a sensitive target — no prior-search credit, change profile,
comment-only pass, sibling collapse, denial cap, or subagent bypass — and its
denial carries "Hard-linked target: prior-search credit, sibling collapse, and
the denial cap do not apply." instead of the sensitive line (a target that is
both says "Sensitive target"). A stat error other than a missing file counts as
hard-linked; a new file that does not exist yet is not.
An Edit, Write, NotebookEdit, or MultiEdit entry is allowed, with an
additionalContext note naming the search, when a qualifying search in the
current human turn already covered the file:
Glob, Grep, LS, and Bash/PowerShell command
segments led by rg, grep, egrep, fgrep, git grep,
git ls-files, find, fd, ls, tree, Get-ChildItem, gci,
Select-String, or sls. Read never counts — Claude Code already
requires a Read before an Edit, so it proves nothing extra.compact_boundary marker),
so searches from before a compaction never count. Tool results, meta, and
sidechain records do not start a turn. Only the transcript tail is read
(last 256 KiB / 2000 lines); when the turn started before that window, the
whole window is the current turn, provided every promptId in it agrees.
Two or more promptIds there mean a turn started through a record the scan
does not recognise, so that window gives no credit. A transcript with no
turn start at all gives no credit.tool_result in the
turn that is not an error (is_error of true or "true", or content
starting with <tool_use_error>). A tool-use id that appears more than once
never counts.tool_use_id is missing or not in the
transcript, the newest assistant message in the turn is excluded instead. A
search without a message id never counts.payment.py is credited by
a search for payment or payment_service, but not by one for
payments. Exclusions are never a stem source. Generic stems (index, main, init, __init__,
utils, util, types, readme, test, tests, config, mod,
lib, setup, app, spec, helpers, common) never match. For a test
target, one test affix is stripped first (.test, .spec, and for Python
and Go test_ or _test), so rg tokenizer src tests credits
tests/tokenizer.test.js; index.test.js still reduces to the generic
index.glob is split on top-level commas;
entries starting with ! are exclusions. In shell segments, rg -g/
--glob/--iglob values starting with !, grep --exclude/
--exclude-dir, fd -E/--exclude, ls -I/--ignore/--hide,
tree -I, git ls-files -x, git :!x/:^x/:(exclude)x pathspecs,
find -name/-path/… under -not or ! or followed by -prune, and
PowerShell -Exclude are exclusions. If an exclusion covers the target
(its glob matches the file name or any directory on its path, or it names
the stem), that search cannot credit the target at all: rg -g '!widget.py' TODO . never credits widget.py, while rg -g '!*.md' widget . still
does. --exclude-from, --ignore-file, and git ls-files -X hide names
the hook cannot see, so such a segment never credits. File-name globs that
restrict a search (Grep glob: '*.py', rg -g '*.py', grep --include,
find -name, tree -P, -Include/-Filter) must match the target's file
name for its stem to count. File types filter the same way: rg -t/
--type and the Grep type include the type's globs, rg -T/--type-not
exclude them, and fd -e/--extension includes *.<ext>, so
rg -t py tokenizer src never credits tokenizer.js. An unknown type name,
--type-add, or --type-clear makes the segment never credit.Glob **/tokenizer.js, find -name tokenizer.js,
rg -g tokenizer.js, Grep glob: 'tokenizer.js') — could only find the
target, so it never credits it. Glob **/tokenizer.* still does.Glob, Grep, and LS only credit a target strictly inside the
directory they searched (their path, else the tool cwd, plus a Glob
pattern's literal leading directories). A Glob pattern is a filter too: it
must match the target's path for the stem to count, so Glob **/tokenizer.py
never credits tokenizer.js. A Glob without glob characters is a
one-file lookup: it names no directory and credits only files directly in
its directory, never the looked-up file itself. A search whose path is the target is not a search.rg, git grep, git ls-files, find, fd, and tree by
default; grep -r/-R/--recursive, ls -R, Get-ChildItem -Recurse)
and not fed by a pipe; otherwise it reads stdin, not the tree. A segment
whose path operands all resolve to the target only reads the target and
does not count (ls src/a.py never credits src/a.py). Commands containing
$( ), backticks, <( ), >( ), or heredocs, or longer than 8192
characters, never credit.cwd; a glob operand
must match the target or a directory above it, and a default git pathspec's
* also matches /), or inside the cwd when it has none:
rg payment docs or grep -rn payment /usr/share/doc never credit
src/payment.py. An input redirection before any operand
(rg payment < file) or a < inside a word reads stdin and never counts.
After cd <dir> in the same command, relative operands resolve against that
directory; after any other directory change, or one in another call of the
turn, only absolute operands count.path plus the pattern's literal prefix (a
pattern that starts with a glob character and has no path names no
directory), a Grep or LS path, or a shell path operand. For shell
operands, the pattern argument of grep-style tools is skipped (unless
-e/-f/--regexp/--file/-Pattern supplied the pattern), values of
value-taking flags (-g, --include, -name, -Filter, …) are skipped,
trailing /, /*, and /** are dropped, ., ~/$ paths, and globs
name no directory, and the operand must be an existing directory at the
time of the check. If any Bash/PowerShell call in the turn changes directory
(cd, pushd, popd, chdir, Set-Location, sl, Push-Location,
Pop-Location), shell directory matching is off for the whole turn; stem
matching still applies. Edit, MultiEdit, and a Write that overwrites an
existing file match by stem only.cwd, then CLAUDE_PROJECT_DIR, then the process directory. Comparison is
case-insensitive for Windows paths only. Shell commands are read with bash
quoting rules, so on Windows an operand credits when spelled C:/x/src;
backslash spellings (C:\x\src, quoted or not) and MSYS spellings
(/c/x/src) do not credit and the write is denied as usual.fact_force_credited is incremented; the denial count and ordinal are not
touched. MultiEdit credits per entry and denies the first uncredited entry
as before.<tool>
<detail>): <why>.", naming the closest such call and why it failed: sent
in the same batch, its filters exclude the file, its search path does not
contain it, it searched piped input, it was a Read or another command
rather than a search, or the file name is too generic to match. The detail
is sanitized and cut to 60 characters; no other transcript text is shown.An Edit whose old and new text differ only in comments and whitespace changes
no behaviour, so the first-touch questions carry no signal. Such an Edit (or a
MultiEdit whose entries for that file are all such changes) of a code,
test, or prose target that is not sensitive passes with the note "Comment
or whitespace-only change to <file>". The file is not marked checked:
the next change to it that alters code meets the normal first-touch gate. The
pass is counted in trivial_allows and never touches the denial count.
It applies only to the extensions listed in Questions fit the change, and only when every entry reads cleanly:
// and /* */ (C family), # (Python), # at the start of
a word (shell), # and <# #> (PowerShell), or REM lines (batch);
C preprocessor lines and batch :: labels are code; directive comments are
code too (shebangs, encoding cookies, # type:, # noqa, # nosec,
// @ts-..., eslint-..., //go:build, // +build, //go:embed, Rust
doc comments, #Requires, shellcheck, NOLINT, NOSONAR, fallthrough
markers and similar); whitespace inside strings, line breaks between code, and
the presence of whitespace between tokens are code (a+b → a + b is not
trivial);r", R", @", $", f-strings, Kotlin $), a string running to
the end of a line, a line comment or code line ending in \, ??/, a
nested or unterminated block comment; in JS/TS any / outside a comment (regex versus
division), JSX-like tags, and -->; in shell scripts heredocs, backticks,
$( inside double quotes, $'...', line continuations and here-strings
(see the design notes for the full list);The edit is also checked against the current file (a regular file of at
most 1 MiB, read by the hook and never stored): old_string must be found as
the Edit tool would apply it, the lines around the change must compare equal
as above, and the text before them must end in plain code, not inside a
template literal, docstring, raw string, heredoc or here-string and not after
a line continuation. A missing, unreadable or larger file, or a Go file that
imports "C", never passes this way.
When several new files are created in one directory, the answers for each would be identical, so only the first is denied:
code, test, or prose. Never collapsed: config and instruction
files, and any path with a segment starting with . (dotfiles and every
dot-directory: .claude, .github, .git, .husky, .devcontainer,
.githooks, .idea, .vscode, …) or an 8.3 short name (CLAUDE~1),
checked on the same project-relative path as the class.src/tools -> .claude/hooks never collapses. Gates are
keyed by the real directory. Any filesystem error means no collapse.<first> (gated earlier at denial #<n> this session)". A code denial never
lets a test, README, or config file through.promptId, else its uuid, else a hash of that record. When
the turn started before the transcript tail, the id is the promptId that
every user record in the window agrees on (Claude Code stamps it on the
prompt and on every tool result of the turn); if they disagree there is no
turn id. Only when no transcript path is available at all does a gate
recorded without a turn id match for 120 seconds; a transcript that is
missing, unreadable, or yields no turn id means no collapse. A turn-scoped
gate never matches a call without a turn id, and vice versa; a gate stamped
in the future is ignored.Checked state is keyed on the canonical path: a relative file_path resolves
against the tool's cwd (where the tool runs), then CLAUDE_PROJECT_DIR,
then the process directory, with / separators, lowercased on Windows only.
a.py, ./a.py, and the absolute path share one first-touch. State files
written by earlier versions (raw keys) are still honoured.
GATEGUARD_EXEMPT_GLOBS keeps its project-relative matching (below).
The per-session state file (under GATEGUARD_STATE_DIR) records these fields.
They record class names only, never paths (except dir_gates, which stores
the sanitized first file per directory):
| Field | Meaning |
|---|---|
fact_force_credited | Number of first touches allowed by prior-search credit |
denials_by_class | Denials per target class (code, prose, test, …) |
credited_by_class | Credits per target class |
sibling_allows | Number of Writes allowed by sibling collapse |
dir_gates | Per class and directory, the denial that opened a sibling window (turn, time, first file, ordinal); capped at 50 entries |
cap_allows | Number of first touches passed through by the denial cap (GATEGUARD_FACT_FORCE_MAX_DENIALS); these never count as denials |
trivial_allows | Number of comment or whitespace-only edits passed without the first-touch check; these never mark the file checked |
routine_readonly_passes | Number of read-only shell commands passed before the routine shell gate fired; these never mark it checked |
Missing or malformed fields load as empty or zero, so older state files keep working.
GATEGUARD_FACT_FORCE_MAX_DENIALS (unset means no cap) and
GATEGUARD_METRICS (unset means no metrics). The existing controls are
unchanged.permissionDecision: "allow", so other hooks
and permission rules still apply.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.
In long sessions, only the first GATEGUARD_FACT_FORCE_FULL_DENIALS
fact-force denials (default 3) emit the full fact block; later
denials are condensed to a single line carrying the denial ordinal, so
near-identical blocks cannot accumulate in the context window and
amplify model repetition loops (#2142). Retrying the same file or
command after presenting facts never re-triggers the gate. That is a
message budget; to stop denying new files after a number of denials, set
the opt-in denial budget GATEGUARD_FACT_FORCE_MAX_DENIALS (below).
ECC_GATEGUARD=off (or GATEGUARD_DISABLED=1) turns the gate off entirely.
The variables in this table do not — each narrows one behaviour while the
load-bearing destructive-Bash checks keep running:
| Variable | Default | Effect |
|---|---|---|
GATEGUARD_BASH_ROUTINE_DISABLED | unset (gate on) | Disables the routine-Bash gate only. The destructive-Bash gate (rm -rf, git reset --hard, drop table, dd if=, …) is unaffected. |
GATEGUARD_EXEMPT_GLOBS | unset (no exemptions) | Comma-separated globs; a matching Edit/Write/MultiEdit/NotebookEdit target skips first-touch fact-forcing, even when it is sensitive or hard-linked. Intended for low-import-value trees (tests, generated artifacts, scratch dirs) where "who imports this / what schema" carries no signal. |
GATEGUARD_FACT_FORCE_FULL_DENIALS | 3 | How many denials emit the full fact block before later ones condense to a single line. 0 condenses from the very first denial. |
GATEGUARD_FACT_FORCE_MAX_DENIALS | unset (no cap) | A denial budget: caps how many first-touch Edit/Write/MultiEdit/NotebookEdit denials a session draws. Once that many denials have been issued, further new paths pass through instead of being denied (counted in cap_allows, not as denials); 0 passes from the first. Prior-search credit and sibling collapse run first and never use it up, and sensitive and hard-linked targets are always denied. Destructive and routine Bash stay gated. The cap is best effort when hooks run concurrently: a lost count update can add a denial, but never lets a new path through early. Opt-in: unset, or any value that is not a whole non-negative integer (surrounding spaces allowed), keeps the deny-every-new-path behaviour; a malformed value is reported once on stderr (ignoring malformed GATEGUARD_FACT_FORCE_MAX_DENIALS=…; the denial cap is not active.). Unlike GATEGUARD_FACT_FORCE_FULL_DENIALS, a message budget that only changes how much text a denial carries, this changes whether the operation is blocked. Condensed denials name this variable. |
GATEGUARD_BASH_EXTRA_DESTRUCTIVE | unset | Extra destructive-command patterns, as regex source, added to the built-in set. A malformed regex is treated as unset (built-ins still apply) and logged once to stderr. |
GATEGUARD_STATE_DIR | ~/.gateguard | Where per-session gate state is kept. If state cannot be persisted the gate allows the operation rather than looping, and names this variable in the warning. |
GATEGUARD_METRICS | unset (off) | Records one line per Edit/Write/MultiEdit/NotebookEdit/Bash/PowerShell decision in <GATEGUARD_STATE_DIR>/metrics.jsonl (see Decision metrics). Never changes a decision. |
GATEGUARD_EXTERNAL_CONTRACT_QUESTION | unset (off) | Adds a code question after the data-schema question: name what outside the repository decides the format, units, timezone or protocol semantics. Experimental: in hidden-intent trials it did not change outcomes (docs/gateguard/question-effectiveness.md). Never changes a decision. |
GATEGUARD_BASH_ROUTINE_DISABLED, GATEGUARD_METRICS and
GATEGUARD_EXTERNAL_CONTRACT_QUESTION accept 1, true, on, enabled,
enable, or yes (case- and whitespace-insensitive); any other value leaves
the routine gate on, metrics off, and the question unasked.
With GATEGUARD_METRICS=1, each gate decision appends one JSON line to
metrics.jsonl in the state directory (a MultiEdit call writes one line per
file it decided):
{"v":1,"ts":"2026-09-01T10:02:00.000Z","session":"3f9c0a1b2d4e","tool":"Edit","class":"code","decision":"deny","reason":"near-miss:out-of-scope","questions":["local-callers","quote-instruction"],"sensitive":false,"profile":{"known":true,"language":"js","touchesPublicSurface":false,"touchesData":false,"trivial":false}}
| Field | Meaning |
|---|---|
v | Schema version (1) |
session | First 12 hex characters of the sha256 of the session key |
tool | Edit, Write, MultiEdit, NotebookEdit, Bash or PowerShell |
class | Target class, or null for shell commands |
decision | deny, credit, sibling, cap, trivial, pass-checked, pass-exempt, pass-subagent, routine-deny, routine-readonly, destructive-deny or pass |
reason | Short code: first-touch, sensitive, hard-linked, near-miss:<code>, subagent-sensitive, subagent-hard-linked, prior-search, comment-whitespace, same-turn-dir, max-denials, checked, exempt-glob, claude-settings, no-path, subagent, readonly-git, readonly, first-command, routine-checked, routine-disabled, destructive, destructive-retry, state-error |
questions | Question ids of a first-touch denial; null otherwise |
sensitive | Whether the target is a sensitive target |
profile | Change-profile flags when the profile was computed; null otherwise |
Privacy: lines never contain paths, commands, file content, transcript
text or the session id itself; every text field must be a short lowercase
code or is written as null. Metrics errors are ignored. Past 1 MiB the file
is renamed to metrics.jsonl.1, replacing the previous one.
Summarise the metrics with:
node scripts/gateguard-report.js # GATEGUARD_STATE_DIR or ~/.gateguard
node scripts/gateguard-report.js --dir /path/to/state --json
It reads metrics.jsonl.1 then metrics.jsonl, skips malformed lines, and
prints, per session and in total: decisions by type, denials by class and
reason, the share of first-touch denials that followed a near-miss search,
the rates of denial, credit, sibling collapse, cap and trivial edits among
first touches, routine read-only passes, and the most-asked question ids.
| Variable | Effect |
|---|---|
ECC_GATEGUARD=off | Disables GateGuard for the session. Accepts 0, false, off, disabled, or disable. |
GATEGUARD_DISABLED=1 | Same effect. Recognises 1 only — the spellings above do not apply here. |
For hook-level control, keep using ECC_DISABLED_HOOKS with the GateGuard hook ID.
GATEGUARD_EXEMPT_GLOBSPatterns match the entire project-relative target path. The project root is
CLAUDE_PROJECT_DIR, falling back to the hook payload's cwd, then the hook
process working directory. Relative globs never exempt targets outside that
root. Explicit absolute globs match the entire absolute target path and may
deliberately exempt paths outside the project.
Both patterns and paths use / separators and lowercase matching. * matches
within a segment, ** across segments, and ? one non-separator character.
**/ includes zero directories, so **/tests/** also matches tests/foo.js.
Malformed patterns are dropped without granting an exemption.
An exemption applies before every other rule, including the sensitive and
hard-link rules (see Order of checks).
Since 2.2.1, services/** only covers the project's root services tree, and
*.md only covers its root Markdown files. Use **/*.md for all Markdown
files within the project. Existing unanchored exemptions may need adjustment:
{
"env": {
"GATEGUARD_BASH_ROUTINE_DISABLED": "1",
"GATEGUARD_EXEMPT_GLOBS": "**/tests/**,tests/**,**/*.test.*,**/docs/**,**/dist/**"
}
}
pip install gateguard-ai
gateguard init
This adds .gateguard.yml for per-project configuration (custom messages, ignore paths, gate toggles).
%Y/%m/%d %H:%M. Checking data structure (with redacted values) prevents this entire class of bugs..gateguard.yml to ignore paths like .venv/, node_modules/, .git/.safety-guard — Runtime safety checks (complementary, not overlapping)code-reviewer — Post-edit review (GateGuard is pre-edit investigation)まだレビューはありません。使ってみた感想をお寄せください。
概要と使いどころ
Web・iOS・Androidの画面を、読み上げやキーボード操作に対応させ、ラベル、配色、操作対象の大きさなどをWCAG 2.2に沿って設計・点検するスキル。
AIエージェントの不調を、指示・記憶・ツール実行・画面表示など12の層から調べるスキル。コードやログを根拠に原因を整理し、重要度順の指摘と修正案をまとめます。
実際の開発課題で複数のコーディングエージェントを比較するスキル。成功率、取得可能なAPI費用、所要時間、繰り返し実行の安定性を測り、選定や更新後の評価に使えます。
AIエージェントが使うツールの種類や入出力、エラーからの復帰手順を設計・見直します。文脈の情報量も整理し、作業完了率や再試行回数で改善を評価します。
AIエージェントが失敗や同じ操作を繰り返す原因を、エラーと実行状況から整理します。小さな復旧操作を試し、結果と根拠を引き継げる報告にまとめるスキルです。
AIエージェントの失敗や同じ操作の繰り返しを記録し、原因の切り分け、小さな復旧操作、結果の報告まで進める手順を示して、根拠のある再試行につなげるスキル。