@devrik-tools/claude-gates 0.1.1 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrik-tools/claude-gates",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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",
@@ -0,0 +1,45 @@
1
+ // autonomous-mode — when the project turns on autonomous mode, the assistant must stop
2
+ // asking and decide. This gate denies the AskUserQuestion tool while the mode is on, so a
3
+ // question popup cannot interrupt an unattended run. The assistant is expected to take the
4
+ // best, aligned decision and state the reversible assumption instead of asking.
5
+ //
6
+ // justification: no existing tool covers this. It is the deterministic half of "run without
7
+ // asking": a gate can block the question tool; the prose reminder (via the session-start
8
+ // hook and this gate's message) covers the rest, which a hook cannot force.
9
+ //
10
+ // ── When it acts ────────────────────────────────────────────────────────────────────
11
+ // Only when autonomousMode is enabled in .ai/config.json (project) or the global config.
12
+ // Off by default. The user is also reminded at session start that the mode is on (see the
13
+ // session hook), in case they forgot to turn it off.
14
+ //
15
+ // ── What it does NOT do ─────────────────────────────────────────────────────────────
16
+ // It blocks the AskUserQuestion TOOL. It cannot stop the assistant from asking in plain
17
+ // prose (no hook sees chat text) — the injected reminder pushes against that, but the only
18
+ // deterministic lever is the question tool, and this gate pulls it.
19
+
20
+ import { runGate, deny, TOOL_GROUPS } from '../../lib/hook-io.mjs';
21
+
22
+ const GATE_ID = 'autonomous-mode';
23
+ const CONFIG_KEY = 'autonomousMode';
24
+
25
+ const QUESTION_TOOLS = new Set(TOOL_GROUPS.question);
26
+
27
+ const DENY_MESSAGE =
28
+ 'Autonomous mode is ON for this project: do not ask the user. Take the best decision ' +
29
+ 'that is aligned with the goal and the project rules, state the reversible assumption you ' +
30
+ 'made, and proceed. Only a genuinely irreversible or dangerous choice (deleting data, ' +
31
+ 'money, production) would justify stopping — and then say so in prose, do not use the ' +
32
+ 'question popup. To let questions through again, set "autonomousMode": false in ' +
33
+ '.ai/config.json.';
34
+
35
+ runGate(
36
+ {
37
+ id: GATE_ID,
38
+ configKey: CONFIG_KEY,
39
+ enabledByDefault: false,
40
+ },
41
+ ({ toolName }) => {
42
+ if (!QUESTION_TOOLS.has(toolName)) return;
43
+ deny(GATE_ID, DENY_MESSAGE);
44
+ },
45
+ );
@@ -0,0 +1,58 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync } from 'node:child_process';
3
+ import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { test } from 'node:test';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const GATE = join(dirname(fileURLToPath(import.meta.url)), 'index.mjs');
10
+
11
+ // Runs the gate in a temp project with HOME isolated (so the real global config is not
12
+ // read). `autonomous` writes the flag on. Returns the parsed deny output, or null.
13
+ function runGate(payload, { autonomous } = {}) {
14
+ const project = mkdtempSync(join(tmpdir(), 'autonomous-mode-'));
15
+ mkdirSync(join(project, '.git'));
16
+ mkdirSync(join(project, '.ai'));
17
+ writeFileSync(
18
+ join(project, '.ai', 'config.json'),
19
+ JSON.stringify({
20
+ gates: { autonomousMode: { enabled: Boolean(autonomous) } },
21
+ }),
22
+ );
23
+ const out = execFileSync(process.execPath, [GATE], {
24
+ input: JSON.stringify(payload),
25
+ encoding: 'utf8',
26
+ cwd: project,
27
+ env: { ...process.env, HOME: project, USERPROFILE: project },
28
+ });
29
+ return out.trim() ? JSON.parse(out.trim()) : null;
30
+ }
31
+
32
+ function ask() {
33
+ return {
34
+ tool_name: 'AskUserQuestion',
35
+ tool_input: { questions: [{ question: 'A or B?' }] },
36
+ };
37
+ }
38
+ function isDeny(result) {
39
+ return result?.hookSpecificOutput?.permissionDecision === 'deny';
40
+ }
41
+
42
+ test('autonomous mode ON: AskUserQuestion is denied', () => {
43
+ assert.ok(isDeny(runGate(ask(), { autonomous: true })));
44
+ });
45
+
46
+ test('autonomous mode OFF (default): questions pass', () => {
47
+ assert.equal(runGate(ask(), { autonomous: false }), null);
48
+ });
49
+
50
+ test('even ON, a non-question tool is never blocked', () => {
51
+ assert.equal(
52
+ runGate(
53
+ { tool_name: 'Write', tool_input: { file_path: 'x.js', content: '' } },
54
+ { autonomous: true },
55
+ ),
56
+ null,
57
+ );
58
+ });
@@ -260,6 +260,26 @@
260
260
  "timeout": 10
261
261
  }
262
262
  ]
263
+ },
264
+ {
265
+ "matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command",
266
+ "hooks": [
267
+ {
268
+ "type": "command",
269
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/forge-flow/index.mjs\"",
270
+ "timeout": 10
271
+ }
272
+ ]
273
+ },
274
+ {
275
+ "matcher": "AskUserQuestion",
276
+ "hooks": [
277
+ {
278
+ "type": "command",
279
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/autonomous-mode/index.mjs\"",
280
+ "timeout": 10
281
+ }
282
+ ]
263
283
  }
264
284
  ]
265
285
  }
package/registry.json CHANGED
@@ -630,6 +630,24 @@
630
630
  }
631
631
  ]
632
632
  },
633
+ {
634
+ "id": "autonomy",
635
+ "name": "Autonomy",
636
+ "description": "Autonomous mode: block the question popup so an unattended run decides instead of asking.",
637
+ "gates": [
638
+ {
639
+ "id": "autonomous-mode",
640
+ "configKey": "autonomousMode",
641
+ "default": false,
642
+ "event": "PreToolUse",
643
+ "tools": [
644
+ "question"
645
+ ],
646
+ "description": "When autonomous mode is on, denies AskUserQuestion so the assistant decides instead of asking.",
647
+ "script": "gates/autonomous-mode/index.mjs"
648
+ }
649
+ ]
650
+ },
633
651
  {
634
652
  "id": "session",
635
653
  "name": "Session start",