@devrik-tools/claude-gates
v1.2.1
Published
49 installable, deterministic gates (hooks) for Claude Code: block destructive commands and protected paths, enforce delegation/spec/quality/research rules, and track tasks that only close with verified evidence. Configurable per project.
Maintainers
Readme
claude-gates
Installable, deterministic gates (hooks) for Claude Code. They run automatically before Claude executes a tool and block or warn when something breaks a rule: a destructive command, a write to a protected file, a delegation with no brief, and more. Everything is configurable per project and can be turned on/off whenever you want.
The core idea: instead of trusting the model to remember the rules, a deterministic
hook enforces them. A git reset --hard does not run because the model chose to behave —
it is blocked because a gate intercepts it.
Install
Two steps. The plugin wires the hooks into Claude Code; the CLI picks which gates run and with what configuration.
# 1. Register the marketplace and install the plugin (wires the hooks)
claude plugin marketplace add https://github.com/DevRik99/claude-gates
claude plugin install gates@claude-gates
# 2. Choose which gates to adopt (interactive menu) — or use npx without cloning
npx @devrik-tools/claude-gates initRestart the Claude Code session (or run /plugin) so the hooks load.
Why two things? The plugin always ships all 50 gates; the config decides which ones run. So you can turn one on without reinstalling — it is one line in a JSON file.
How it works (the model in 30 seconds)
Claude is about to use a tool (Write, Bash, Agent…)
│
▼
Claude Code fires the PreToolUse hooks in parallel
│
▼
Each gate that applies (by its "matcher") runs in its own process:
· reads the payload · reads its project config
· if off → exits · if the rule is broken → DENY (block) or WARN (advise)
│
▼
If any gate blocks, the tool does not run.- DENY: the action is deterministically wrong → it is blocked.
- WARN: the action needs judgment → the gate injects a note and lets it proceed.
- Silence: the common path. A gate never nags when there is nothing to object to.
Every DENY/WARN message is prefixed [configKey] — the exact key name to look up (or turn
off/tune) under "gates" in .ai/config.json, e.g. [blockPathsOutsideRootWhitelist] '...'
is not on the whitelist. means the setting is blockPathsOutsideRootWhitelist in the table
below, not some other name you'd have to guess or map from a short gate id.
Every gate is self-contained (Node built-ins only, no runtime dependencies), so it works even if you install one on its own.
The gates (50, in 11 families)
[on] = enabled by default; [off] = enable it if you want it.
🔒 Security — hard blocks on destructive actions
| Gate | | What it does |
| ---------------------- | --- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| bash-commands | on | Blocks git reset --hard, rm -rf over protected areas, force push, and killing processes by name. |
| block-remote-publish | on | Blocks git push, gh pr merge, gh release create without authorization. Set blockRemotePublish: false to let the agent publish on its own. |
| protected-paths | on | Blocks writes to .env, lockfiles and the harness itself. |
| root-whitelist | on | Blocks new root-level files/folders outside a whitelist. |
| no-blocking | off | Blocks sleep, tail -f, polling loops and foreground servers. |
| require-monitor | on | Blocks a background command (run_in_background: true) that does not declare its monitor with a MONITOR-PLANNED: marker, and blocks further execution while an unmonitored background command is still pending. |
🤝 Delegation — requirements on the brief when delegating to a subagent
| Gate | | What it does |
| ----------------------- | --- | ---------------------------------------------------------------------------------------------------------------------- |
| brief-before-delegate | off | Requires goal, steps and done-when criteria in the prompt. |
| intent-flow | off | Requires IN SCOPE / OUT OF SCOPE / EDGE CASES sections. |
| risk-level | off | Requires a declared level (QUESTION/MICRO/STANDARD/HIGH-RISK). |
| circuit-breaker | off | Cuts the same delegation retried without real changes. |
| no-memory-dependency | off | Blocks a brief that relies on the subagent "remembering" the chat (add memory-not-needed to allow a false positive). |
| force-parallel | on | Blocks the Nth consecutive sequential delegation inside a time window: independent delegations must be launched together in one message (add SEQUENTIAL-JUSTIFIED when the second one really depends on the first). |
📋 Spec-driven flow — only relevant if the project adopted spec-driven development
| Gate | | What it does |
| ------------------------- | --- | -------------------------------------------------------------------- |
| feature-catalog | on | A single feature in progress; closing requires asserts and review. |
| sdd-specs | off | Requires non-empty requirements/design/tasks before implementing. |
| brief-approved | on | Requires a recorded user approval (status: approved + quote) on the cited feature's brief before implementing. |
| implementation-pipeline | off | Requires declaring definition → writing → validation → QA → closure. |
| mandatory-flow | off | Requires an active task with a contract before implementing. |
| test-matrix | off | Requires a test matrix (the types the requirement makes mandatory). |
✨ Quality — code hygiene, diagnosis and language
| Gate | | What it does |
| --------------------------- | --- | -------------------------------------------------------------------------------------------------------------- |
| dependency-skills | on | Blocks a new direct dependency with no matching skill (declare it in depsWithoutOwnApi if it needs none). |
| root-cause-first | off | Requires an origin→symptom diagnosis before a patch. |
| never-assume | off | Flags unverified assumptions in briefs and code. |
| rule-skill-autodiscovery | off | Loads gates the project declares in its rules/ and skills/. |
| recurrence-lock | on | A second occurrence of a defect class requires its deterministic block. |
| test-after-implementation | off | Blocks a test written after its paired implementation (add test-after-impl:allow for a regression test). |
| adversarial-tests | off | Blocks a test file with no checkable adversarial evidence: an ATTACK MATRIX row per category (boundary, invalid input, missing/empty, invalid state, dependency failure, idempotency/order, invariant, security), a case in the file backing each COVERED row, and the mutations the suite kills. requiredCategories adds three more rows the gate can corroborate but does not demand by default: decision-table, metamorphic, partial-write. claude-gates tests [paths…] re-runs the same check over files already on disk. Add adversarial-tests:allow in a comment for a deliberate exception. |
| no-reconfirm | on | Never re-ask what you already answered. |
| neutral-spanish | on | Blocks voseo or regional lexicon in written text (add neutral-spanish:allow for a deliberate quote/fixture). |
| diagnosis-before-patch | on | Warns when timeouts/retries change without evidence. |
| lint-commit | off | Blocks git commit while the project's lint script fails (autodetects npm run lint; silent if none). |
| staged-lint | off | Blocks git commit when the staged files fail lint — lints only what you staged, so your change can't add new lint debt while pre-existing debt in untouched files never blocks you. Add [skip-lint] for a deliberate exception. |
| atomic-commit | off | Blocks a git commit that is not atomic — one mixing more than N natures of change (code/tests/deps/config…) or staging more reviewable files than a commit should carry. Docs/assets/generated aren't counted. Add [wip] for a deliberately broad commit. |
| no-coauthor | on | Blocks a git commit carrying an AI/agent attribution trailer (Co-Authored-By, Generated with, a session trailer). Add [allow-coauthor] for one legitimate co-author. |
| no-lint-suppression | on | Blocks a write that silences the linter/type-checker (eslint-disable, @ts-ignore, a rule set to off) instead of fixing the code. Add lint-ok: <reason> on the same line for a documented false positive. |
| no-explanatory-comments | on | Blocks a code write that adds comments narrating what the code does. Only decision comments (why, trade-off, limitation), tool directives, TODO/FIXME and typed JSDoc tags pass. Judges only new comments (diff against disk). Add comment-ok: <reason> for a documented exception. |
| no-trivial-scripts | on | Blocks an inline interpreter script that does a file operation the Edit/Write tool handles directly (node -e with writeFileSync, python -c with open(…, 'w'), sed -i, perl -i, Set-Content/Add-Content). Inline scripts that only compute are not caught. |
🔎 Tool discovery — don't reinvent the wheel
| Gate | | What it does |
| -------------------- | --- | ------------------------------------------------------------------------------------------------------------ |
| reuse-before-build | on | Before building a tool, consults the project tool map; blocks if you did not audit (local → Context7 → web). |
| tool-map | off | Records discovered tools in .ai/tool-map.json so exploration is not repeated. |
| skill-first | off | Blocks a write/command/delegation an available skill plausibly covers until the question was asked: load the skill, or say why it does not fit (no skill covers this / using the <name> skill). Reads the same catalog capability-map injects. |
🧠 Research flow — memory first, never guess a library
| Gate | | What it does |
| -------------- | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| engram-first | on | Blocks WebSearch, WebFetch and context7 until a mem_search ran in the session (engram is the first source); blocks the Stop event while research happened with no later mem_save; at session start warns once when engram cloud is configured but the project is not enrolled. |
| library-docs | on | Blocks a write that imports a package the project does not use anywhere yet unless this session looked it up: an engram hit about it, or context7 docs followed by a mem_save. Never guess a library API. |
🏭 Forge pipeline — enforces the forge workflow
| Gate | | What it does |
| ------------ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| forge-flow | off | In a project that adopted forge, blocks editing/running unless an active forge run exists. Closes the hole the MCP cannot: it forces you through the pipeline. |
🤖 Autonomy — let an unattended run decide instead of asking
| Gate | | What it does |
| ------------------ | --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| autonomous-mode | off | With autonomous mode on, blocks AskUserQuestion and blocks a turn that ends only to wait: it re-injects, once per cycle, the instruction to decide and proceed, leaving pending only what genuinely needs the user. |
⏹️ Completion — a turn does not end with work still open
| Gate | | What it does |
| -------------------- | --- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| stop-pending | on | Blocks the Stop event while this project has active tasks (open/in_forge); lists them and how to close them with verified evidence or abandon them. blocked tasks don't hold the turn by default. |
| require-task-split | on | Blocks writes and execution while an active task larger than small has no sub-tasks registered: split it into independently verifiable pieces first (task add --parent <id>). |
🗂️ Task tracking — the tasks plugin
| Gate | | What it does |
| ----------------------------- | --- | ---------------------------------------------------------------------------------------------------------- |
| remind-open-tasks | on | Makes the assistant classify and register new work via the CLI, and recites the active tasks every N messages. |
| list-tasks-on-session-start | on | Lists this project's active tasks when a session opens. Silent when there are none. |
🩺 Session & context — startup checks and capability injection
| Gate | | What it does |
| ---------------- | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| doctor | on | On session start, runs the environment validator and only speaks on failure. |
| ask-adoption | on | In a project that never answered, makes the assistant ask what to adopt. |
| wiring-check | on | Warns when a registered hook is missing or a script is orphaned. |
| capability-map | on | Every injectEveryMessages messages (default 10; always on the first run and whenever a capability is added/removed), injects the project's capability catalog — skills, agents/subagents, commands — as compact data, and persists it to .ai/capability-map.json (like the tool map). Autosynced from disk. Never blocks. |
Configuration: turn gates on, off, and tune them
Everything lives in one file. Project overrides global:
- Project:
<root>/.ai/config.json - Global (fallback):
~/.claude/claude-gates/config.json
init writes the selection there and materializes each gate's default values, so you
see and edit every knob:
{
"adopted": "partial",
"gateVersion": "3.1.0",
"gates": {
"blockDestructiveShellCommands": {
"enabled": true,
"rmRfProtectedAreas": ["/", "*", "src", "tests"],
"denyPatterns": ["git reset --hard", "…"]
},
"blockRemotePublish": { "enabled": false },
"warnNonNeutralSpanish": {
"enabled": true,
"regionalMarkers": ["tenés", "podés", "…"],
"escapeHatch": "neutral-spanish:allow"
},
"requireBriefBeforeDelegating": { "enabled": false }
}
}- Turn a gate off:
"enabled": false. Off instantly, no reinstall. Or from the CLI:claude-gates disable <gate>/claude-gates enable <gate>(gate ids, config keys, family ids orall;--projector--global), andclaude-gates statusto see what is on for the current directory and where each value comes from. - Re-running
initmerges, never overwrites. A gate already in the file only changes when you name it (--gates,--families,--all,--none) and, when itsenabledwould flip, you confirm it gate by gate. With--yes(no TTY) the existing values are kept and reported; pass--forceto apply them without asking. - A wrong-typed param never breaks a gate. A string where a list is expected, or a malformed regex, falls back to the built-in default and is reported once per session in the gate's own message (and in the decision log).
- Tune its behavior: edit its parameters (the whitelist, the patterns, the thresholds). What the project declares replaces the gate's default.
- A gate absent from the config uses its catalog default. Keys you already had in the file
(e.g.
autoCommit) are kept intact. - Let the agent push: set
"blockRemotePublish": { "enabled": false }. Nothing is hardcoded — every gate, remote publish included, obeys this flag. - Escape hatches: a few gates block (deny) but accept an explicit opt-out marker in the
content/prompt for a legitimate case:
neutral-spanish:allow(a deliberate regional quote),test-after-impl:allow(a regression test),memory-not-needed(a non-memory phrase),[allow-coauthor](one legitimate co-author on a commit),lint-ok: <reason>(a documented linter false positive),[skip-lint](skip the staged-lint check for one commit),[wip](allow one deliberately broad, non-atomic commit),comment-ok: <reason>(one explanatory comment that must stay),SEQUENTIAL-JUSTIFIED(a delegation that genuinely depends on the previous one),MONITOR-PLANNED:(the background command declares how it will be monitored).dependency-skillsopts out via itsdepsWithoutOwnApilist.skill-firstclears on one sentence in the content/prompt:no skill covers this, orusing the <name> skill. - Capability injection:
capability-map(on by default) is fully tunable — pick which kinds to surface ("kinds": ["skills", "agents", "commands"]), cap each blurb (maxClauseChars, default 120), add extra roots per kind, throttle how often the full catalog is re-injected (injectEveryMessages, default 10 — the persisted map file itself still refreshes every message; the catalog is ALSO re-injected mid-throttle whenever the prompt's kind of work changes, e.g. debugging → releasing, which you can turn off with"reinjectOnWorkNatureChange": false), or turn off persistence ("persist": false) and point the map file elsewhere (mapFile). Skills are also scanned by default under~/.agents/skills,<project>/.agents/skills,~/.ai/skillsand<project>/.ai/skills(skill-only roots some installers use besides.claude/skills— no config needed), in addition to anyextraSkillsDirsthe project declares. A description too long to fitmaxClauseCharsfalls back to mechanical word-boundary truncation, but you can hand-write a better one-line summary per capability in~/.claude/blurb-overrides.json(global) or<project>/<blurbOverridesFile>(default.ai/blurb-overrides.json, project wins per key) — a{ "skill-name": "short summary" }map, used verbatim instead of the mechanical cut. A capability's re-scan is skipped (its blurb reused verbatim) whenever disk is unchanged since the last scan (same set of source files, same mtimes); removing a skill/agent/command drops its entry from the map on the very next run.
Generated artifacts
Everything an agent produces that is not source code — a deterministic check it wrote so you can see whether something works, an audit of what already exists, a note explaining what it did — has one shape and one home, so it can be found by convention and validated in CI instead of landing wherever the model felt like.
claude-gates new check my-claim --title "..." --source "what asked for this"| Kind | Lives in | For |
| ------- | ------------- | ------------------------------------------------------------------ |
| check | .ai/checks/ | One verifiable claim: the command, what it must print, the evidence it actually printed. |
| audit | .ai/audits/ | Looking before building: searched / exists / missing / decision. |
| note | .ai/notes/ | Reasoning that would otherwise live only in a chat log. |
When the same thing goes wrong twice, the record is data rather than a document: register
the defect class in .ai/reincidencias.json, which recurrence-lock reads to block
mutating work until the class is closed at the root.
The contract is cli/artifacts.mjs; the full rules are in
.ai/README.md. Two tests keep it honest — one covers the module, the
other walks this repo's own .ai/ tree and fails on any artifact that drifted (missing
front matter, wrong directory, a missing section, or a check marked passed whose
evidence is still the generated placeholder).
Decision log
Every deny, warn and Stop-block is appended as one JSON line to
<root>/.ai/gates-log.jsonl (timestamp, gate, config key, tool, a one-line summary of the
action, the reason, the session). Read it with:
claude-gates log # last 30 decisions for this project
claude-gates log --deny --gate bash-commands --tail 100
claude-gates log --since 2026-09-01T00:00:00Z --jsonThe file rotates once at 5 MB (gates-log.1.jsonl). Set CLAUDE_GATES_LOG=0 to disable it.
Tasks: registered with a criterion, closed with verified evidence
A task carries its verification criterion from the moment it is created — task add
refuses a task nobody can prove done — and task close refuses free text: it is done only
when the check actually passes.
# Register: the criterion is mandatory (--verify-command or --verify-path)
claude-gates task add "migrate the config loader" \
--size medium --verify-command "npm test" --verify-expect "fail 0"
claude-gates task add "write the migration guide" \
--parent <id> --verify-path docs/migration.md --verify-contains "## Upgrading"
claude-gates task list [--all] # active tasks (or the whole history)
# Close: with no --check/--exists, the task's own verify criterion is re-run
claude-gates task close <id>
claude-gates task close <id> --check "npm test" --expect "fail 0" --note "suite green"
claude-gates task close <id> --exists dist/report.html --contains "All green"
claude-gates task abandon <id> --reason "obsolete"
claude-gates task promote <id> <runId> # link the task to a forge runThe verified result (command, exit code, output tail, timestamp) is stored with the task.
Two gates lean on this store: require-task-split blocks implementing a task bigger than
small that has no sub-tasks, and stop-pending blocks the turn from ending while tasks
are still open.
CLI commands
# Interactive menu: pick plugins, families or gates, per project or global, and install.
npx @devrik-tools/claude-gates init
# Non-interactive (for CI or scripts):
claude-gates init --project|--global --defaults|--all|--none|--families a,b|--gates x,y --yes --dry-run
claude-gates init --new # ONLY gates this config never decided about — upgrade
# without re-answering or changing anything already set
claude-gates init --no-install # write the config but do not install the plugin
claude-gates init --force # apply changes to gates already in the file without asking
# Toggle and inspect what runs here:
claude-gates enable <gate|family|all> [--project|--global]
claude-gates disable <gate|family|all> [--project|--global]
claude-gates status # effective on/off per gate and its source (project/global/default)
claude-gates log [--tail N] [--deny] [--gate id] [--since iso] [--json]
claude-gates doctor # is Claude Code running THIS package version of the plugins?
# Tasks (the store the completion gates read):
claude-gates task add <title> --size <size> --verify-command <cmd>|--verify-path <path> [--parent <id>]
claude-gates task list [--all]
claude-gates task close <id> [--check <cmd> --expect <text>] [--exists <path> --contains <text>]
claude-gates task abandon <id> --reason <text>
claude-gates task promote <id> <runId>
# Inspect the catalog:
claude-gates registry --list # list families and gates
claude-gates registry --check # validate registry.json and that hooks.json is in sync
claude-gates registry --sync-hooks # regenerate each plugin's hooks.json from the registry
# Verify the gates actually react (not just that they are wired):
claude-gates smoke # feed each gate a known violation; exits non-zero if any does not block/warn
# Verify the tests themselves were written to break the code, not to watch it work:
claude-gates tests [paths...] # the adversarial-tests check over files already on disk; exits non-zero on any without evidencesmoke is the behavioral check registry --check (structure) and the doctor hook (files
exist) do not do: it feeds every gate a known violation and confirms it really denies/warns.
Gates whose violation needs seeded state (a db, a git repo, cross-call state) report skip,
never a false pass. Run it after install, or in CI, to catch a gate that is wired but silently
allows.
Repository layout
registry.json Catalog: families → gates (id, configKey, default, tools, params).
The single source of truth; the menu and the hooks derive from it.
cli/ The npm CLI (commander + @clack/prompts + zod).
registry.mjs · selection.mjs Load/validate the catalog; turn a selection into config.
config.mjs · materialize.mjs Where config lives, merge, and dump each gate's defaults.
init.mjs · install.mjs Interactive flow + install the plugin.
plugins/gates/ The gates plugin.
.claude-plugin/plugin.json
hooks/hooks.json Generated from registry.json (`registry --sync-hooks`). Loaded by Claude Code.
hooks/lib/ Shared hook code (Node built-ins only).
hooks/gates/<id>/ One gate per folder: index.mjs (the rule) + test.mjs (its test).
plugins/tasks/ The tasks plugin: persists per-project tasks, reminds of the open ones
and lists them on session start.
.claude-plugin/marketplace.json Lists the marketplace plugins.Adding a gate = one folder in plugins/gates/hooks/gates/<id>/ (with index.mjs and
test.mjs) + one entry in registry.json, then claude-gates registry --sync-hooks.
Gates share hooks/lib/ (payload readers, git normalization, session state, delegation
vocabulary, the decision log, the test harness): a gate file is only its rule.
Development
npm test # run all tests (node --test)
npm run registry:check # validate the catalog
npm run lint # eslint (boundaries, no-magic-numbers, sonarjs, cspell…)Each gate is tested in isolation: node --test plugins/gates/hooks/gates/<id>/test.mjs
(name the test files: a glob that also matches index.mjs hangs, because a gate waits on stdin).
This repo ships its own
.ai/config.jsonthat locally disables the gates that would false-positive when editing the gates themselves (e.g.reuse-before-buildthinks you are "building a tool" when you are really editing an existing gate). It is versioned so the repo behaves the same on any machine.
Requirements
Node ≥ 22.5 (the forge-flow gate uses node:sqlite, available from that version).
License
MIT.
