@devrik-tools/claude-gates 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +17 -0
- package/README.es.md +219 -0
- package/README.md +222 -0
- package/cli/__tests__/config.test.mjs +101 -0
- package/cli/__tests__/init-flags.test.mjs +111 -0
- package/cli/__tests__/registry-gates-consistency.test.mjs +44 -0
- package/cli/__tests__/selection.test.mjs +200 -0
- package/cli/config.mjs +113 -0
- package/cli/constants.mjs +36 -0
- package/cli/index.mjs +97 -0
- package/cli/init.mjs +256 -0
- package/cli/install.mjs +80 -0
- package/cli/materialize.mjs +51 -0
- package/cli/registry.mjs +127 -0
- package/cli/selection.mjs +83 -0
- package/package.json +73 -0
- package/plugins/gates/.claude-plugin/plugin.json +9 -0
- package/plugins/gates/hooks/gates/audit-before-build/index.mjs +88 -0
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +92 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +233 -0
- package/plugins/gates/hooks/gates/bash-commands/test.mjs +113 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +177 -0
- package/plugins/gates/hooks/gates/brief-before-delegate/test.mjs +111 -0
- package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +456 -0
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +143 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +118 -0
- package/plugins/gates/hooks/gates/dependency-skills/test.mjs +109 -0
- package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +49 -0
- package/plugins/gates/hooks/gates/diagnosis-before-patch/test.mjs +68 -0
- package/plugins/gates/hooks/gates/feature-catalog/index.mjs +100 -0
- package/plugins/gates/hooks/gates/feature-catalog/test.mjs +97 -0
- package/plugins/gates/hooks/gates/forge-flow/index.mjs +112 -0
- package/plugins/gates/hooks/gates/forge-flow/test.mjs +135 -0
- package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +186 -0
- package/plugins/gates/hooks/gates/implementation-pipeline/test.mjs +86 -0
- package/plugins/gates/hooks/gates/intent-flow/index.mjs +238 -0
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +136 -0
- package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +166 -0
- package/plugins/gates/hooks/gates/mandatory-flow/test.mjs +119 -0
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +85 -0
- package/plugins/gates/hooks/gates/neutral-spanish/test.mjs +65 -0
- package/plugins/gates/hooks/gates/never-assume/index.mjs +55 -0
- package/plugins/gates/hooks/gates/never-assume/test.mjs +78 -0
- package/plugins/gates/hooks/gates/no-blocking/index.mjs +142 -0
- package/plugins/gates/hooks/gates/no-blocking/test.mjs +108 -0
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +120 -0
- package/plugins/gates/hooks/gates/no-memory-dependency/test.mjs +106 -0
- package/plugins/gates/hooks/gates/no-reconfirm/index.mjs +202 -0
- package/plugins/gates/hooks/gates/no-reconfirm/test.mjs +131 -0
- package/plugins/gates/hooks/gates/protected-paths/index.mjs +147 -0
- package/plugins/gates/hooks/gates/protected-paths/test.mjs +75 -0
- package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +64 -0
- package/plugins/gates/hooks/gates/recurrence-lock/test.mjs +99 -0
- package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +150 -0
- package/plugins/gates/hooks/gates/reuse-before-build/test.mjs +101 -0
- package/plugins/gates/hooks/gates/risk-level/index.mjs +203 -0
- package/plugins/gates/hooks/gates/risk-level/test.mjs +125 -0
- package/plugins/gates/hooks/gates/root-cause-first/index.mjs +50 -0
- package/plugins/gates/hooks/gates/root-cause-first/test.mjs +73 -0
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +111 -0
- package/plugins/gates/hooks/gates/root-whitelist/test.mjs +80 -0
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +99 -0
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/test.mjs +78 -0
- package/plugins/gates/hooks/gates/sdd-specs/index.mjs +251 -0
- package/plugins/gates/hooks/gates/sdd-specs/test.mjs +163 -0
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +101 -0
- package/plugins/gates/hooks/gates/test-after-implementation/test.mjs +81 -0
- package/plugins/gates/hooks/gates/test-matrix/index.mjs +181 -0
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +87 -0
- package/plugins/gates/hooks/gates/tool-map/index.mjs +140 -0
- package/plugins/gates/hooks/gates/tool-map/test.mjs +87 -0
- package/plugins/gates/hooks/hooks.json +266 -0
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +154 -0
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +154 -0
- package/plugins/gates/hooks/lib/config.mjs +165 -0
- package/plugins/gates/hooks/lib/hook-io.mjs +208 -0
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +132 -0
- package/plugins/tasks/hooks/lib/task-store.mjs +159 -0
- package/plugins/tasks/hooks/register-requests.mjs +108 -0
- package/registry.json +668 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-gates",
|
|
3
|
+
"owner": {
|
|
4
|
+
"name": "claude-gates"
|
|
5
|
+
},
|
|
6
|
+
"metadata": {
|
|
7
|
+
"description": "Installable deterministic gates (hooks) for Claude Code, selectable per project or globally with `npx @devrik99/claude-gates init`."
|
|
8
|
+
},
|
|
9
|
+
"plugins": [
|
|
10
|
+
{
|
|
11
|
+
"name": "gates",
|
|
12
|
+
"source": "./plugins/gates",
|
|
13
|
+
"description": "PreToolUse gates: destructive-command blocks, protected paths, delegation/spec/quality rules, tool-discovery and forge-pipeline enforcement. Which gates run is decided by .ai/config.json (project) or ~/.claude/claude-gates/config.json (global).",
|
|
14
|
+
"version": "0.1.0"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
package/README.es.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# claude-gates
|
|
2
|
+
|
|
3
|
+
**Barreras (gates) instalables y deterministas para Claude Code.** Se ejecutan
|
|
4
|
+
automáticamente antes de que Claude use una herramienta y **bloquean o avisan** cuando algo
|
|
5
|
+
viola una regla: un comando destructivo, una escritura a un archivo protegido, una
|
|
6
|
+
delegación sin brief, y más. Todo es **configurable por proyecto** y **se puede
|
|
7
|
+
prender/apagar** cuando quieras.
|
|
8
|
+
|
|
9
|
+
La idea de fondo: en vez de confiar en que el modelo *recuerde* las reglas, un hook
|
|
10
|
+
**determinista** las hace cumplir. Un `git reset --hard` no se ejecuta porque el modelo
|
|
11
|
+
haya decidido portarse bien; se bloquea porque un gate lo intercepta.
|
|
12
|
+
|
|
13
|
+
🇬🇧 [Read it in English](./README.md)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Instalación
|
|
18
|
+
|
|
19
|
+
Dos pasos. El **plugin** engancha los hooks en Claude Code; el **CLI** elige qué gates se
|
|
20
|
+
ejecutan y con qué configuración.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# 1. Registrar el marketplace e instalar el plugin (engancha los hooks)
|
|
24
|
+
claude plugin marketplace add https://github.com/DevRik99/claude-gates
|
|
25
|
+
claude plugin install gates@claude-gates
|
|
26
|
+
|
|
27
|
+
# 2. Elegir qué gates adoptar (menú interactivo) — o usar npx sin clonar
|
|
28
|
+
npx @devrik-tools/claude-gates init
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Reinicia la sesión de Claude Code (o ejecuta `/plugin`) para que los hooks carguen.
|
|
32
|
+
|
|
33
|
+
> **¿Por qué dos cosas?** El plugin **siempre trae los 30 gates**; la configuración decide
|
|
34
|
+
> **cuáles se ejecutan**. Así puedes prender uno sin reinstalar: es una línea en un JSON.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Cómo funciona (el modelo en 30 segundos)
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
Claude va a usar una herramienta (Write, Bash, Agent…)
|
|
42
|
+
│
|
|
43
|
+
▼
|
|
44
|
+
Claude Code dispara los hooks PreToolUse en paralelo
|
|
45
|
+
│
|
|
46
|
+
▼
|
|
47
|
+
Cada gate que aplica (según su "matcher") se ejecuta en su propio proceso:
|
|
48
|
+
· lee el payload · lee su configuración del proyecto
|
|
49
|
+
· si está apagado → sale · si la regla se viola → DENY (bloquea) o WARN (avisa)
|
|
50
|
+
│
|
|
51
|
+
▼
|
|
52
|
+
Si algún gate bloquea, la herramienta no se ejecuta.
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- **DENY**: la acción es determinísticamente incorrecta → se bloquea.
|
|
56
|
+
- **WARN**: la acción necesita criterio → el gate inyecta un aviso y deja seguir.
|
|
57
|
+
- **Silencio**: el caso común. Un gate no molesta si no hay nada que objetar.
|
|
58
|
+
|
|
59
|
+
Cada gate es **autocontenido** (solo Node built-ins, sin dependencias en tiempo de
|
|
60
|
+
ejecución), así que funciona aunque instales uno suelto por fuera.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Los gates (30, en 7 familias)
|
|
65
|
+
|
|
66
|
+
`[on]` = encendidos por defecto; `[off]` = los prendes si los quieres.
|
|
67
|
+
|
|
68
|
+
### 🔒 Security — bloqueos duros sobre lo destructivo
|
|
69
|
+
| Gate | | Qué hace |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| `bash-commands` | on | Bloquea `git reset --hard`, `rm -rf`, force push, matar procesos por nombre, y publicar al remoto sin autorización. |
|
|
72
|
+
| `protected-paths` | on | Bloquea escrituras a `.env`, lockfiles y el propio harness. |
|
|
73
|
+
| `root-whitelist` | on | Bloquea crear archivos/carpetas nuevos en la raíz fuera de una lista blanca. |
|
|
74
|
+
| `no-blocking` | off | Bloquea `sleep`, `tail -f`, bucles de sondeo y servidores en primer plano. |
|
|
75
|
+
|
|
76
|
+
### 🤝 Delegation — exigencias sobre el brief al delegar a un subagente
|
|
77
|
+
| Gate | | Qué hace |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| `brief-before-delegate` | off | Exige objetivo, pasos y criterio de "listo" en el prompt. |
|
|
80
|
+
| `intent-flow` | off | Exige secciones QUÉ SÍ / QUÉ NO / EDGE CASES. |
|
|
81
|
+
| `risk-level` | off | Exige declarar el nivel (QUESTION/MICRO/STANDARD/HIGH-RISK). |
|
|
82
|
+
| `circuit-breaker` | off | Corta la misma delegación reintentada sin cambios reales. |
|
|
83
|
+
| `no-memory-dependency` | off | Avisa si el brief depende de que el subagente "recuerde" la conversación. |
|
|
84
|
+
|
|
85
|
+
### 📋 Spec-driven flow — solo aplican si el proyecto adoptó desarrollo por specs
|
|
86
|
+
| Gate | | Qué hace |
|
|
87
|
+
|---|---|---|
|
|
88
|
+
| `feature-catalog` | on | Una sola feature en progreso; cerrar exige asserts y revisión. |
|
|
89
|
+
| `sdd-specs` | off | Exige requirements/design/tasks no vacíos antes de implementar. |
|
|
90
|
+
| `implementation-pipeline` | off | Exige declarar definición → escritura → validación → QA → cierre. |
|
|
91
|
+
| `mandatory-flow` | off | Exige una tarea activa con contrato antes de implementar. |
|
|
92
|
+
| `test-matrix` | off | Exige una matriz de tests (los tipos que el requerimiento vuelve obligatorios). |
|
|
93
|
+
|
|
94
|
+
### ✨ Quality — higiene de código, diagnóstico y lenguaje
|
|
95
|
+
| Gate | | Qué hace |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `dependency-skills` | on | Una dependencia directa nueva exige una skill vigente. |
|
|
98
|
+
| `root-cause-first` | off | Exige un diagnóstico origen→síntoma antes de un parche. |
|
|
99
|
+
| `audit-before-build` | off | Antes de un script/gate nuevo, exige declarar que nada existente lo cubre. |
|
|
100
|
+
| `never-assume` | off | Marca suposiciones sin verificar en briefs y código. |
|
|
101
|
+
| `rule-skill-autodiscovery` | off | Carga los gates que el proyecto declara en sus `rules/` y `skills/`. |
|
|
102
|
+
| `recurrence-lock` | on | La segunda aparición de un defecto exige su bloqueo determinista. |
|
|
103
|
+
| `test-after-implementation` | off | Avisa cuando se escribe un test después de su implementación. |
|
|
104
|
+
| `no-reconfirm` | on | Nunca vuelve a preguntar lo que ya respondiste. |
|
|
105
|
+
| `neutral-spanish` | on | Avisa sobre voseo o léxico regional en el texto escrito. |
|
|
106
|
+
| `diagnosis-before-patch` | on | Avisa cuando se cambian timeouts/reintentos sin evidencia. |
|
|
107
|
+
|
|
108
|
+
### 🔎 Tool discovery — no reinventar la rueda
|
|
109
|
+
| Gate | | Qué hace |
|
|
110
|
+
|---|---|---|
|
|
111
|
+
| `reuse-before-build` | off | Antes de construir una herramienta, consulta el mapa de herramientas del proyecto; bloquea si no auditaste (local → Context7 → web). |
|
|
112
|
+
| `tool-map` | off | Registra las herramientas descubiertas en `.ai/tool-map.json` para no volver a explorar. |
|
|
113
|
+
|
|
114
|
+
### 🏭 Forge pipeline — obliga a seguir el flujo de forge
|
|
115
|
+
| Gate | | Qué hace |
|
|
116
|
+
|---|---|---|
|
|
117
|
+
| `forge-flow` | off | En un proyecto que adoptó [forge](https://github.com/DevRik99/forge-mcp), bloquea editar/ejecutar si no hay un run de forge activo. Cierra el hueco que el MCP no puede: te obliga a pasar por el pipeline. |
|
|
118
|
+
|
|
119
|
+
### 🩺 Session start — validaciones al arrancar la sesión *(en construcción)*
|
|
120
|
+
`doctor`, `ask-adoption`, `wiring-check` — declarados en el catálogo; sus scripts se migran
|
|
121
|
+
a continuación.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Configuración: prender, apagar y ajustar cada gate
|
|
126
|
+
|
|
127
|
+
Todo vive en un solo archivo. **El proyecto manda sobre el global:**
|
|
128
|
+
|
|
129
|
+
- **Proyecto:** `<raíz>/.ai/config.json`
|
|
130
|
+
- **Global (fallback):** `~/.claude/claude-gates/config.json`
|
|
131
|
+
|
|
132
|
+
`init` escribe ahí la selección **y materializa los valores por defecto de cada gate**, así
|
|
133
|
+
ves y editas cada perilla:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"adopted": "partial",
|
|
138
|
+
"gateVersion": "3.0.0",
|
|
139
|
+
"gates": {
|
|
140
|
+
"blockDestructiveShellCommands": {
|
|
141
|
+
"enabled": true,
|
|
142
|
+
"rmRfProtectedAreas": ["/", "*", "src", "tests"],
|
|
143
|
+
"denyPatterns": ["git reset --hard", "…"]
|
|
144
|
+
},
|
|
145
|
+
"requireBriefBeforeDelegating": { "enabled": false }
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- **Apagar un gate:** `"enabled": false`. Se apaga al instante, sin reinstalar.
|
|
151
|
+
- **Ajustar su comportamiento:** editas sus parámetros (la lista blanca, los patrones, los
|
|
152
|
+
umbrales). Lo que declara el proyecto **reemplaza** el default del gate.
|
|
153
|
+
- Un gate que no aparece en la configuración usa su default del catálogo. Las claves que ya
|
|
154
|
+
tuvieras en el archivo (por ejemplo `autoCommit`) se conservan intactas.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Comandos del CLI
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
# Menú interactivo: elige plugins, familias o gates, por proyecto o global, e instala.
|
|
162
|
+
npx @devrik-tools/claude-gates init
|
|
163
|
+
|
|
164
|
+
# Sin menú (para CI o scripts):
|
|
165
|
+
claude-gates init --project|--global --defaults|--all|--none|--families a,b|--gates x,y --yes --dry-run
|
|
166
|
+
claude-gates init --no-install # escribe la configuración pero no instala el plugin
|
|
167
|
+
|
|
168
|
+
# Inspeccionar el catálogo:
|
|
169
|
+
claude-gates registry --list # lista familias y gates
|
|
170
|
+
claude-gates registry --check # valida registry.json
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Estructura del repositorio
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
registry.json Catálogo: familias → gates (id, configKey, default, tools, params).
|
|
179
|
+
Es la única fuente de verdad; el menú y los hooks derivan de él.
|
|
180
|
+
cli/ El CLI de npm (commander + @clack/prompts + zod).
|
|
181
|
+
registry.mjs · selection.mjs Cargar/validar el catálogo; convertir la selección en configuración.
|
|
182
|
+
config.mjs · materialize.mjs Dónde vive la configuración, merge, y volcar los defaults de cada gate.
|
|
183
|
+
init.mjs · install.mjs Flujo interactivo + instalar el plugin.
|
|
184
|
+
plugins/gates/ El plugin de gates.
|
|
185
|
+
.claude-plugin/plugin.json
|
|
186
|
+
hooks/hooks.json Una entrada por gate (matcher + comando). Lo carga Claude Code.
|
|
187
|
+
hooks/lib/ Código compartido de los hooks (Node built-ins only).
|
|
188
|
+
hooks/gates/<id>/ Un gate por carpeta: index.mjs (la regla) + test.mjs (su test).
|
|
189
|
+
plugins/tasks/ El plugin de tareas (en construcción): persiste tareas por proyecto.
|
|
190
|
+
.claude-plugin/marketplace.json Lista los plugins del marketplace.
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Agregar un gate** = una carpeta en `plugins/gates/hooks/gates/<id>/` (con `index.mjs` y
|
|
194
|
+
`test.mjs`) + una entrada en `registry.json`. El resto se deriva solo.
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Desarrollo
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
npm test # ejecuta todos los tests (node --test)
|
|
202
|
+
npm run registry:check # valida el catálogo
|
|
203
|
+
npm run lint # eslint (boundaries, no-magic-numbers, sonarjs, cspell…)
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Cada gate se testea aislado: `node --test plugins/gates/hooks/gates/<id>/test.mjs`.
|
|
207
|
+
|
|
208
|
+
> Este repositorio trae su propio `.ai/config.json` que apaga localmente los gates que
|
|
209
|
+
> darían falso positivo al **editar los gates mismos** (por ejemplo, `audit-before-build`
|
|
210
|
+
> cree que estás "construyendo una herramienta" cuando en realidad editas un gate
|
|
211
|
+
> existente). Por eso está versionado: el repo se comporta igual en cualquier máquina.
|
|
212
|
+
|
|
213
|
+
## Requisitos
|
|
214
|
+
|
|
215
|
+
Node **≥ 22.5** (el gate `forge-flow` usa `node:sqlite`, disponible desde esa versión).
|
|
216
|
+
|
|
217
|
+
## Licencia
|
|
218
|
+
|
|
219
|
+
MIT.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# claude-gates
|
|
2
|
+
|
|
3
|
+
**Installable, deterministic gates (hooks) for Claude Code.** They run automatically before
|
|
4
|
+
Claude executes a tool and **block or warn** when something breaks a rule: a destructive
|
|
5
|
+
command, a write to a protected file, a delegation with no brief, and more. Everything is
|
|
6
|
+
**configurable per project** and can be **turned on/off** whenever you want.
|
|
7
|
+
|
|
8
|
+
The core idea: instead of trusting the model to *remember* the rules, a **deterministic**
|
|
9
|
+
hook enforces them. A `git reset --hard` does not run because the model chose to behave —
|
|
10
|
+
it is blocked because a gate intercepts it.
|
|
11
|
+
|
|
12
|
+
🇪🇸 [Léelo en español](./README.es.md)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
Two steps. The **plugin** wires the hooks into Claude Code; the **CLI** picks which gates
|
|
19
|
+
run and with what configuration.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 1. Register the marketplace and install the plugin (wires the hooks)
|
|
23
|
+
claude plugin marketplace add https://github.com/DevRik99/claude-gates
|
|
24
|
+
claude plugin install gates@claude-gates
|
|
25
|
+
|
|
26
|
+
# 2. Choose which gates to adopt (interactive menu) — or use npx without cloning
|
|
27
|
+
npx @devrik-tools/claude-gates init
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Restart the Claude Code session (or run `/plugin`) so the hooks load.
|
|
31
|
+
|
|
32
|
+
> **Why two things?** The plugin **always ships all 30 gates**; the config decides **which
|
|
33
|
+
> ones run**. So you can turn one on without reinstalling — it is one line in a JSON file.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## How it works (the model in 30 seconds)
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
Claude is about to use a tool (Write, Bash, Agent…)
|
|
41
|
+
│
|
|
42
|
+
▼
|
|
43
|
+
Claude Code fires the PreToolUse hooks in parallel
|
|
44
|
+
│
|
|
45
|
+
▼
|
|
46
|
+
Each gate that applies (by its "matcher") runs in its own process:
|
|
47
|
+
· reads the payload · reads its project config
|
|
48
|
+
· if off → exits · if the rule is broken → DENY (block) or WARN (advise)
|
|
49
|
+
│
|
|
50
|
+
▼
|
|
51
|
+
If any gate blocks, the tool does not run.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **DENY**: the action is deterministically wrong → it is blocked.
|
|
55
|
+
- **WARN**: the action needs judgment → the gate injects a note and lets it proceed.
|
|
56
|
+
- **Silence**: the common path. A gate never nags when there is nothing to object to.
|
|
57
|
+
|
|
58
|
+
Every gate is **self-contained** (Node built-ins only, no runtime dependencies), so it
|
|
59
|
+
works even if you install one on its own.
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## The gates (30, in 7 families)
|
|
64
|
+
|
|
65
|
+
`[on]` = enabled by default; `[off]` = enable it if you want it.
|
|
66
|
+
|
|
67
|
+
### 🔒 Security — hard blocks on destructive actions
|
|
68
|
+
| Gate | | What it does |
|
|
69
|
+
|---|---|---|
|
|
70
|
+
| `bash-commands` | on | Blocks `git reset --hard`, `rm -rf`, force push, killing processes by name, and publishing to a remote without authorization. |
|
|
71
|
+
| `protected-paths` | on | Blocks writes to `.env`, lockfiles and the harness itself. |
|
|
72
|
+
| `root-whitelist` | on | Blocks new root-level files/folders outside a whitelist. |
|
|
73
|
+
| `no-blocking` | off | Blocks `sleep`, `tail -f`, polling loops and foreground servers. |
|
|
74
|
+
|
|
75
|
+
### 🤝 Delegation — requirements on the brief when delegating to a subagent
|
|
76
|
+
| Gate | | What it does |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `brief-before-delegate` | off | Requires goal, steps and done-when criteria in the prompt. |
|
|
79
|
+
| `intent-flow` | off | Requires IN SCOPE / OUT OF SCOPE / EDGE CASES sections. |
|
|
80
|
+
| `risk-level` | off | Requires a declared level (QUESTION/MICRO/STANDARD/HIGH-RISK). |
|
|
81
|
+
| `circuit-breaker` | off | Cuts the same delegation retried without real changes. |
|
|
82
|
+
| `no-memory-dependency` | off | Warns when the brief relies on the subagent "remembering" the chat. |
|
|
83
|
+
|
|
84
|
+
### 📋 Spec-driven flow — only relevant if the project adopted spec-driven development
|
|
85
|
+
| Gate | | What it does |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `feature-catalog` | on | A single feature in progress; closing requires asserts and review. |
|
|
88
|
+
| `sdd-specs` | off | Requires non-empty requirements/design/tasks before implementing. |
|
|
89
|
+
| `implementation-pipeline` | off | Requires declaring definition → writing → validation → QA → closure. |
|
|
90
|
+
| `mandatory-flow` | off | Requires an active task with a contract before implementing. |
|
|
91
|
+
| `test-matrix` | off | Requires a test matrix (the types the requirement makes mandatory). |
|
|
92
|
+
|
|
93
|
+
### ✨ Quality — code hygiene, diagnosis and language
|
|
94
|
+
| Gate | | What it does |
|
|
95
|
+
|---|---|---|
|
|
96
|
+
| `dependency-skills` | on | A new direct dependency requires a current skill. |
|
|
97
|
+
| `root-cause-first` | off | Requires an origin→symptom diagnosis before a patch. |
|
|
98
|
+
| `audit-before-build` | off | Before a new script/gate, requires stating that nothing existing covers it. |
|
|
99
|
+
| `never-assume` | off | Flags unverified assumptions in briefs and code. |
|
|
100
|
+
| `rule-skill-autodiscovery` | off | Loads gates the project declares in its `rules/` and `skills/`. |
|
|
101
|
+
| `recurrence-lock` | on | A second occurrence of a defect class requires its deterministic block. |
|
|
102
|
+
| `test-after-implementation` | off | Warns when a test is written after its paired implementation. |
|
|
103
|
+
| `no-reconfirm` | on | Never re-ask what you already answered. |
|
|
104
|
+
| `neutral-spanish` | on | Warns about voseo or regional lexicon in written text. |
|
|
105
|
+
| `diagnosis-before-patch` | on | Warns when timeouts/retries change without evidence. |
|
|
106
|
+
|
|
107
|
+
### 🔎 Tool discovery — don't reinvent the wheel
|
|
108
|
+
| Gate | | What it does |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| `reuse-before-build` | off | Before building a tool, consults the project tool map; blocks if you did not audit (local → Context7 → web). |
|
|
111
|
+
| `tool-map` | off | Records discovered tools in `.ai/tool-map.json` so exploration is not repeated. |
|
|
112
|
+
|
|
113
|
+
### 🏭 Forge pipeline — enforces the forge workflow
|
|
114
|
+
| Gate | | What it does |
|
|
115
|
+
|---|---|---|
|
|
116
|
+
| `forge-flow` | off | In a project that adopted [forge](https://github.com/DevRik99/forge-mcp), blocks editing/running unless an active forge run exists. Closes the hole the MCP cannot: it forces you through the pipeline. |
|
|
117
|
+
|
|
118
|
+
### 🩺 Session start — startup checks *(work in progress)*
|
|
119
|
+
`doctor`, `ask-adoption`, `wiring-check` — declared in the catalog; their scripts are being
|
|
120
|
+
migrated next.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Configuration: turn gates on, off, and tune them
|
|
125
|
+
|
|
126
|
+
Everything lives in one file. **Project overrides global:**
|
|
127
|
+
|
|
128
|
+
- **Project:** `<root>/.ai/config.json`
|
|
129
|
+
- **Global (fallback):** `~/.claude/claude-gates/config.json`
|
|
130
|
+
|
|
131
|
+
`init` writes the selection there **and materializes each gate's default values**, so you
|
|
132
|
+
see and edit every knob:
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"adopted": "partial",
|
|
137
|
+
"gateVersion": "3.0.0",
|
|
138
|
+
"gates": {
|
|
139
|
+
"blockDestructiveShellCommands": {
|
|
140
|
+
"enabled": true,
|
|
141
|
+
"rmRfProtectedAreas": ["/", "*", "src", "tests"],
|
|
142
|
+
"denyPatterns": ["git reset --hard", "…"]
|
|
143
|
+
},
|
|
144
|
+
"warnNonNeutralSpanish": {
|
|
145
|
+
"enabled": true,
|
|
146
|
+
"regionalMarkers": ["tenés", "podés", "…"]
|
|
147
|
+
},
|
|
148
|
+
"requireBriefBeforeDelegating": { "enabled": false }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
- **Turn a gate off:** `"enabled": false`. Off instantly, no reinstall.
|
|
154
|
+
- **Tune its behavior:** edit its parameters (the whitelist, the patterns, the thresholds).
|
|
155
|
+
What the project declares **replaces** the gate's default.
|
|
156
|
+
- A gate absent from the config uses its catalog default. Keys you already had in the file
|
|
157
|
+
(e.g. `autoCommit`) are kept intact.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## CLI commands
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Interactive menu: pick plugins, families or gates, per project or global, and install.
|
|
165
|
+
npx @devrik-tools/claude-gates init
|
|
166
|
+
|
|
167
|
+
# Non-interactive (for CI or scripts):
|
|
168
|
+
claude-gates init --project|--global --defaults|--all|--none|--families a,b|--gates x,y --yes --dry-run
|
|
169
|
+
claude-gates init --no-install # write the config but do not install the plugin
|
|
170
|
+
|
|
171
|
+
# Inspect the catalog:
|
|
172
|
+
claude-gates registry --list # list families and gates
|
|
173
|
+
claude-gates registry --check # validate registry.json
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Repository layout
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
registry.json Catalog: families → gates (id, configKey, default, tools, params).
|
|
182
|
+
The single source of truth; the menu and the hooks derive from it.
|
|
183
|
+
cli/ The npm CLI (commander + @clack/prompts + zod).
|
|
184
|
+
registry.mjs · selection.mjs Load/validate the catalog; turn a selection into config.
|
|
185
|
+
config.mjs · materialize.mjs Where config lives, merge, and dump each gate's defaults.
|
|
186
|
+
init.mjs · install.mjs Interactive flow + install the plugin.
|
|
187
|
+
plugins/gates/ The gates plugin.
|
|
188
|
+
.claude-plugin/plugin.json
|
|
189
|
+
hooks/hooks.json One entry per gate (matcher + command). Loaded by Claude Code.
|
|
190
|
+
hooks/lib/ Shared hook code (Node built-ins only).
|
|
191
|
+
hooks/gates/<id>/ One gate per folder: index.mjs (the rule) + test.mjs (its test).
|
|
192
|
+
plugins/tasks/ The tasks plugin (work in progress): persists per-project tasks.
|
|
193
|
+
.claude-plugin/marketplace.json Lists the marketplace plugins.
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Adding a gate** = one folder in `plugins/gates/hooks/gates/<id>/` (with `index.mjs` and
|
|
197
|
+
`test.mjs`) + one entry in `registry.json`. Everything else derives automatically.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
npm test # run all tests (node --test)
|
|
205
|
+
npm run registry:check # validate the catalog
|
|
206
|
+
npm run lint # eslint (boundaries, no-magic-numbers, sonarjs, cspell…)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Each gate is tested in isolation: `node --test plugins/gates/hooks/gates/<id>/test.mjs`.
|
|
210
|
+
|
|
211
|
+
> This repo ships its own `.ai/config.json` that locally disables the gates that would
|
|
212
|
+
> false-positive when **editing the gates themselves** (e.g. `audit-before-build` thinks
|
|
213
|
+
> you are "building a tool" when you are really editing an existing gate). It is versioned
|
|
214
|
+
> so the repo behaves the same on any machine.
|
|
215
|
+
|
|
216
|
+
## Requirements
|
|
217
|
+
|
|
218
|
+
Node **≥ 22.5** (the `forge-flow` gate uses `node:sqlite`, available from that version).
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
} from 'node:fs';
|
|
9
|
+
import { tmpdir } from 'node:os';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { test } from 'node:test';
|
|
12
|
+
import {
|
|
13
|
+
SCOPES,
|
|
14
|
+
findProjectRoot,
|
|
15
|
+
configPathFor,
|
|
16
|
+
readConfig,
|
|
17
|
+
mergeConfig,
|
|
18
|
+
writeConfig,
|
|
19
|
+
} from '../config.mjs';
|
|
20
|
+
|
|
21
|
+
function temporaryDirectory() {
|
|
22
|
+
return mkdtempSync(join(tmpdir(), 'claude-gates-'));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test('findProjectRoot climbs to the .git directory and falls back to the start', () => {
|
|
26
|
+
const root = temporaryDirectory();
|
|
27
|
+
mkdirSync(join(root, '.git'));
|
|
28
|
+
const nested = join(root, 'src', 'deep');
|
|
29
|
+
mkdirSync(nested, { recursive: true });
|
|
30
|
+
assert.equal(findProjectRoot(nested), root);
|
|
31
|
+
|
|
32
|
+
const loose = temporaryDirectory();
|
|
33
|
+
assert.equal(findProjectRoot(loose), loose);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('findProjectRoot never climbs into the home directory', () => {
|
|
37
|
+
const fakeHome = temporaryDirectory();
|
|
38
|
+
mkdirSync(join(fakeHome, '.ai'));
|
|
39
|
+
const project = join(fakeHome, 'repos', 'app');
|
|
40
|
+
mkdirSync(project, { recursive: true });
|
|
41
|
+
assert.equal(findProjectRoot(project, { home: fakeHome }), project);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('configPathFor resolves both scopes', () => {
|
|
45
|
+
const home = temporaryDirectory();
|
|
46
|
+
const project = temporaryDirectory();
|
|
47
|
+
mkdirSync(join(project, '.ai'));
|
|
48
|
+
assert.equal(
|
|
49
|
+
configPathFor(SCOPES.GLOBAL, { home }),
|
|
50
|
+
join(home, '.claude', 'claude-gates', 'config.json'),
|
|
51
|
+
);
|
|
52
|
+
assert.equal(
|
|
53
|
+
configPathFor(SCOPES.PROJECT, { cwd: project }),
|
|
54
|
+
join(project, '.ai', 'config.json'),
|
|
55
|
+
);
|
|
56
|
+
assert.throws(() => configPathFor('nope'), /Unknown scope/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('readConfig distinguishes missing, valid and corrupt', () => {
|
|
60
|
+
const directory = temporaryDirectory();
|
|
61
|
+
const path = join(directory, 'config.json');
|
|
62
|
+
assert.deepEqual(readConfig(path), {
|
|
63
|
+
exists: false,
|
|
64
|
+
data: {},
|
|
65
|
+
corrupt: false,
|
|
66
|
+
});
|
|
67
|
+
writeFileSync(path, '{"autoCommit":true}');
|
|
68
|
+
assert.deepEqual(readConfig(path), {
|
|
69
|
+
exists: true,
|
|
70
|
+
data: { autoCommit: true },
|
|
71
|
+
corrupt: false,
|
|
72
|
+
});
|
|
73
|
+
writeFileSync(path, '{nope');
|
|
74
|
+
assert.equal(readConfig(path).corrupt, true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('mergeConfig keeps unrelated keys and unknown gates, overrides known ones', () => {
|
|
78
|
+
const existing = {
|
|
79
|
+
autoCommit: true,
|
|
80
|
+
gates: { oldGate: true, blockX: false },
|
|
81
|
+
};
|
|
82
|
+
const merged = mergeConfig(existing, {
|
|
83
|
+
adopted: 'partial',
|
|
84
|
+
gates: { blockX: true, blockY: false },
|
|
85
|
+
gateVersion: '3.0.0',
|
|
86
|
+
});
|
|
87
|
+
assert.deepEqual(merged, {
|
|
88
|
+
autoCommit: true,
|
|
89
|
+
adopted: 'partial',
|
|
90
|
+
gateVersion: '3.0.0',
|
|
91
|
+
gates: { oldGate: true, blockX: true, blockY: false },
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('writeConfig creates parent folders and writes pretty JSON with trailing newline', () => {
|
|
96
|
+
const directory = temporaryDirectory();
|
|
97
|
+
const path = join(directory, 'a', 'b', 'config.json');
|
|
98
|
+
writeConfig(path, { adopted: true });
|
|
99
|
+
assert.ok(existsSync(path));
|
|
100
|
+
assert.equal(readFileSync(path, 'utf8'), '{\n "adopted": true\n}\n');
|
|
101
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
3
|
+
import {
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
writeFileSync,
|
|
8
|
+
existsSync,
|
|
9
|
+
} from 'node:fs';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { join, dirname } from 'node:path';
|
|
12
|
+
import { test } from 'node:test';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { normalizeOptions } from '../init.mjs';
|
|
15
|
+
|
|
16
|
+
const CLI = join(dirname(fileURLToPath(import.meta.url)), '..', 'index.mjs');
|
|
17
|
+
|
|
18
|
+
test('normalizeOptions maps commander options', () => {
|
|
19
|
+
assert.deepEqual(
|
|
20
|
+
normalizeOptions({
|
|
21
|
+
global: true,
|
|
22
|
+
families: ['security', 'quality'],
|
|
23
|
+
yes: true,
|
|
24
|
+
dryRun: true,
|
|
25
|
+
}),
|
|
26
|
+
{
|
|
27
|
+
scope: 'global',
|
|
28
|
+
mode: 'families',
|
|
29
|
+
families: ['security', 'quality'],
|
|
30
|
+
gates: [],
|
|
31
|
+
yes: true,
|
|
32
|
+
dryRun: true,
|
|
33
|
+
install: undefined,
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
assert.equal(normalizeOptions({ gates: ['bash-commands'] }).mode, 'granular');
|
|
37
|
+
assert.equal(normalizeOptions({}).mode, null);
|
|
38
|
+
assert.throws(() => normalizeOptions({ all: true, none: true }), /Pick one/);
|
|
39
|
+
assert.throws(
|
|
40
|
+
() => normalizeOptions({ project: true, global: true }),
|
|
41
|
+
/Pick one/,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('non-interactive init writes .ai/config.json at the project root', () => {
|
|
46
|
+
const project = mkdtempSync(join(tmpdir(), 'claude-gates-proj-'));
|
|
47
|
+
mkdirSync(join(project, '.git'));
|
|
48
|
+
const nested = join(project, 'src');
|
|
49
|
+
mkdirSync(nested);
|
|
50
|
+
|
|
51
|
+
execFileSync(
|
|
52
|
+
process.execPath,
|
|
53
|
+
[
|
|
54
|
+
CLI,
|
|
55
|
+
'init',
|
|
56
|
+
'--project',
|
|
57
|
+
'--families',
|
|
58
|
+
'security',
|
|
59
|
+
'--yes',
|
|
60
|
+
'--no-install',
|
|
61
|
+
],
|
|
62
|
+
{ cwd: nested, encoding: 'utf8' },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const path = join(project, '.ai', 'config.json');
|
|
66
|
+
assert.ok(existsSync(path), 'config written at git root, not at cwd');
|
|
67
|
+
const config = JSON.parse(readFileSync(path, 'utf8'));
|
|
68
|
+
assert.equal(config.adopted, 'partial');
|
|
69
|
+
// Gates with params are materialized as { enabled, ...defaults } so the knobs are editable.
|
|
70
|
+
assert.equal(config.gates.blockDestructiveShellCommands.enabled, true);
|
|
71
|
+
assert.equal(config.gates.requireBriefBeforeDelegating.enabled, false);
|
|
72
|
+
assert.match(config.gateVersion, /^\d+\.\d+\.\d+$/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('--dry-run writes nothing', () => {
|
|
76
|
+
const project = mkdtempSync(join(tmpdir(), 'claude-gates-dry-'));
|
|
77
|
+
execFileSync(
|
|
78
|
+
process.execPath,
|
|
79
|
+
[CLI, 'init', '--project', '--all', '--yes', '--dry-run'],
|
|
80
|
+
{ cwd: project, encoding: 'utf8' },
|
|
81
|
+
);
|
|
82
|
+
assert.ok(!existsSync(join(project, '.ai', 'config.json')));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('--none records an explicit refusal', () => {
|
|
86
|
+
const project = mkdtempSync(join(tmpdir(), 'claude-gates-none-'));
|
|
87
|
+
execFileSync(
|
|
88
|
+
process.execPath,
|
|
89
|
+
[CLI, 'init', '--project', '--none', '--yes', '--no-install'],
|
|
90
|
+
{ cwd: project, encoding: 'utf8' },
|
|
91
|
+
);
|
|
92
|
+
const config = JSON.parse(
|
|
93
|
+
readFileSync(join(project, '.ai', 'config.json'), 'utf8'),
|
|
94
|
+
);
|
|
95
|
+
assert.equal(config.adopted, false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('a corrupt existing config aborts without writing', () => {
|
|
99
|
+
const project = mkdtempSync(join(tmpdir(), 'claude-gates-corrupt-'));
|
|
100
|
+
mkdirSync(join(project, '.ai'));
|
|
101
|
+
const path = join(project, '.ai', 'config.json');
|
|
102
|
+
writeFileSync(path, '{broken');
|
|
103
|
+
assert.throws(() =>
|
|
104
|
+
execFileSync(
|
|
105
|
+
process.execPath,
|
|
106
|
+
[CLI, 'init', '--project', '--all', '--yes'],
|
|
107
|
+
{ cwd: project, encoding: 'utf8', stdio: 'pipe' },
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
assert.equal(readFileSync(path, 'utf8'), '{broken');
|
|
111
|
+
});
|