@devrik-tools/claude-gates 0.2.0 → 0.2.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/package.json +1 -1
- package/plugins/gates/.claude-plugin/plugin.json +1 -1
- package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +8 -6
- package/plugins/gates/hooks/gates/root-whitelist/index.mjs +59 -44
- package/plugins/gates/hooks/gates/root-whitelist/root-whitelist.edge.test.mjs +27 -0
- package/plugins/gates/hooks/hooks.json +346 -346
- package/plugins/gates/hooks/lib/__tests__/hook-io.test.mjs +14 -1
- package/plugins/gates/hooks/lib/hook-io.mjs +39 -0
- package/plugins/tasks/.claude-plugin/plugin.json +1 -1
|
@@ -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.2.
|
|
14
|
+
"version": "0.2.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.2.
|
|
20
|
+
"version": "0.2.1"
|
|
21
21
|
}
|
|
22
22
|
]
|
|
23
23
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devrik-tools/claude-gates",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gates",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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"
|
|
@@ -28,12 +28,14 @@ const CONFIG_KEY = 'autonomousMode';
|
|
|
28
28
|
const QUESTION_GROUPS = ['question'];
|
|
29
29
|
|
|
30
30
|
const DENY_MESSAGE =
|
|
31
|
-
'Autonomous mode is ON for this project: do not ask the user
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
31
|
+
'Autonomous mode is ON for this project: do not ask the user, decide instead. The question ' +
|
|
32
|
+
'popup was intercepted and never shown. Now, in your VISIBLE reply, leave a one-line trace ' +
|
|
33
|
+
'so the user sees what was suppressed and what you chose — e.g. "[autonomous] I was going ' +
|
|
34
|
+
'to ask <the question>; I decided <choice>, assuming <reversible assumption>." Then take ' +
|
|
35
|
+
'that best, goal-aligned decision and proceed. Only a genuinely irreversible or dangerous ' +
|
|
36
|
+
'choice (deleting data, money, production) justifies stopping — and then stop in prose ' +
|
|
37
|
+
'saying exactly what and why, do not reach for the popup again. To let questions through, ' +
|
|
38
|
+
'set "autonomousMode": false in .ai/config.json.';
|
|
37
39
|
|
|
38
40
|
runGate(
|
|
39
41
|
{
|
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
// dotfiles are a different, well-known convention this gate does not police.
|
|
15
15
|
|
|
16
16
|
import { basename, isAbsolute, resolve, sep } from 'node:path';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
runGate,
|
|
19
|
+
deny,
|
|
20
|
+
toolInGroups,
|
|
21
|
+
writtenPathOf,
|
|
22
|
+
shellWrittenPaths,
|
|
23
|
+
} from '../../lib/hook-io.mjs';
|
|
18
24
|
|
|
19
25
|
const GATE_ID = 'root-whitelist';
|
|
20
26
|
const CONFIG_KEY = 'blockPathsOutsideRootWhitelist';
|
|
@@ -49,6 +55,40 @@ const DEFAULT_ROOT_FOLDERS_WHITELIST = [
|
|
|
49
55
|
'plugins',
|
|
50
56
|
];
|
|
51
57
|
|
|
58
|
+
// The whitelist verdict for one target path. Returns a deny reason string when the path is a
|
|
59
|
+
// non-whitelisted new thing at the project root, or null when it is allowed (outside the root,
|
|
60
|
+
// a dotfile, or whitelisted). Shared by the write-tool path and every shell-created path, so a
|
|
61
|
+
// `printf x > basura.txt` is judged by the same rule as a Write to basura.txt.
|
|
62
|
+
function rootWhitelistViolation(target, filesWhitelist, foldersWhitelist) {
|
|
63
|
+
if (!target) return null;
|
|
64
|
+
|
|
65
|
+
// Some tools send a path relative to cwd rather than absolute. resolve() leaves an
|
|
66
|
+
// already-absolute path merely normalized ('..' collapsed, separators fixed), and resolves
|
|
67
|
+
// a relative one against process.cwd() — so either shape lands on the same absolute path.
|
|
68
|
+
const normalized = isAbsolute(target)
|
|
69
|
+
? resolve(target)
|
|
70
|
+
: resolve(process.cwd(), target);
|
|
71
|
+
const projectRoot = process.cwd() + sep;
|
|
72
|
+
|
|
73
|
+
// A path outside the project (scratchpad, temp, another repo) is not at its root — not this
|
|
74
|
+
// rule's business. Judged AFTER resolving, so a relative path is judged by where it lands.
|
|
75
|
+
if (!normalized.startsWith(projectRoot)) return null;
|
|
76
|
+
|
|
77
|
+
const relativePath = normalized.slice(projectRoot.length);
|
|
78
|
+
|
|
79
|
+
if (!relativePath.includes(sep)) {
|
|
80
|
+
const fileName = basename(relativePath);
|
|
81
|
+
if (fileName.startsWith('.') || filesWhitelist.has(fileName.toLowerCase())) return null;
|
|
82
|
+
return `'${fileName}' at the project root is not on the whitelist (${[...filesWhitelist].join(', ')}).`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const topDirectory = relativePath.split(sep)[0];
|
|
86
|
+
if (topDirectory.startsWith('.') || foldersWhitelist.has(topDirectory.toLowerCase())) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return `Folder '${topDirectory}' at the project root is not on the whitelist (${[...foldersWhitelist].join(', ')}).`;
|
|
90
|
+
}
|
|
91
|
+
|
|
52
92
|
runGate(
|
|
53
93
|
{
|
|
54
94
|
id: GATE_ID,
|
|
@@ -60,30 +100,12 @@ runGate(
|
|
|
60
100
|
},
|
|
61
101
|
},
|
|
62
102
|
({ toolName, toolInput, parameters }) => {
|
|
63
|
-
|
|
103
|
+
const isWrite = toolInGroups(toolName, ['write']);
|
|
104
|
+
const isShell = toolInGroups(toolName, ['shell']);
|
|
105
|
+
if (!isWrite && !isShell) return;
|
|
64
106
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Some tools send a path relative to cwd rather than absolute. resolve() leaves an
|
|
69
|
-
// already-absolute path merely normalized ('..' collapsed, separators fixed), and
|
|
70
|
-
// resolves a relative one against process.cwd() — so either shape lands on the same
|
|
71
|
-
// absolute path the root check below expects.
|
|
72
|
-
const normalized = isAbsolute(target)
|
|
73
|
-
? resolve(target)
|
|
74
|
-
: resolve(process.cwd(), target);
|
|
75
|
-
const projectRoot = process.cwd() + sep;
|
|
76
|
-
|
|
77
|
-
// The whitelist describes the project's ROOT. A file outside the project (session
|
|
78
|
-
// scratchpad, system temp, another repo) is not at its root, and this rule does not
|
|
79
|
-
// govern it: letting it through is correct, not an exception. This check runs AFTER
|
|
80
|
-
// resolving relative paths, so a relative path is judged by where it actually lands.
|
|
81
|
-
if (!normalized.startsWith(projectRoot)) return;
|
|
82
|
-
|
|
83
|
-
const relativePath = normalized.slice(projectRoot.length);
|
|
84
|
-
// Windows filesystems are case-insensitive: 'Package.json' and 'package.json' are the
|
|
85
|
-
// same file. Comparing lowercase on both sides avoids denying a whitelisted name that
|
|
86
|
-
// merely differs in case (a false positive, not a real gap).
|
|
107
|
+
// Windows filesystems are case-insensitive: 'Package.json' and 'package.json' are the same
|
|
108
|
+
// file. Comparing lowercase on both sides avoids a false positive on a case difference.
|
|
87
109
|
const filesWhitelist = new Set(
|
|
88
110
|
(parameters.rootFilesWhitelist ?? []).map((name) => name.toLowerCase()),
|
|
89
111
|
);
|
|
@@ -91,26 +113,19 @@ runGate(
|
|
|
91
113
|
(parameters.rootFoldersWhitelist ?? []).map((name) => name.toLowerCase()),
|
|
92
114
|
);
|
|
93
115
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
}
|
|
116
|
+
// The paths to judge: a write tool's target, OR every path a shell command creates via a
|
|
117
|
+
// redirection / touch / tee / cp / mv. The latter closes the hole where `> basura.txt`
|
|
118
|
+
// created a root file that a Write to the same name would have been denied. This does not
|
|
119
|
+
// resolve a dynamically built path (`> "$f"`) — that honest limitation is the boundary of
|
|
120
|
+
// a regex, and is why the write-tool path (which carries a concrete file_path) stays the
|
|
121
|
+
// primary, most reliable surface.
|
|
122
|
+
const targets = isShell
|
|
123
|
+
? shellWrittenPaths(String(toolInput?.command ?? toolInput?.CommandLine ?? ''))
|
|
124
|
+
: [writtenPathOf(toolInput)];
|
|
104
125
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
)
|
|
110
|
-
return;
|
|
111
|
-
deny(
|
|
112
|
-
GATE_ID,
|
|
113
|
-
`Folder '${topDirectory}' at the project root is not on the whitelist (${[...foldersWhitelist].join(', ')}).`,
|
|
114
|
-
);
|
|
126
|
+
for (const target of targets) {
|
|
127
|
+
const reason = rootWhitelistViolation(target, filesWhitelist, foldersWhitelist);
|
|
128
|
+
if (reason) deny(GATE_ID, reason);
|
|
129
|
+
}
|
|
115
130
|
},
|
|
116
131
|
);
|
|
@@ -95,3 +95,30 @@ test('FIXED: a root file matching the whitelist except for letter case is now al
|
|
|
95
95
|
// `TargetFile`, or `target_file`) — writeTargetFrom does not recognize `path` alone
|
|
96
96
|
// unless the tool is also in WRITE_TOOLS. Since MCP tools already fail the toolName
|
|
97
97
|
// check, this compounds the same root bug; not a separate one worth a distinct test.
|
|
98
|
+
|
|
99
|
+
// ── FIXED (the VPS bug): a shell REDIRECTION creating a root file evaded the gate, because
|
|
100
|
+
// root-whitelist only watched write tools, not Bash. It now also inspects the paths a shell
|
|
101
|
+
// command creates (redirection / touch / tee / cp / mv), matching protected-paths' behavior.
|
|
102
|
+
function bash(command) {
|
|
103
|
+
return { tool_name: 'Bash', tool_input: { command } };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
test('FIXED: a shell redirection creating an orphan root file is now denied', () => {
|
|
107
|
+
const p = project();
|
|
108
|
+
assert.ok(isDeny(p.run(bash('printf "x" > basura.txt'))), 'printf > basura.txt must be denied');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('FIXED: touch of an orphan root file via Bash is now denied', () => {
|
|
112
|
+
const p = project();
|
|
113
|
+
assert.ok(isDeny(p.run(bash('touch orphan.js'))), 'touch orphan.js must be denied');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('OK: a shell redirection into a whitelisted folder is allowed', () => {
|
|
117
|
+
const p = project();
|
|
118
|
+
assert.equal(p.run(bash('echo x > src/ok.js')), null, 'src/ is whitelisted');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('OK: a shell command that creates nothing (git status) is allowed', () => {
|
|
122
|
+
const p = project();
|
|
123
|
+
assert.equal(p.run(bash('git status')), null);
|
|
124
|
+
});
|
|
@@ -1,346 +1,346 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hooks": {
|
|
3
|
-
"PreToolUse": [
|
|
4
|
-
{
|
|
5
|
-
"matcher": "Bash|run_command|Agent|Task|invoke_subagent|mcp__.*",
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/bash-commands/index.mjs\"",
|
|
10
|
-
"timeout":
|
|
11
|
-
}
|
|
12
|
-
]
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__.*",
|
|
16
|
-
"hooks": [
|
|
17
|
-
{
|
|
18
|
-
"type": "command",
|
|
19
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/protected-paths/index.mjs\"",
|
|
20
|
-
"timeout":
|
|
21
|
-
}
|
|
22
|
-
]
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
26
|
-
"hooks": [
|
|
27
|
-
{
|
|
28
|
-
"type": "command",
|
|
29
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/root-whitelist/index.mjs\"",
|
|
30
|
-
"timeout":
|
|
31
|
-
}
|
|
32
|
-
]
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
"matcher": "Bash|run_command|Agent|Task|invoke_subagent|mcp__.*",
|
|
36
|
-
"hooks": [
|
|
37
|
-
{
|
|
38
|
-
"type": "command",
|
|
39
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/no-blocking/index.mjs\"",
|
|
40
|
-
"timeout":
|
|
41
|
-
}
|
|
42
|
-
]
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
46
|
-
"hooks": [
|
|
47
|
-
{
|
|
48
|
-
"type": "command",
|
|
49
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/brief-before-delegate/index.mjs\"",
|
|
50
|
-
"timeout":
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
56
|
-
"hooks": [
|
|
57
|
-
{
|
|
58
|
-
"type": "command",
|
|
59
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/intent-flow/index.mjs\"",
|
|
60
|
-
"timeout":
|
|
61
|
-
}
|
|
62
|
-
]
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
66
|
-
"hooks": [
|
|
67
|
-
{
|
|
68
|
-
"type": "command",
|
|
69
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/risk-level/index.mjs\"",
|
|
70
|
-
"timeout":
|
|
71
|
-
}
|
|
72
|
-
]
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
76
|
-
"hooks": [
|
|
77
|
-
{
|
|
78
|
-
"type": "command",
|
|
79
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/circuit-breaker/index.mjs\"",
|
|
80
|
-
"timeout":
|
|
81
|
-
}
|
|
82
|
-
]
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
86
|
-
"hooks": [
|
|
87
|
-
{
|
|
88
|
-
"type": "command",
|
|
89
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/no-memory-dependency/index.mjs\"",
|
|
90
|
-
"timeout":
|
|
91
|
-
}
|
|
92
|
-
]
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
96
|
-
"hooks": [
|
|
97
|
-
{
|
|
98
|
-
"type": "command",
|
|
99
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/feature-catalog/index.mjs\"",
|
|
100
|
-
"timeout":
|
|
101
|
-
}
|
|
102
|
-
]
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
|
|
106
|
-
"hooks": [
|
|
107
|
-
{
|
|
108
|
-
"type": "command",
|
|
109
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/sdd-specs/index.mjs\"",
|
|
110
|
-
"timeout":
|
|
111
|
-
}
|
|
112
|
-
]
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
116
|
-
"hooks": [
|
|
117
|
-
{
|
|
118
|
-
"type": "command",
|
|
119
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/implementation-pipeline/index.mjs\"",
|
|
120
|
-
"timeout":
|
|
121
|
-
}
|
|
122
|
-
]
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
126
|
-
"hooks": [
|
|
127
|
-
{
|
|
128
|
-
"type": "command",
|
|
129
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/mandatory-flow/index.mjs\"",
|
|
130
|
-
"timeout":
|
|
131
|
-
}
|
|
132
|
-
]
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
136
|
-
"hooks": [
|
|
137
|
-
{
|
|
138
|
-
"type": "command",
|
|
139
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/test-matrix/index.mjs\"",
|
|
140
|
-
"timeout":
|
|
141
|
-
}
|
|
142
|
-
]
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
146
|
-
"hooks": [
|
|
147
|
-
{
|
|
148
|
-
"type": "command",
|
|
149
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/dependency-skills/index.mjs\"",
|
|
150
|
-
"timeout":
|
|
151
|
-
}
|
|
152
|
-
]
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
|
|
156
|
-
"hooks": [
|
|
157
|
-
{
|
|
158
|
-
"type": "command",
|
|
159
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/root-cause-first/index.mjs\"",
|
|
160
|
-
"timeout":
|
|
161
|
-
}
|
|
162
|
-
]
|
|
163
|
-
},
|
|
164
|
-
{
|
|
165
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
|
|
166
|
-
"hooks": [
|
|
167
|
-
{
|
|
168
|
-
"type": "command",
|
|
169
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/audit-before-build/index.mjs\"",
|
|
170
|
-
"timeout":
|
|
171
|
-
}
|
|
172
|
-
]
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
|
|
176
|
-
"hooks": [
|
|
177
|
-
{
|
|
178
|
-
"type": "command",
|
|
179
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/never-assume/index.mjs\"",
|
|
180
|
-
"timeout":
|
|
181
|
-
}
|
|
182
|
-
]
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__ide__executeCode|Agent|Task|invoke_subagent|mcp__.*",
|
|
186
|
-
"hooks": [
|
|
187
|
-
{
|
|
188
|
-
"type": "command",
|
|
189
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/rule-skill-autodiscovery/index.mjs\"",
|
|
190
|
-
"timeout":
|
|
191
|
-
}
|
|
192
|
-
]
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__ide__executeCode|Agent|Task|invoke_subagent|mcp__.*",
|
|
196
|
-
"hooks": [
|
|
197
|
-
{
|
|
198
|
-
"type": "command",
|
|
199
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/recurrence-lock/index.mjs\"",
|
|
200
|
-
"timeout":
|
|
201
|
-
}
|
|
202
|
-
]
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
206
|
-
"hooks": [
|
|
207
|
-
{
|
|
208
|
-
"type": "command",
|
|
209
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/test-after-implementation/index.mjs\"",
|
|
210
|
-
"timeout":
|
|
211
|
-
}
|
|
212
|
-
]
|
|
213
|
-
},
|
|
214
|
-
{
|
|
215
|
-
"matcher": "AskUserQuestion|mcp__.*",
|
|
216
|
-
"hooks": [
|
|
217
|
-
{
|
|
218
|
-
"type": "command",
|
|
219
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/no-reconfirm/index.mjs\"",
|
|
220
|
-
"timeout":
|
|
221
|
-
}
|
|
222
|
-
]
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
226
|
-
"hooks": [
|
|
227
|
-
{
|
|
228
|
-
"type": "command",
|
|
229
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/neutral-spanish/index.mjs\"",
|
|
230
|
-
"timeout":
|
|
231
|
-
}
|
|
232
|
-
]
|
|
233
|
-
},
|
|
234
|
-
{
|
|
235
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
236
|
-
"hooks": [
|
|
237
|
-
{
|
|
238
|
-
"type": "command",
|
|
239
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/diagnosis-before-patch/index.mjs\"",
|
|
240
|
-
"timeout":
|
|
241
|
-
}
|
|
242
|
-
]
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
|
|
246
|
-
"hooks": [
|
|
247
|
-
{
|
|
248
|
-
"type": "command",
|
|
249
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/reuse-before-build/index.mjs\"",
|
|
250
|
-
"timeout":
|
|
251
|
-
}
|
|
252
|
-
]
|
|
253
|
-
},
|
|
254
|
-
{
|
|
255
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
256
|
-
"hooks": [
|
|
257
|
-
{
|
|
258
|
-
"type": "command",
|
|
259
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/tool-map/index.mjs\"",
|
|
260
|
-
"timeout":
|
|
261
|
-
}
|
|
262
|
-
]
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__.*",
|
|
266
|
-
"hooks": [
|
|
267
|
-
{
|
|
268
|
-
"type": "command",
|
|
269
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/forge-flow/index.mjs\"",
|
|
270
|
-
"timeout":
|
|
271
|
-
}
|
|
272
|
-
]
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
"matcher": "AskUserQuestion|mcp__.*",
|
|
276
|
-
"hooks": [
|
|
277
|
-
{
|
|
278
|
-
"type": "command",
|
|
279
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/autonomous-mode/index.mjs\"",
|
|
280
|
-
"timeout":
|
|
281
|
-
}
|
|
282
|
-
]
|
|
283
|
-
},
|
|
284
|
-
{
|
|
285
|
-
"matcher": "Bash|run_command|mcp__.*",
|
|
286
|
-
"hooks": [
|
|
287
|
-
{
|
|
288
|
-
"type": "command",
|
|
289
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/lint-commit/index.mjs\"",
|
|
290
|
-
"timeout": 65
|
|
291
|
-
}
|
|
292
|
-
]
|
|
293
|
-
},
|
|
294
|
-
{
|
|
295
|
-
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
296
|
-
"hooks": [
|
|
297
|
-
{
|
|
298
|
-
"type": "command",
|
|
299
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/force-parallel/index.mjs\"",
|
|
300
|
-
"timeout":
|
|
301
|
-
}
|
|
302
|
-
]
|
|
303
|
-
}
|
|
304
|
-
],
|
|
305
|
-
"Stop": [
|
|
306
|
-
{
|
|
307
|
-
"hooks": [
|
|
308
|
-
{
|
|
309
|
-
"type": "command",
|
|
310
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/stop-pending/index.mjs\"",
|
|
311
|
-
"timeout":
|
|
312
|
-
}
|
|
313
|
-
]
|
|
314
|
-
}
|
|
315
|
-
],
|
|
316
|
-
"SessionStart": [
|
|
317
|
-
{
|
|
318
|
-
"hooks": [
|
|
319
|
-
{
|
|
320
|
-
"type": "command",
|
|
321
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/doctor.mjs\"",
|
|
322
|
-
"timeout":
|
|
323
|
-
}
|
|
324
|
-
]
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
"hooks": [
|
|
328
|
-
{
|
|
329
|
-
"type": "command",
|
|
330
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ask-adoption.mjs\"",
|
|
331
|
-
"timeout":
|
|
332
|
-
}
|
|
333
|
-
]
|
|
334
|
-
},
|
|
335
|
-
{
|
|
336
|
-
"hooks": [
|
|
337
|
-
{
|
|
338
|
-
"type": "command",
|
|
339
|
-
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/wiring-check.mjs\"",
|
|
340
|
-
"timeout":
|
|
341
|
-
}
|
|
342
|
-
]
|
|
343
|
-
}
|
|
344
|
-
]
|
|
345
|
-
}
|
|
346
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PreToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": "Bash|run_command|Agent|Task|invoke_subagent|mcp__.*",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/bash-commands/index.mjs\"",
|
|
10
|
+
"timeout": 30
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__.*",
|
|
16
|
+
"hooks": [
|
|
17
|
+
{
|
|
18
|
+
"type": "command",
|
|
19
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/protected-paths/index.mjs\"",
|
|
20
|
+
"timeout": 30
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__.*",
|
|
26
|
+
"hooks": [
|
|
27
|
+
{
|
|
28
|
+
"type": "command",
|
|
29
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/root-whitelist/index.mjs\"",
|
|
30
|
+
"timeout": 30
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"matcher": "Bash|run_command|Agent|Task|invoke_subagent|mcp__.*",
|
|
36
|
+
"hooks": [
|
|
37
|
+
{
|
|
38
|
+
"type": "command",
|
|
39
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/no-blocking/index.mjs\"",
|
|
40
|
+
"timeout": 30
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
46
|
+
"hooks": [
|
|
47
|
+
{
|
|
48
|
+
"type": "command",
|
|
49
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/brief-before-delegate/index.mjs\"",
|
|
50
|
+
"timeout": 30
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
56
|
+
"hooks": [
|
|
57
|
+
{
|
|
58
|
+
"type": "command",
|
|
59
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/intent-flow/index.mjs\"",
|
|
60
|
+
"timeout": 30
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
66
|
+
"hooks": [
|
|
67
|
+
{
|
|
68
|
+
"type": "command",
|
|
69
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/risk-level/index.mjs\"",
|
|
70
|
+
"timeout": 30
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
76
|
+
"hooks": [
|
|
77
|
+
{
|
|
78
|
+
"type": "command",
|
|
79
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/circuit-breaker/index.mjs\"",
|
|
80
|
+
"timeout": 30
|
|
81
|
+
}
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
86
|
+
"hooks": [
|
|
87
|
+
{
|
|
88
|
+
"type": "command",
|
|
89
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/no-memory-dependency/index.mjs\"",
|
|
90
|
+
"timeout": 30
|
|
91
|
+
}
|
|
92
|
+
]
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
96
|
+
"hooks": [
|
|
97
|
+
{
|
|
98
|
+
"type": "command",
|
|
99
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/feature-catalog/index.mjs\"",
|
|
100
|
+
"timeout": 30
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
|
|
106
|
+
"hooks": [
|
|
107
|
+
{
|
|
108
|
+
"type": "command",
|
|
109
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/sdd-specs/index.mjs\"",
|
|
110
|
+
"timeout": 30
|
|
111
|
+
}
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
116
|
+
"hooks": [
|
|
117
|
+
{
|
|
118
|
+
"type": "command",
|
|
119
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/implementation-pipeline/index.mjs\"",
|
|
120
|
+
"timeout": 30
|
|
121
|
+
}
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
126
|
+
"hooks": [
|
|
127
|
+
{
|
|
128
|
+
"type": "command",
|
|
129
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/mandatory-flow/index.mjs\"",
|
|
130
|
+
"timeout": 30
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
136
|
+
"hooks": [
|
|
137
|
+
{
|
|
138
|
+
"type": "command",
|
|
139
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/test-matrix/index.mjs\"",
|
|
140
|
+
"timeout": 30
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
146
|
+
"hooks": [
|
|
147
|
+
{
|
|
148
|
+
"type": "command",
|
|
149
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/dependency-skills/index.mjs\"",
|
|
150
|
+
"timeout": 30
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
|
|
156
|
+
"hooks": [
|
|
157
|
+
{
|
|
158
|
+
"type": "command",
|
|
159
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/root-cause-first/index.mjs\"",
|
|
160
|
+
"timeout": 30
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
|
|
166
|
+
"hooks": [
|
|
167
|
+
{
|
|
168
|
+
"type": "command",
|
|
169
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/audit-before-build/index.mjs\"",
|
|
170
|
+
"timeout": 30
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
|
|
176
|
+
"hooks": [
|
|
177
|
+
{
|
|
178
|
+
"type": "command",
|
|
179
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/never-assume/index.mjs\"",
|
|
180
|
+
"timeout": 30
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__ide__executeCode|Agent|Task|invoke_subagent|mcp__.*",
|
|
186
|
+
"hooks": [
|
|
187
|
+
{
|
|
188
|
+
"type": "command",
|
|
189
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/rule-skill-autodiscovery/index.mjs\"",
|
|
190
|
+
"timeout": 30
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__ide__executeCode|Agent|Task|invoke_subagent|mcp__.*",
|
|
196
|
+
"hooks": [
|
|
197
|
+
{
|
|
198
|
+
"type": "command",
|
|
199
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/recurrence-lock/index.mjs\"",
|
|
200
|
+
"timeout": 30
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
206
|
+
"hooks": [
|
|
207
|
+
{
|
|
208
|
+
"type": "command",
|
|
209
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/test-after-implementation/index.mjs\"",
|
|
210
|
+
"timeout": 30
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"matcher": "AskUserQuestion|mcp__.*",
|
|
216
|
+
"hooks": [
|
|
217
|
+
{
|
|
218
|
+
"type": "command",
|
|
219
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/no-reconfirm/index.mjs\"",
|
|
220
|
+
"timeout": 30
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
226
|
+
"hooks": [
|
|
227
|
+
{
|
|
228
|
+
"type": "command",
|
|
229
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/neutral-spanish/index.mjs\"",
|
|
230
|
+
"timeout": 30
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
236
|
+
"hooks": [
|
|
237
|
+
{
|
|
238
|
+
"type": "command",
|
|
239
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/diagnosis-before-patch/index.mjs\"",
|
|
240
|
+
"timeout": 30
|
|
241
|
+
}
|
|
242
|
+
]
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Agent|Task|invoke_subagent|mcp__.*",
|
|
246
|
+
"hooks": [
|
|
247
|
+
{
|
|
248
|
+
"type": "command",
|
|
249
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/reuse-before-build/index.mjs\"",
|
|
250
|
+
"timeout": 30
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|mcp__.*",
|
|
256
|
+
"hooks": [
|
|
257
|
+
{
|
|
258
|
+
"type": "command",
|
|
259
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/tool-map/index.mjs\"",
|
|
260
|
+
"timeout": 30
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"matcher": "Write|Edit|NotebookEdit|write_to_file|replace_file_content|Bash|run_command|mcp__.*",
|
|
266
|
+
"hooks": [
|
|
267
|
+
{
|
|
268
|
+
"type": "command",
|
|
269
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/forge-flow/index.mjs\"",
|
|
270
|
+
"timeout": 30
|
|
271
|
+
}
|
|
272
|
+
]
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"matcher": "AskUserQuestion|mcp__.*",
|
|
276
|
+
"hooks": [
|
|
277
|
+
{
|
|
278
|
+
"type": "command",
|
|
279
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/autonomous-mode/index.mjs\"",
|
|
280
|
+
"timeout": 30
|
|
281
|
+
}
|
|
282
|
+
]
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
"matcher": "Bash|run_command|mcp__.*",
|
|
286
|
+
"hooks": [
|
|
287
|
+
{
|
|
288
|
+
"type": "command",
|
|
289
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/lint-commit/index.mjs\"",
|
|
290
|
+
"timeout": 65
|
|
291
|
+
}
|
|
292
|
+
]
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
"matcher": "Agent|Task|invoke_subagent|mcp__.*",
|
|
296
|
+
"hooks": [
|
|
297
|
+
{
|
|
298
|
+
"type": "command",
|
|
299
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/force-parallel/index.mjs\"",
|
|
300
|
+
"timeout": 30
|
|
301
|
+
}
|
|
302
|
+
]
|
|
303
|
+
}
|
|
304
|
+
],
|
|
305
|
+
"Stop": [
|
|
306
|
+
{
|
|
307
|
+
"hooks": [
|
|
308
|
+
{
|
|
309
|
+
"type": "command",
|
|
310
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gates/stop-pending/index.mjs\"",
|
|
311
|
+
"timeout": 30
|
|
312
|
+
}
|
|
313
|
+
]
|
|
314
|
+
}
|
|
315
|
+
],
|
|
316
|
+
"SessionStart": [
|
|
317
|
+
{
|
|
318
|
+
"hooks": [
|
|
319
|
+
{
|
|
320
|
+
"type": "command",
|
|
321
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/doctor.mjs\"",
|
|
322
|
+
"timeout": 30
|
|
323
|
+
}
|
|
324
|
+
]
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"hooks": [
|
|
328
|
+
{
|
|
329
|
+
"type": "command",
|
|
330
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/ask-adoption.mjs\"",
|
|
331
|
+
"timeout": 30
|
|
332
|
+
}
|
|
333
|
+
]
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"hooks": [
|
|
337
|
+
{
|
|
338
|
+
"type": "command",
|
|
339
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/wiring-check.mjs\"",
|
|
340
|
+
"timeout": 30
|
|
341
|
+
}
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
}
|
|
346
|
+
}
|
|
@@ -11,7 +11,8 @@ import {
|
|
|
11
11
|
toolInGroups,
|
|
12
12
|
writtenContentOf,
|
|
13
13
|
writtenPathOf,
|
|
14
|
-
delegationPromptOf,
|
|
14
|
+
delegationPromptOf,
|
|
15
|
+
shellWrittenPaths,
|
|
15
16
|
} from '../hook-io.mjs';
|
|
16
17
|
|
|
17
18
|
// deny/warn/allow/runGate call process.exit and cannot be invoked in-process without
|
|
@@ -208,3 +209,15 @@ test('allow exits 0 with no stdout', () => {
|
|
|
208
209
|
const output = runInChildProcess('mod.allow()', { input: '' });
|
|
209
210
|
assert.equal(output, '');
|
|
210
211
|
});
|
|
212
|
+
|
|
213
|
+
test('shellWrittenPaths extracts paths a shell command creates', () => {
|
|
214
|
+
assert.deepEqual(shellWrittenPaths('printf x > basura.txt'), ['basura.txt']);
|
|
215
|
+
assert.deepEqual(shellWrittenPaths('echo hi >> log.txt'), ['log.txt']);
|
|
216
|
+
assert.deepEqual(shellWrittenPaths('touch nuevo.js'), ['nuevo.js']);
|
|
217
|
+
assert.deepEqual(shellWrittenPaths('tee out.txt'), ['out.txt']);
|
|
218
|
+
// creates nothing -> empty
|
|
219
|
+
assert.deepEqual(shellWrittenPaths('git status'), []);
|
|
220
|
+
assert.deepEqual(shellWrittenPaths('cat archivo.txt'), []);
|
|
221
|
+
// a dynamically built path is NOT extracted (documented limitation)
|
|
222
|
+
assert.deepEqual(shellWrittenPaths('printf x > "$f"'), []);
|
|
223
|
+
});
|
|
@@ -155,6 +155,45 @@ export function writtenPathOf(toolInput) {
|
|
|
155
155
|
return String(path);
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// Shell forms that CREATE or write a file at a named path, so a gate protecting a path can
|
|
159
|
+
// see a `printf ... > basura.txt` the same way it sees a Write. Each pattern captures the
|
|
160
|
+
// target path. Conservative by design: it over-detects (a gate then finds the path benign)
|
|
161
|
+
// rather than under-detects (a silent hole). It does NOT resolve variables, command
|
|
162
|
+
// substitution, or subshells — a path built dynamically (`> "$f"`) is not extracted; that
|
|
163
|
+
// limitation is documented on the gates that use this, the honest boundary of a regex.
|
|
164
|
+
const SHELL_WRITE_PATTERNS = [
|
|
165
|
+
// redirection: `> file`, `>> file`, `1> file`, `&> file` (not `2>` alone — stderr)
|
|
166
|
+
/(?:^|\s|;|&&|\|\|)(?:[0-9]*|&)>>?\s*(['"]?)([^\s'"|;&<>]+)\1/g,
|
|
167
|
+
// touch / tee target(s)
|
|
168
|
+
/\b(?:touch|tee)\s+(?:-\S+\s+)*(['"]?)([^\s'"|;&<>]+)\1/g,
|
|
169
|
+
// cp / mv / install destination is the LAST path; capture the first arg after the command
|
|
170
|
+
// as a cheap proxy (over-detects the source too, which is acceptable — a gate re-checks).
|
|
171
|
+
/\b(?:cp|mv|install)\s+(?:-\S+\s+)*(['"]?)([^\s'"|;&<>]+)\1/g,
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Every filesystem path a shell command appears to create or write to (redirections, touch,
|
|
176
|
+
* tee, cp/mv destinations). Returns a de-duplicated list, empty when none is found. A gate
|
|
177
|
+
* that protects paths should check these IN ADDITION to writtenPathOf, or a shell redirection
|
|
178
|
+
* slips past it (the exact hole that let `printf x > basura.txt` evade root-whitelist while a
|
|
179
|
+
* Write to the same path was blocked).
|
|
180
|
+
*/
|
|
181
|
+
export function shellWrittenPaths(command) {
|
|
182
|
+
const text = String(command ?? '');
|
|
183
|
+
const found = new Set();
|
|
184
|
+
for (const pattern of SHELL_WRITE_PATTERNS) {
|
|
185
|
+
for (const match of text.matchAll(pattern)) {
|
|
186
|
+
const path = match[2];
|
|
187
|
+
// Skip a dynamically built target (a variable/substitution): we cannot resolve `$f`,
|
|
188
|
+
// `${x}` or `$(…)` to a real path, and guessing would only add false positives. This is
|
|
189
|
+
// the documented limitation — the write-tool surface, which carries a concrete path,
|
|
190
|
+
// stays the reliable one.
|
|
191
|
+
if (path && !/[$`]/.test(path)) found.add(path);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return [...found];
|
|
195
|
+
}
|
|
196
|
+
|
|
158
197
|
/** The brief/prompt a delegation carries, across native and MCP field shapes. */
|
|
159
198
|
export function delegationPromptOf(toolInput) {
|
|
160
199
|
if (!toolInput || typeof toolInput !== 'object') return '';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tasks",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Deterministic task tracking for Claude Code: persists tasks the model registers via the CLI, reminds of open tasks on a message counter, and lists active tasks on session start.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Devrik"
|