@devrik-tools/claude-gates 0.2.1 → 0.3.1
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 +2 -2
- package/README.es.md +7 -6
- package/README.md +16 -7
- package/cli/index.mjs +4 -0
- package/cli/init.mjs +18 -1
- package/cli/install.mjs +26 -1
- package/package.json +5 -2
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/gates/autonomous-mode/stop.mjs +78 -0
- package/plugins/gates/hooks/gates/bash-commands/index.mjs +11 -81
- package/plugins/gates/hooks/gates/block-remote-publish/index.mjs +140 -0
- package/plugins/gates/hooks/gates/dependency-skills/index.mjs +130 -126
- package/plugins/gates/hooks/gates/neutral-spanish/index.mjs +104 -79
- package/plugins/gates/hooks/gates/no-memory-dependency/index.mjs +138 -123
- package/plugins/gates/hooks/gates/stop-pending/index.mjs +6 -1
- package/plugins/gates/hooks/gates/test-after-implementation/index.mjs +124 -101
- package/plugins/gates/hooks/hooks.json +19 -0
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
- package/plugins/tasks/hooks/register-requests.mjs +7 -3
- package/registry.json +738 -793
- package/cli/__tests__/config.test.mjs +0 -113
- package/cli/__tests__/init-flags.test.mjs +0 -111
- package/cli/__tests__/install.test.mjs +0 -102
- package/cli/__tests__/materialize.test.mjs +0 -95
- package/cli/__tests__/registry-gates-consistency.test.mjs +0 -52
- package/cli/__tests__/selection.test.mjs +0 -200
- package/cli/__tests__/task.test.mjs +0 -124
- package/cli/__tests__/version-consistency.test.mjs +0 -44
- package/plugins/gates/hooks/__tests__/ask-adoption.test.mjs +0 -83
- package/plugins/gates/hooks/__tests__/doctor.test.mjs +0 -85
- package/plugins/gates/hooks/__tests__/wiring-check.test.mjs +0 -65
- package/plugins/gates/hooks/gates/audit-before-build/audit-before-build.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/audit-before-build/test.mjs +0 -117
- package/plugins/gates/hooks/gates/autonomous-mode/autonomous.edge.test.mjs +0 -104
- package/plugins/gates/hooks/gates/autonomous-mode/test.mjs +0 -58
- package/plugins/gates/hooks/gates/bash-commands/bash-commands.edge.test.mjs +0 -165
- package/plugins/gates/hooks/gates/bash-commands/test.mjs +0 -126
- package/plugins/gates/hooks/gates/brief-before-delegate/brief-before-delegate.edge.test.mjs +0 -151
- package/plugins/gates/hooks/gates/brief-before-delegate/test.mjs +0 -111
- package/plugins/gates/hooks/gates/circuit-breaker/circuit-breaker.edge.test.mjs +0 -207
- package/plugins/gates/hooks/gates/circuit-breaker/test.mjs +0 -161
- package/plugins/gates/hooks/gates/dependency-skills/dependency-skills.edge.test.mjs +0 -69
- package/plugins/gates/hooks/gates/dependency-skills/test.mjs +0 -109
- package/plugins/gates/hooks/gates/diagnosis-before-patch/diagnosis-before-patch.edge.test.mjs +0 -68
- package/plugins/gates/hooks/gates/diagnosis-before-patch/test.mjs +0 -68
- package/plugins/gates/hooks/gates/feature-catalog/edge.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/feature-catalog/test.mjs +0 -97
- package/plugins/gates/hooks/gates/force-parallel/test.mjs +0 -88
- package/plugins/gates/hooks/gates/forge-flow/forge-flow.edge.test.mjs +0 -155
- package/plugins/gates/hooks/gates/forge-flow/test.mjs +0 -135
- package/plugins/gates/hooks/gates/implementation-pipeline/edge.edge.test.mjs +0 -66
- package/plugins/gates/hooks/gates/implementation-pipeline/test.mjs +0 -86
- package/plugins/gates/hooks/gates/intent-flow/intent-flow.edge.test.mjs +0 -96
- package/plugins/gates/hooks/gates/intent-flow/test.mjs +0 -165
- package/plugins/gates/hooks/gates/lint-commit/test.mjs +0 -85
- package/plugins/gates/hooks/gates/mandatory-flow/edge.edge.test.mjs +0 -100
- package/plugins/gates/hooks/gates/mandatory-flow/test.mjs +0 -119
- package/plugins/gates/hooks/gates/neutral-spanish/neutral-spanish.edge.test.mjs +0 -80
- package/plugins/gates/hooks/gates/neutral-spanish/test.mjs +0 -65
- package/plugins/gates/hooks/gates/never-assume/never-assume.edge.test.mjs +0 -71
- package/plugins/gates/hooks/gates/never-assume/test.mjs +0 -95
- package/plugins/gates/hooks/gates/no-blocking/no-blocking.edge.test.mjs +0 -109
- package/plugins/gates/hooks/gates/no-blocking/test.mjs +0 -108
- package/plugins/gates/hooks/gates/no-memory-dependency/no-memory-dependency.edge.test.mjs +0 -83
- package/plugins/gates/hooks/gates/no-memory-dependency/test.mjs +0 -106
- package/plugins/gates/hooks/gates/no-reconfirm/no-reconfirm.edge.test.mjs +0 -91
- package/plugins/gates/hooks/gates/no-reconfirm/test.mjs +0 -131
- package/plugins/gates/hooks/gates/protected-paths/protected-paths.edge.test.mjs +0 -126
- package/plugins/gates/hooks/gates/protected-paths/test.mjs +0 -75
- package/plugins/gates/hooks/gates/recurrence-lock/recurrence-lock.edge.test.mjs +0 -102
- package/plugins/gates/hooks/gates/recurrence-lock/test.mjs +0 -99
- package/plugins/gates/hooks/gates/reuse-before-build/edge.edge.test.mjs +0 -94
- package/plugins/gates/hooks/gates/reuse-before-build/test.mjs +0 -101
- package/plugins/gates/hooks/gates/risk-level/risk-level.edge.test.mjs +0 -116
- package/plugins/gates/hooks/gates/risk-level/test.mjs +0 -153
- package/plugins/gates/hooks/gates/root-cause-first/root-cause-first.edge.test.mjs +0 -58
- package/plugins/gates/hooks/gates/root-cause-first/test.mjs +0 -73
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +0 -124
- package/plugins/gates/hooks/gates/root-whitelist/test.mjs +0 -80
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/rule-skill-autodiscovery.edge.test.mjs +0 -126
- package/plugins/gates/hooks/gates/rule-skill-autodiscovery/test.mjs +0 -78
- package/plugins/gates/hooks/gates/sdd-specs/edge.edge.test.mjs +0 -130
- package/plugins/gates/hooks/gates/sdd-specs/test.mjs +0 -163
- package/plugins/gates/hooks/gates/stop-pending/test.mjs +0 -114
- package/plugins/gates/hooks/gates/test-after-implementation/test-after-implementation.edge.test.mjs +0 -72
- package/plugins/gates/hooks/gates/test-after-implementation/test.mjs +0 -81
- package/plugins/gates/hooks/gates/test-matrix/edge.edge.test.mjs +0 -63
- package/plugins/gates/hooks/gates/test-matrix/test.mjs +0 -114
- package/plugins/gates/hooks/gates/tool-map/edge.edge.test.mjs +0 -62
- package/plugins/gates/hooks/gates/tool-map/test.mjs +0 -87
- package/plugins/gates/hooks/lib/__tests__/config.test.mjs +0 -173
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +0 -223
- package/plugins/gates/hooks/lib/__tests__/signals.test.mjs +0 -114
- package/plugins/gates/hooks/lib/hook-io-config.edge.test.mjs +0 -189
- package/plugins/tasks/hooks/__tests__/register-requests.test.mjs +0 -100
- package/plugins/tasks/hooks/__tests__/session-tasks.test.mjs +0 -95
- package/plugins/tasks/hooks/lib/__tests__/task-store.test.mjs +0 -180
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
"name": "gates",
|
|
12
12
|
"source": "./plugins/gates",
|
|
13
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.
|
|
14
|
+
"version": "0.3.1"
|
|
15
15
|
},
|
|
16
16
|
{
|
|
17
17
|
"name": "tasks",
|
|
18
18
|
"source": "./plugins/tasks",
|
|
19
19
|
"description": "Deterministic task tracking: the model registers tasks via the CLI, a UserPromptSubmit hook reminds of open tasks every few messages, and a SessionStart hook lists active tasks when a session opens.",
|
|
20
|
-
"version": "0.
|
|
20
|
+
"version": "0.3.1"
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
}
|
package/README.es.md
CHANGED
|
@@ -68,9 +68,10 @@ ejecución), así que funciona aunque instales uno suelto por fuera.
|
|
|
68
68
|
### 🔒 Security — bloqueos duros sobre lo destructivo
|
|
69
69
|
| Gate | | Qué hace |
|
|
70
70
|
|---|---|---|
|
|
71
|
-
| `bash-commands` | on | Bloquea `git reset --hard`, `rm -rf
|
|
71
|
+
| `bash-commands` | on | Bloquea `git reset --hard`, `rm -rf` sobre áreas protegidas, force push, y matar procesos por nombre. |
|
|
72
|
+
| `block-remote-publish` | on | Bloquea `git push`, `gh pr merge`, `gh release create` sin autorización. Poné `blockRemotePublish: false` para permitir que el agente publique por su cuenta. |
|
|
72
73
|
| `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
|
|
74
|
+
| `root-whitelist` | on | Bloquea crear archivos/carpetas nuevos en la raíz fuera de la lista blanca. |
|
|
74
75
|
| `no-blocking` | off | Bloquea `sleep`, `tail -f`, bucles de sondeo y servidores en primer plano. |
|
|
75
76
|
|
|
76
77
|
### 🤝 Delegation — exigencias sobre el brief al delegar a un subagente
|
|
@@ -80,7 +81,7 @@ ejecución), así que funciona aunque instales uno suelto por fuera.
|
|
|
80
81
|
| `intent-flow` | off | Exige secciones QUÉ SÍ / QUÉ NO / EDGE CASES. |
|
|
81
82
|
| `risk-level` | off | Exige declarar el nivel (QUESTION/MICRO/STANDARD/HIGH-RISK). |
|
|
82
83
|
| `circuit-breaker` | off | Corta la misma delegación reintentada sin cambios reales. |
|
|
83
|
-
| `no-memory-dependency` | off |
|
|
84
|
+
| `no-memory-dependency` | off | Bloquea un brief que depende de que el subagente "recuerde" la conversación (marcador `memory-not-needed` para un falso positivo). |
|
|
84
85
|
|
|
85
86
|
### 📋 Spec-driven flow — solo aplican si el proyecto adoptó desarrollo por specs
|
|
86
87
|
| Gate | | Qué hace |
|
|
@@ -94,15 +95,15 @@ ejecución), así que funciona aunque instales uno suelto por fuera.
|
|
|
94
95
|
### ✨ Quality — higiene de código, diagnóstico y lenguaje
|
|
95
96
|
| Gate | | Qué hace |
|
|
96
97
|
|---|---|---|
|
|
97
|
-
| `dependency-skills` | on |
|
|
98
|
+
| `dependency-skills` | on | Bloquea una dependencia directa nueva sin skill que la cubra (declarala en `depsWithoutOwnApi` si no necesita). |
|
|
98
99
|
| `root-cause-first` | off | Exige un diagnóstico origen→síntoma antes de un parche. |
|
|
99
100
|
| `audit-before-build` | off | Antes de un script/gate nuevo, exige declarar que nada existente lo cubre. |
|
|
100
101
|
| `never-assume` | off | Marca suposiciones sin verificar en briefs y código. |
|
|
101
102
|
| `rule-skill-autodiscovery` | off | Carga los gates que el proyecto declara en sus `rules/` y `skills/`. |
|
|
102
103
|
| `recurrence-lock` | on | La segunda aparición de un defecto exige su bloqueo determinista. |
|
|
103
|
-
| `test-after-implementation` | off |
|
|
104
|
+
| `test-after-implementation` | off | Bloquea un test escrito después de su implementación pareja (marcador `test-after-impl:allow` para un test de regresión). |
|
|
104
105
|
| `no-reconfirm` | on | Nunca vuelve a preguntar lo que ya respondiste. |
|
|
105
|
-
| `neutral-spanish` | on |
|
|
106
|
+
| `neutral-spanish` | on | Bloquea voseo o léxico regional en el texto escrito (marcador `neutral-spanish:allow` para una cita/fixture deliberada). |
|
|
106
107
|
| `diagnosis-before-patch` | on | Avisa cuando se cambian timeouts/reintentos sin evidencia. |
|
|
107
108
|
|
|
108
109
|
### 🔎 Tool discovery — no reinventar la rueda
|
package/README.md
CHANGED
|
@@ -60,14 +60,15 @@ works even if you install one on its own.
|
|
|
60
60
|
|
|
61
61
|
---
|
|
62
62
|
|
|
63
|
-
## The gates (
|
|
63
|
+
## The gates (in families)
|
|
64
64
|
|
|
65
65
|
`[on]` = enabled by default; `[off]` = enable it if you want it.
|
|
66
66
|
|
|
67
67
|
### 🔒 Security — hard blocks on destructive actions
|
|
68
68
|
| Gate | | What it does |
|
|
69
69
|
|---|---|---|
|
|
70
|
-
| `bash-commands` | on | Blocks `git reset --hard`, `rm -rf
|
|
70
|
+
| `bash-commands` | on | Blocks `git reset --hard`, `rm -rf` over protected areas, force push, and killing processes by name. |
|
|
71
|
+
| `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. |
|
|
71
72
|
| `protected-paths` | on | Blocks writes to `.env`, lockfiles and the harness itself. |
|
|
72
73
|
| `root-whitelist` | on | Blocks new root-level files/folders outside a whitelist. |
|
|
73
74
|
| `no-blocking` | off | Blocks `sleep`, `tail -f`, polling loops and foreground servers. |
|
|
@@ -79,7 +80,7 @@ works even if you install one on its own.
|
|
|
79
80
|
| `intent-flow` | off | Requires IN SCOPE / OUT OF SCOPE / EDGE CASES sections. |
|
|
80
81
|
| `risk-level` | off | Requires a declared level (QUESTION/MICRO/STANDARD/HIGH-RISK). |
|
|
81
82
|
| `circuit-breaker` | off | Cuts the same delegation retried without real changes. |
|
|
82
|
-
| `no-memory-dependency` | off |
|
|
83
|
+
| `no-memory-dependency` | off | Blocks a brief that relies on the subagent "remembering" the chat (add `memory-not-needed` to allow a false positive). |
|
|
83
84
|
|
|
84
85
|
### 📋 Spec-driven flow — only relevant if the project adopted spec-driven development
|
|
85
86
|
| Gate | | What it does |
|
|
@@ -93,15 +94,15 @@ works even if you install one on its own.
|
|
|
93
94
|
### ✨ Quality — code hygiene, diagnosis and language
|
|
94
95
|
| Gate | | What it does |
|
|
95
96
|
|---|---|---|
|
|
96
|
-
| `dependency-skills` | on |
|
|
97
|
+
| `dependency-skills` | on | Blocks a new direct dependency with no matching skill (declare it in `depsWithoutOwnApi` if it needs none). |
|
|
97
98
|
| `root-cause-first` | off | Requires an origin→symptom diagnosis before a patch. |
|
|
98
99
|
| `audit-before-build` | off | Before a new script/gate, requires stating that nothing existing covers it. |
|
|
99
100
|
| `never-assume` | off | Flags unverified assumptions in briefs and code. |
|
|
100
101
|
| `rule-skill-autodiscovery` | off | Loads gates the project declares in its `rules/` and `skills/`. |
|
|
101
102
|
| `recurrence-lock` | on | A second occurrence of a defect class requires its deterministic block. |
|
|
102
|
-
| `test-after-implementation` | off |
|
|
103
|
+
| `test-after-implementation` | off | Blocks a test written after its paired implementation (add `test-after-impl:allow` for a regression test). |
|
|
103
104
|
| `no-reconfirm` | on | Never re-ask what you already answered. |
|
|
104
|
-
| `neutral-spanish` | on |
|
|
105
|
+
| `neutral-spanish` | on | Blocks voseo or regional lexicon in written text (add `neutral-spanish:allow` for a deliberate quote/fixture). |
|
|
105
106
|
| `diagnosis-before-patch` | on | Warns when timeouts/retries change without evidence. |
|
|
106
107
|
|
|
107
108
|
### 🔎 Tool discovery — don't reinvent the wheel
|
|
@@ -141,9 +142,11 @@ see and edit every knob:
|
|
|
141
142
|
"rmRfProtectedAreas": ["/", "*", "src", "tests"],
|
|
142
143
|
"denyPatterns": ["git reset --hard", "…"]
|
|
143
144
|
},
|
|
145
|
+
"blockRemotePublish": { "enabled": false },
|
|
144
146
|
"warnNonNeutralSpanish": {
|
|
145
147
|
"enabled": true,
|
|
146
|
-
"regionalMarkers": ["tenés", "podés", "…"]
|
|
148
|
+
"regionalMarkers": ["tenés", "podés", "…"],
|
|
149
|
+
"escapeHatch": "neutral-spanish:allow"
|
|
147
150
|
},
|
|
148
151
|
"requireBriefBeforeDelegating": { "enabled": false }
|
|
149
152
|
}
|
|
@@ -155,6 +158,12 @@ see and edit every knob:
|
|
|
155
158
|
What the project declares **replaces** the gate's default.
|
|
156
159
|
- A gate absent from the config uses its catalog default. Keys you already had in the file
|
|
157
160
|
(e.g. `autoCommit`) are kept intact.
|
|
161
|
+
- **Let the agent push:** set `"blockRemotePublish": { "enabled": false }`. Nothing is
|
|
162
|
+
hardcoded — every gate, remote publish included, obeys this flag.
|
|
163
|
+
- **Escape hatches:** a few gates block (deny) but accept an explicit opt-out marker in the
|
|
164
|
+
content/prompt for a legitimate case: `neutral-spanish:allow` (a deliberate regional
|
|
165
|
+
quote), `test-after-impl:allow` (a regression test), `memory-not-needed` (a
|
|
166
|
+
non-memory phrase). `dependency-skills` opts out via its `depsWithoutOwnApi` list.
|
|
158
167
|
|
|
159
168
|
---
|
|
160
169
|
|
package/cli/index.mjs
CHANGED
|
@@ -78,6 +78,10 @@ program
|
|
|
78
78
|
)
|
|
79
79
|
.option('-y, --yes', 'never prompt; use flags and defaults')
|
|
80
80
|
.option('--no-install', 'write the config but do not install the plugin')
|
|
81
|
+
.option(
|
|
82
|
+
'--no-remove-previous',
|
|
83
|
+
'keep any previously installed plugin version instead of removing it first',
|
|
84
|
+
)
|
|
81
85
|
.option('--dry-run', 'show the selection without writing')
|
|
82
86
|
.action(async (options) => {
|
|
83
87
|
const { runInit } = await import('./init.mjs');
|
package/cli/init.mjs
CHANGED
|
@@ -68,6 +68,7 @@ export function normalizeOptions(options = {}) {
|
|
|
68
68
|
yes: Boolean(options.yes),
|
|
69
69
|
dryRun: Boolean(options.dryRun),
|
|
70
70
|
install: options.install,
|
|
71
|
+
removePrevious: options.removePrevious,
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
74
|
|
|
@@ -172,6 +173,15 @@ function renderSummary(registry, gatesMap) {
|
|
|
172
173
|
.join('\n');
|
|
173
174
|
}
|
|
174
175
|
|
|
176
|
+
async function askRemovePrevious(io) {
|
|
177
|
+
return bail(
|
|
178
|
+
await io.confirm({
|
|
179
|
+
message: 'Remove the previous plugin version before installing the new one?',
|
|
180
|
+
initialValue: true,
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
175
185
|
async function confirmWrite(io, fileExists) {
|
|
176
186
|
const confirmed = bail(
|
|
177
187
|
await io.confirm({
|
|
@@ -239,7 +249,14 @@ export async function runInit(
|
|
|
239
249
|
return { path, config: merged, written: true, installed: false };
|
|
240
250
|
}
|
|
241
251
|
|
|
242
|
-
|
|
252
|
+
// Default true: a previous plugin version is removed before installing the new one, unless
|
|
253
|
+
// --no-remove-previous turned it off. Only asked when interactive — --yes or no TTY uses the
|
|
254
|
+
// flag/default without a prompt, so scripted and non-interactive runs never block on input.
|
|
255
|
+
const removePrevious =
|
|
256
|
+
flags.removePrevious !== false &&
|
|
257
|
+
(!interactive || (await askRemovePrevious(io)));
|
|
258
|
+
|
|
259
|
+
const result = installPlugin(scope, { cwd, removePrevious });
|
|
243
260
|
if (result.installed) {
|
|
244
261
|
io.outro(
|
|
245
262
|
`Written ${path} and installed all plugins (${result.scope} scope). ` +
|
package/cli/install.mjs
CHANGED
|
@@ -88,6 +88,25 @@ function reasonFor(error) {
|
|
|
88
88
|
: (error?.stderr || error?.message || String(error)).trim().split('\n')[0];
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Best-effort removal of a previously installed version of each manifest plugin, at the
|
|
93
|
+
* given scope, before the fresh install runs. `claude plugin uninstall <plugin> --scope
|
|
94
|
+
* <scope>` is verified (via `claude plugin uninstall --help`) to take the bare plugin name
|
|
95
|
+
* (no `@marketplace`, unlike install) and the same `--scope` values as install. When there
|
|
96
|
+
* is no previous install, uninstall fails — that is expected and non-fatal, so failures here
|
|
97
|
+
* are swallowed and never stop the install that follows.
|
|
98
|
+
*/
|
|
99
|
+
function removePreviousInstalls(targets, scope, runClaude) {
|
|
100
|
+
for (const { plugin } of targets) {
|
|
101
|
+
try {
|
|
102
|
+
runClaude(['plugin', 'uninstall', plugin, '--scope', scope, '--yes']);
|
|
103
|
+
} catch {
|
|
104
|
+
// No previous install (or removal failed for some other reason) — non-fatal either way;
|
|
105
|
+
// the install below is what actually matters.
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
91
110
|
/**
|
|
92
111
|
* Registers the marketplace (idempotent: a second add just reports it already exists, which
|
|
93
112
|
* is not fatal) once, then installs EVERY plugin the manifest declares at the scope matching
|
|
@@ -102,10 +121,14 @@ function reasonFor(error) {
|
|
|
102
121
|
* `runClaude` is an injectable seam (defaults to the real `claude` binary) so tests can
|
|
103
122
|
* exercise the multi-plugin partial-failure logic without actually invoking the CLI and
|
|
104
123
|
* installing plugins on the machine running the test.
|
|
124
|
+
*
|
|
125
|
+
* `removePrevious` (default true) uninstalls each plugin's previous version at this scope
|
|
126
|
+
* before installing, so a stale version never lingers alongside the new one. It is best-effort
|
|
127
|
+
* and never fails the overall install.
|
|
105
128
|
*/
|
|
106
129
|
export function installPlugin(
|
|
107
130
|
configScope,
|
|
108
|
-
{ cwd = process.cwd(), runClaude = realClaude } = {},
|
|
131
|
+
{ cwd = process.cwd(), runClaude = realClaude, removePrevious = true } = {},
|
|
109
132
|
) {
|
|
110
133
|
const scope = PLUGIN_SCOPE[configScope] ?? 'user';
|
|
111
134
|
|
|
@@ -120,6 +143,8 @@ export function installPlugin(
|
|
|
120
143
|
// module top, means the name reflects the live registration this run just ensured.
|
|
121
144
|
const targets = marketplaceAndPlugins(runClaude);
|
|
122
145
|
|
|
146
|
+
if (removePrevious) removePreviousInstalls(targets, scope, runClaude);
|
|
147
|
+
|
|
123
148
|
const results = targets.map(({ marketplace, plugin }) => {
|
|
124
149
|
try {
|
|
125
150
|
runClaude([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devrik-tools/claude-gates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Installable, deterministic gates (hooks) for Claude Code: block destructive commands, protected paths, and enforce delegation/spec/quality rules. Configurable per project.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -33,7 +33,10 @@
|
|
|
33
33
|
"README.md",
|
|
34
34
|
"README.es.md",
|
|
35
35
|
"plugins",
|
|
36
|
-
".claude-plugin"
|
|
36
|
+
".claude-plugin",
|
|
37
|
+
"!**/*.test.mjs",
|
|
38
|
+
"!**/test.mjs",
|
|
39
|
+
"!**/__tests__/**"
|
|
37
40
|
],
|
|
38
41
|
"scripts": {
|
|
39
42
|
"test": "node --test",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gates",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Deterministic gates for Claude Code: destructive-command blocks, protected paths, delegation briefs, spec-driven flow and session-start validation. Selection lives in config, not in code.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Devrik"
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// autonomous-mode (Stop half) — closes the hole its PreToolUse half cannot reach. The
|
|
2
|
+
// PreToolUse gate (index.mjs) blocks the AskUserQuestion TOOL, but nothing stops the
|
|
3
|
+
// assistant from ending its turn with a question in plain PROSE — and when it does, the
|
|
4
|
+
// session just sits there waiting for a human who is supposed to be away. No hook can read
|
|
5
|
+
// chat text to detect that a turn ended on a question. So this Stop hook takes the only
|
|
6
|
+
// deterministic lever available: when autonomous mode is ON and the turn is ending, it
|
|
7
|
+
// re-injects an instruction to NOT wait — decide, delegate, and keep going on everything
|
|
8
|
+
// that does not strictly require the user's own explicit decision.
|
|
9
|
+
//
|
|
10
|
+
// ── What the re-injected message says (per the user's own framing) ───────────────────
|
|
11
|
+
// In autonomous mode work is delegated to the assistant. It must only leave pending the ONE
|
|
12
|
+
// thing that genuinely needs the user's exclusive, explicit decision (irreversible/dangerous
|
|
13
|
+
// — deleting data, money, production). Everything else: proceed. If there are OTHER tasks it
|
|
14
|
+
// can advance, advance them; isolate only the truly blocking one and state it in prose, then
|
|
15
|
+
// stop. It must not end the turn on a prose question just to wait.
|
|
16
|
+
//
|
|
17
|
+
// ── stop_hook_active loop-guard (mandatory) ──────────────────────────────────────────
|
|
18
|
+
// Claude Code re-invokes Stop hooks with stop_hook_active=true after a block. Blocking again
|
|
19
|
+
// would loop forever. So when stop_hook_active === true this hook unconditionally allows the
|
|
20
|
+
// stop: the reminder is injected exactly once per cycle. After that one nudge, if the
|
|
21
|
+
// assistant still ends the turn, it genuinely had nothing to proceed on (or a real blocker),
|
|
22
|
+
// and the session must be allowed to end — a frozen session is worse than one missed nudge.
|
|
23
|
+
//
|
|
24
|
+
// ── Fail-safe (mandatory for a Stop hook) ────────────────────────────────────────────
|
|
25
|
+
// Everything is wrapped so that no config, no project, a corrupt payload, or any unexpected
|
|
26
|
+
// error allows the stop. A broken Stop hook must never hang a session.
|
|
27
|
+
|
|
28
|
+
import { isGateEnabled } from '../../lib/config.mjs';
|
|
29
|
+
import { readHookPayload } from '../../lib/hook-io.mjs';
|
|
30
|
+
|
|
31
|
+
const CONFIG_KEY = 'autonomousMode';
|
|
32
|
+
|
|
33
|
+
const CONTINUE_MESSAGE =
|
|
34
|
+
'[autonomous] Autonomous mode is ON — do not end the turn to wait for the user. Work here ' +
|
|
35
|
+
'is delegated to you. Leave PENDING only the one thing that genuinely needs the user’s own ' +
|
|
36
|
+
'explicit, exclusive decision (a truly irreversible or dangerous choice: deleting data, ' +
|
|
37
|
+
'money, production). For everything else: decide the best goal-aligned option, state the ' +
|
|
38
|
+
'reversible assumption in one line, and proceed. If there are OTHER tasks you can advance, ' +
|
|
39
|
+
'advance them now instead of stopping; isolate only the genuinely blocking item, state it ' +
|
|
40
|
+
'in prose, and stop only for that. Do NOT end the turn on a prose question just to wait. ' +
|
|
41
|
+
'If you have truly nothing left to do and nothing needs the user, ending is fine — this ' +
|
|
42
|
+
'reminder fires once, so a real end will go through on the next stop.';
|
|
43
|
+
|
|
44
|
+
function block(reason) {
|
|
45
|
+
process.stdout.write(JSON.stringify({ decision: 'block', reason }));
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function allow() {
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function main() {
|
|
54
|
+
try {
|
|
55
|
+
const rawPayload = readHookPayload();
|
|
56
|
+
if (rawPayload === null) return allow();
|
|
57
|
+
|
|
58
|
+
let payload;
|
|
59
|
+
try {
|
|
60
|
+
payload = JSON.parse(rawPayload);
|
|
61
|
+
} catch {
|
|
62
|
+
return allow(); // unparseable Stop payload: fail-safe, never hang the session
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Loop-guard: this hook already nudged once this cycle. Never block again.
|
|
66
|
+
if (payload?.stop_hook_active === true) return allow();
|
|
67
|
+
|
|
68
|
+
// Only act when the project turned autonomous mode ON. Off by default (registry).
|
|
69
|
+
if (!isGateEnabled(CONFIG_KEY, false, process.cwd())) return allow();
|
|
70
|
+
|
|
71
|
+
return block(CONTINUE_MESSAGE);
|
|
72
|
+
} catch {
|
|
73
|
+
// A broken Stop hook must never hang a session.
|
|
74
|
+
return allow();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
main();
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
// bash-commands — denies destructive shell commands
|
|
2
|
-
//
|
|
3
|
-
// prompt (a subagent can be told "run git reset --hard" in prose).
|
|
1
|
+
// bash-commands — denies destructive shell commands (rm -rf over protected areas, git
|
|
2
|
+
// reset --hard, force push, kill-by-name). Runs on a real Bash/run_command call and on a
|
|
3
|
+
// delegation prompt (a subagent can be told "run git reset --hard" in prose).
|
|
4
|
+
//
|
|
5
|
+
// Remote-publish blocking (git push / gh pr merge / gh release create) used to live here as
|
|
6
|
+
// a hardcoded, non-configurable block. It was split out into the block-remote-publish gate
|
|
7
|
+
// so it carries its own enabled flag: a project can now allow the agent to push by disabling
|
|
8
|
+
// that gate WITHOUT also disabling the destructive-command protections below.
|
|
4
9
|
//
|
|
5
10
|
// ── What a project can configure (params) ───────────────────────────────────────────
|
|
6
11
|
// denyPatterns regex sources (matched case-insensitively) of destructive
|
|
@@ -10,18 +15,6 @@
|
|
|
10
15
|
// by default: this harness legitimately uses inline interpreters.
|
|
11
16
|
// The defaults live here, in the source, so a project reads them and knows exactly what
|
|
12
17
|
// its override replaces.
|
|
13
|
-
//
|
|
14
|
-
// ── What is NOT configurable (base, non-negotiable) ─────────────────────────────────
|
|
15
|
-
// Remote-publish detection (`git push`, `gh pr merge`, `gh release create`) is a hard
|
|
16
|
-
// block with no config knob and no in-command escape hatch: authorization is something
|
|
17
|
-
// the USER states in chat, never a token the agent could write into its own command.
|
|
18
|
-
//
|
|
19
|
-
// ── Two-stage intent check, only for delegation prompts ─────────────────────────────
|
|
20
|
-
// A real command's `command` field IS what the shell runs — a single-stage regex is
|
|
21
|
-
// right. A delegation prompt is natural language that may DESCRIBE a command ("I extended
|
|
22
|
-
// the guard to deny git push") without asking anyone to run it. There, publish rules run
|
|
23
|
-
// a second stage: strip quoted/example text, then require that at least one surviving
|
|
24
|
-
// mention is not governed by a reporting verb before denying.
|
|
25
18
|
|
|
26
19
|
import {
|
|
27
20
|
runGate,
|
|
@@ -107,22 +100,6 @@ function defaultDenyPatterns() {
|
|
|
107
100
|
];
|
|
108
101
|
}
|
|
109
102
|
|
|
110
|
-
/** Remote-publish rules. Base, non-configurable — see the header. */
|
|
111
|
-
const REMOTE_PUBLISH_RULES = [
|
|
112
|
-
[
|
|
113
|
-
String.raw`\bgit\s+(?:-C\s+\S+\s+)?push\b`,
|
|
114
|
-
"Publishing to a remote ('git push') needs fresh authorization from the user naming " +
|
|
115
|
-
'commits, local branch, remote and target. This gate cannot verify that from the ' +
|
|
116
|
-
'command itself — ask the user and let them run it.',
|
|
117
|
-
],
|
|
118
|
-
[
|
|
119
|
-
String.raw`\bgh\s+(?:pr\s+merge|release\s+create)\b`,
|
|
120
|
-
'Publishing via GitHub CLI (merging a PR or creating a release) needs fresh ' +
|
|
121
|
-
'authorization from the user naming commits, local branch, remote and target. ' +
|
|
122
|
-
'Ask the user and let them run it.',
|
|
123
|
-
],
|
|
124
|
-
];
|
|
125
|
-
|
|
126
103
|
// Interpreter name, then any intermediate flags (e.g. --input-type=module), then an
|
|
127
104
|
// eval flag. Intermediate flags are matched loosely (`-\S+`) to keep the pattern simple.
|
|
128
105
|
const INTERPRETER_EVAL_PATTERN =
|
|
@@ -130,14 +107,6 @@ const INTERPRETER_EVAL_PATTERN =
|
|
|
130
107
|
const RAW_FILE_OPS_PATTERN =
|
|
131
108
|
/\b(writeFileSync|readFileSync|appendFileSync|fs\.writeFile|fs\.readFile|fs\.unlink|fs\.mkdir)\b/;
|
|
132
109
|
|
|
133
|
-
// How far back from a mention to look for a governing verb, and the reporting-verb lexicon
|
|
134
|
-
// that marks a mention as description rather than an order. Only the text before a mention
|
|
135
|
-
// is inspected: what governs it is what precedes it (appending words after a real command
|
|
136
|
-
// would otherwise be a trivial bypass).
|
|
137
|
-
const DESCRIPTION_LOOK_BACK = 80;
|
|
138
|
-
const REPORTING_VERB_PATTERN =
|
|
139
|
-
/(describe|explain|summar|mention|added|built|extended|denies?|deny|prohibit|protection|report|documentation|changelog)/i;
|
|
140
|
-
|
|
141
110
|
function compile(source) {
|
|
142
111
|
return new RegExp(source, 'i');
|
|
143
112
|
}
|
|
@@ -170,29 +139,6 @@ function normalizeGitOptions(command) {
|
|
|
170
139
|
return normalized;
|
|
171
140
|
}
|
|
172
141
|
|
|
173
|
-
function stripQuoted(text) {
|
|
174
|
-
return text
|
|
175
|
-
.replace(/```[\s\S]*?```/g, ' ')
|
|
176
|
-
.replace(/"[^"\n]{0,300}"/g, ' ')
|
|
177
|
-
.replace(/'[^'\n]{0,300}'/g, ' ');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** True when a surviving, non-quoted mention is not governed by a reporting verb. */
|
|
181
|
-
function hasRealPublishIntent(text, pattern) {
|
|
182
|
-
if (!pattern.test(text)) return false;
|
|
183
|
-
const cleaned = stripQuoted(text);
|
|
184
|
-
const global = new RegExp(
|
|
185
|
-
pattern.source,
|
|
186
|
-
`${pattern.flags.replace('g', '')}g`,
|
|
187
|
-
);
|
|
188
|
-
const matches = [...cleaned.matchAll(global)];
|
|
189
|
-
if (matches.length === 0) return false;
|
|
190
|
-
return matches.some((match) => {
|
|
191
|
-
const from = Math.max(0, match.index - DESCRIPTION_LOOK_BACK);
|
|
192
|
-
return !REPORTING_VERB_PATTERN.test(cleaned.slice(from, match.index));
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
142
|
function checkEmbeddedInterpreter(command) {
|
|
197
143
|
const match = INTERPRETER_EVAL_PATTERN.exec(command);
|
|
198
144
|
if (!match) return null;
|
|
@@ -247,21 +193,6 @@ function checkDestructive(command, parameters) {
|
|
|
247
193
|
}
|
|
248
194
|
}
|
|
249
195
|
|
|
250
|
-
/** Remote-publish rules: a real command is checked literally; a delegation prompt by intent. */
|
|
251
|
-
function checkRemotePublish(command, isShell) {
|
|
252
|
-
// For a real shell command, normalize git's global options first (same bypass as above).
|
|
253
|
-
// For a delegation prompt (free text), the intent check runs on the raw text.
|
|
254
|
-
const shellCommand = normalizeGitOptions(command);
|
|
255
|
-
for (const [source, reason] of REMOTE_PUBLISH_RULES) {
|
|
256
|
-
const pattern = compile(source);
|
|
257
|
-
if (isShell) {
|
|
258
|
-
if (pattern.test(shellCommand)) deny(GATE_ID, reason);
|
|
259
|
-
} else if (hasRealPublishIntent(command, pattern)) {
|
|
260
|
-
deny(GATE_ID, reason);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
196
|
runGate(
|
|
266
197
|
{
|
|
267
198
|
id: GATE_ID,
|
|
@@ -274,12 +205,11 @@ runGate(
|
|
|
274
205
|
},
|
|
275
206
|
},
|
|
276
207
|
({ toolName, toolInput, parameters }) => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (!
|
|
208
|
+
// Destructive-command rules act on a real shell command AND on a delegation prompt (a
|
|
209
|
+
// subagent can be told "run rm -rf" in prose). commandTextFrom picks the right text.
|
|
210
|
+
if (!toolInGroups(toolName, ['shell', 'delegation'])) return;
|
|
280
211
|
|
|
281
212
|
const command = commandTextFrom(toolName, toolInput);
|
|
282
213
|
checkDestructive(command, parameters);
|
|
283
|
-
checkRemotePublish(command, isShell);
|
|
284
214
|
},
|
|
285
215
|
);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// block-remote-publish — denies publishing to a remote (`git push`, `gh pr merge`,
|
|
2
|
+
// `gh release create`) without fresh human authorization. Split out of bash-commands so it
|
|
3
|
+
// carries its OWN enabled flag: a project can turn remote-publish blocking off in config
|
|
4
|
+
// (blockRemotePublish: false) WITHOUT losing the destructive-command protections (rm -rf,
|
|
5
|
+
// git reset --hard) that live in bash-commands. Nothing is hardcoded any more — this gate
|
|
6
|
+
// obeys its flag like every other, and appears in the CLI's gate selector on its own.
|
|
7
|
+
//
|
|
8
|
+
// Default: enabled. The safe posture is that the human states the authorization in chat,
|
|
9
|
+
// naming commits, branch, remote and target — a project that wants the agent to push on its
|
|
10
|
+
// own opts in explicitly by disabling this gate.
|
|
11
|
+
//
|
|
12
|
+
// ── Two-stage intent check, only for delegation prompts ─────────────────────────────
|
|
13
|
+
// A real command's `command` field IS what the shell runs — a single-stage regex is right.
|
|
14
|
+
// A delegation prompt is natural language that may DESCRIBE a command ("I extended the guard
|
|
15
|
+
// to deny git push") without asking anyone to run it. There, publish rules run a second
|
|
16
|
+
// stage: strip quoted/example text, then require that at least one surviving mention is not
|
|
17
|
+
// governed by a reporting verb before denying.
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
runGate,
|
|
21
|
+
deny,
|
|
22
|
+
toolInGroups,
|
|
23
|
+
delegationPromptOf,
|
|
24
|
+
} from '../../lib/hook-io.mjs';
|
|
25
|
+
|
|
26
|
+
const GATE_ID = 'block-remote-publish';
|
|
27
|
+
const CONFIG_KEY = 'blockRemotePublish';
|
|
28
|
+
|
|
29
|
+
// Remote-publish rules as `[regexSource, reason]`. Sources (not RegExp) so a project could
|
|
30
|
+
// override them through config if it ever needed to.
|
|
31
|
+
function defaultPublishRules() {
|
|
32
|
+
return [
|
|
33
|
+
[
|
|
34
|
+
String.raw`\bgit\s+(?:-C\s+\S+\s+)?push\b`,
|
|
35
|
+
"Publishing to a remote ('git push') needs fresh authorization from the user naming " +
|
|
36
|
+
'commits, local branch, remote and target. This gate cannot verify that from the ' +
|
|
37
|
+
'command itself — ask the user and let them run it. To let the agent push on its ' +
|
|
38
|
+
'own, set "blockRemotePublish": false in .ai/config.json.',
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
String.raw`\bgh\s+(?:pr\s+merge|release\s+create)\b`,
|
|
42
|
+
'Publishing via GitHub CLI (merging a PR or creating a release) needs fresh ' +
|
|
43
|
+
'authorization from the user naming commits, local branch, remote and target. ' +
|
|
44
|
+
'Ask the user and let them run it. To let the agent publish on its own, set ' +
|
|
45
|
+
'"blockRemotePublish": false in .ai/config.json.',
|
|
46
|
+
],
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// How far back from a mention to look for a governing verb, and the reporting-verb lexicon
|
|
51
|
+
// that marks a mention as description rather than an order. Only the text before a mention
|
|
52
|
+
// is inspected: what governs it is what precedes it (appending words after a real command
|
|
53
|
+
// would otherwise be a trivial bypass).
|
|
54
|
+
const DESCRIPTION_LOOK_BACK = 80;
|
|
55
|
+
const REPORTING_VERB_PATTERN =
|
|
56
|
+
/(describe|explain|summar|mention|added|built|extended|denies?|deny|prohibit|protection|report|documentation|changelog)/i;
|
|
57
|
+
|
|
58
|
+
function compile(source) {
|
|
59
|
+
return new RegExp(source, 'i');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// git's GLOBAL options sit between `git` and the subcommand: `git -C <path> push`, etc. A
|
|
63
|
+
// pattern that matches `git push` contiguously is evaded by any of them. Stripping these
|
|
64
|
+
// first — turning `git -C /repo push` back into `git push` — closes that bypass at once.
|
|
65
|
+
const GIT_OPTION_WITH_VALUE = String.raw`(?:-[Cc]|--git-dir|--work-tree|--namespace|--exec-path|--config-env)(?:\s+|=)\S+`;
|
|
66
|
+
const GIT_FLAG_OPTION = String.raw`--(?:paginate|no-pager|bare|no-optional-locks)|-p`;
|
|
67
|
+
const GIT_GLOBAL_OPTION_PATTERN = new RegExp(
|
|
68
|
+
String.raw`\bgit\s+(?:${GIT_OPTION_WITH_VALUE}|${GIT_FLAG_OPTION})\s+`,
|
|
69
|
+
'i',
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
function normalizeGitOptions(command) {
|
|
73
|
+
let previous;
|
|
74
|
+
let normalized = command;
|
|
75
|
+
do {
|
|
76
|
+
previous = normalized;
|
|
77
|
+
normalized = normalized.replace(GIT_GLOBAL_OPTION_PATTERN, 'git ');
|
|
78
|
+
} while (normalized !== previous);
|
|
79
|
+
return normalized;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stripQuoted(text) {
|
|
83
|
+
return text
|
|
84
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
85
|
+
.replace(/"[^"\n]{0,300}"/g, ' ')
|
|
86
|
+
.replace(/'[^'\n]{0,300}'/g, ' ');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** True when a surviving, non-quoted mention is not governed by a reporting verb. */
|
|
90
|
+
function hasRealPublishIntent(text, pattern) {
|
|
91
|
+
if (!pattern.test(text)) return false;
|
|
92
|
+
const cleaned = stripQuoted(text);
|
|
93
|
+
const global = new RegExp(
|
|
94
|
+
pattern.source,
|
|
95
|
+
`${pattern.flags.replace('g', '')}g`,
|
|
96
|
+
);
|
|
97
|
+
const matches = [...cleaned.matchAll(global)];
|
|
98
|
+
if (matches.length === 0) return false;
|
|
99
|
+
return matches.some((match) => {
|
|
100
|
+
const from = Math.max(0, match.index - DESCRIPTION_LOOK_BACK);
|
|
101
|
+
return !REPORTING_VERB_PATTERN.test(cleaned.slice(from, match.index));
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The text to inspect: a real command's command line, or the delegation prompt. */
|
|
106
|
+
function commandTextFrom(toolName, toolInput) {
|
|
107
|
+
if (toolInGroups(toolName, ['shell'])) {
|
|
108
|
+
return String(toolInput.CommandLine ?? toolInput.command ?? '');
|
|
109
|
+
}
|
|
110
|
+
return delegationPromptOf(toolInput);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
runGate(
|
|
114
|
+
{
|
|
115
|
+
id: GATE_ID,
|
|
116
|
+
configKey: CONFIG_KEY,
|
|
117
|
+
enabledByDefault: true,
|
|
118
|
+
defaultParams: {
|
|
119
|
+
publishRules: defaultPublishRules(),
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
({ toolName, toolInput, parameters }) => {
|
|
123
|
+
const isShell = toolInGroups(toolName, ['shell']);
|
|
124
|
+
const isDelegation = toolInGroups(toolName, ['delegation']);
|
|
125
|
+
if (!isShell && !isDelegation) return;
|
|
126
|
+
|
|
127
|
+
const command = commandTextFrom(toolName, toolInput);
|
|
128
|
+
// For a real shell command, normalize git's global options first. For a delegation
|
|
129
|
+
// prompt (free text), the intent check runs on the raw text.
|
|
130
|
+
const shellCommand = normalizeGitOptions(command);
|
|
131
|
+
for (const [source, reason] of parameters.publishRules) {
|
|
132
|
+
const pattern = compile(source);
|
|
133
|
+
if (isShell) {
|
|
134
|
+
if (pattern.test(shellCommand)) deny(GATE_ID, reason);
|
|
135
|
+
} else if (hasRealPublishIntent(command, pattern)) {
|
|
136
|
+
deny(GATE_ID, reason);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
);
|