@devrik-tools/claude-gates 0.3.1 → 0.6.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.
Files changed (58) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/README.es.md +25 -4
  3. package/README.md +75 -43
  4. package/cli/config.mjs +126 -124
  5. package/cli/index.mjs +154 -104
  6. package/cli/init.mjs +303 -276
  7. package/cli/install.mjs +281 -175
  8. package/cli/materialize.mjs +103 -102
  9. package/cli/registry.mjs +139 -136
  10. package/cli/smoke-fixtures.json +441 -0
  11. package/cli/smoke.mjs +129 -0
  12. package/cli/task.mjs +140 -140
  13. package/package.json +1 -1
  14. package/plugins/gates/.claude-plugin/plugin.json +1 -1
  15. package/plugins/gates/hooks/ask-adoption.mjs +147 -147
  16. package/plugins/gates/hooks/doctor.mjs +207 -207
  17. package/plugins/gates/hooks/gates/atomic-commit/index.mjs +229 -0
  18. package/plugins/gates/hooks/gates/audit-before-build/index.mjs +110 -88
  19. package/plugins/gates/hooks/gates/autonomous-mode/index.mjs +50 -50
  20. package/plugins/gates/hooks/gates/bash-commands/index.mjs +215 -215
  21. package/plugins/gates/hooks/gates/brief-before-delegate/index.mjs +269 -265
  22. package/plugins/gates/hooks/gates/capability-map/index.mjs +377 -0
  23. package/plugins/gates/hooks/gates/circuit-breaker/index.mjs +527 -501
  24. package/plugins/gates/hooks/gates/diagnosis-before-patch/index.mjs +48 -43
  25. package/plugins/gates/hooks/gates/feature-catalog/index.mjs +83 -83
  26. package/plugins/gates/hooks/gates/force-parallel/index.mjs +134 -119
  27. package/plugins/gates/hooks/gates/forge-flow/index.mjs +134 -134
  28. package/plugins/gates/hooks/gates/implementation-pipeline/index.mjs +187 -187
  29. package/plugins/gates/hooks/gates/intent-flow/index.mjs +260 -260
  30. package/plugins/gates/hooks/gates/lint-commit/index.mjs +152 -149
  31. package/plugins/gates/hooks/gates/mandatory-flow/index.mjs +180 -180
  32. package/plugins/gates/hooks/gates/never-assume/index.mjs +59 -58
  33. package/plugins/gates/hooks/gates/no-blocking/index.mjs +163 -148
  34. package/plugins/gates/hooks/gates/no-coauthor/index.mjs +127 -0
  35. package/plugins/gates/hooks/gates/no-lint-suppression/index.mjs +183 -0
  36. package/plugins/gates/hooks/gates/protected-paths/index.mjs +149 -144
  37. package/plugins/gates/hooks/gates/recurrence-lock/index.mjs +91 -89
  38. package/plugins/gates/hooks/gates/reuse-before-build/index.mjs +263 -159
  39. package/plugins/gates/hooks/gates/risk-level/index.mjs +265 -263
  40. package/plugins/gates/hooks/gates/root-cause-first/index.mjs +57 -56
  41. package/plugins/gates/hooks/gates/root-whitelist/index.mjs +211 -131
  42. package/plugins/gates/hooks/gates/rule-skill-autodiscovery/index.mjs +181 -184
  43. package/plugins/gates/hooks/gates/sdd-specs/index.mjs +256 -256
  44. package/plugins/gates/hooks/gates/staged-lint/index.mjs +187 -0
  45. package/plugins/gates/hooks/gates/stop-pending/index.mjs +169 -164
  46. package/plugins/gates/hooks/gates/test-matrix/index.mjs +187 -187
  47. package/plugins/gates/hooks/gates/tool-map/index.mjs +168 -143
  48. package/plugins/gates/hooks/hooks.json +51 -0
  49. package/plugins/gates/hooks/lib/config.mjs +179 -172
  50. package/plugins/gates/hooks/lib/hook-io.mjs +367 -357
  51. package/plugins/gates/hooks/lib/signals.mjs +172 -127
  52. package/plugins/gates/hooks/wiring-check.mjs +227 -227
  53. package/plugins/tasks/.claude-plugin/plugin.json +1 -1
  54. package/plugins/tasks/hooks/hooks.json +26 -26
  55. package/plugins/tasks/hooks/lib/task-store.mjs +217 -197
  56. package/plugins/tasks/hooks/register-requests.mjs +145 -145
  57. package/plugins/tasks/hooks/session-tasks.mjs +108 -108
  58. package/registry.json +171 -1
@@ -1,227 +1,227 @@
1
- // wiring-check — SessionStart hook. Cross-checks registry.json, each plugin's hooks.json and
2
- // the scripts on disk, and warns (never blocks — SessionStart cannot deny) when they
3
- // disagree. This is the check that would have caught the session family itself being
4
- // declared in registry.json with no script and no hooks.json wiring: three checks,
5
- // each catching a different half of a broken install —
6
- //
7
- // 1. registry gate -> not wired: a gate.json entry exists but no hooks.json (of the
8
- // family's plugin) references its script for the declared event.
9
- // 2. hooks.json command -> script missing: a hooks.json entry points at a script path
10
- // that does not exist on disk.
11
- // 3. orphaned gate folder: a `gates/<id>/index.mjs` exists under a plugin's hooks/ dir
12
- // but registry.json has no gate pointing at it.
13
- //
14
- // Consistent (nothing to report) -> silent. Fail-safe: wrapped in try/catch, a bug here
15
- // must never block session start.
16
- //
17
- // justification: no existing tool covers this. `cli/__tests__/registry-gates-consistency
18
- // .test.mjs` checks registry-vs-disk at CI/dev time; this is the SAME class of check
19
- // surfaced live, at session start, in an already-installed plugin where the source
20
- // checkout (and that test) is not present.
21
-
22
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
23
- import { homedir } from 'node:os';
24
- import { dirname, join } from 'node:path';
25
- import { fileURLToPath } from 'node:url';
26
-
27
- const STDIN_FILE_DESCRIPTOR = 0;
28
- const SESSION_START_EVENT = 'SessionStart';
29
- const CONFIG_KEY = 'checkWiringOnStart';
30
-
31
- const HOOKS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
32
- const REPOSITORY_ROOT = join(HOOKS_DIRECTORY, '..', '..', '..');
33
- const REGISTRY_PATH = join(REPOSITORY_ROOT, 'registry.json');
34
- const DEFAULT_PLUGIN = 'gates';
35
-
36
- const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
37
- const PROJECT_CONFIG = join('.ai', 'config.json');
38
- const GLOBAL_CONFIG = join('.claude', 'claude-gates', 'config.json');
39
-
40
- const BOM_CHAR_CODE = 0xfeff;
41
-
42
- /** Strips a UTF-8 BOM a Windows editor/tool may have written before the JSON. */
43
- function stripBom(text) {
44
- return text.charCodeAt(0) === BOM_CHAR_CODE ? text.slice(1) : text;
45
- }
46
-
47
- function readJson(path) {
48
- try {
49
- return JSON.parse(stripBom(readFileSync(path, 'utf8')));
50
- } catch {
51
- return null;
52
- }
53
- }
54
-
55
- function projectRootOf(startDirectory) {
56
- let current = startDirectory;
57
- while (true) {
58
- if (
59
- PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
60
- ) {
61
- return current;
62
- }
63
- const parent = dirname(current);
64
- if (parent === current) return null;
65
- current = parent;
66
- }
67
- }
68
-
69
- /** The wiring-check gate config entry (project overrides global), or null. */
70
- function wiringConfig(startDirectory) {
71
- const root = projectRootOf(startDirectory);
72
- const projectData = root ? readJson(join(root, PROJECT_CONFIG)) : null;
73
- const globalData = readJson(join(homedir(), GLOBAL_CONFIG));
74
- const layer = projectData?.gates ?? globalData?.gates ?? {};
75
- const entry = layer[CONFIG_KEY];
76
- if (typeof entry === 'boolean') return { enabled: entry };
77
- if (entry && typeof entry === 'object') return entry;
78
- return null;
79
- }
80
-
81
- function readPayload() {
82
- try {
83
- return JSON.parse(readFileSync(STDIN_FILE_DESCRIPTOR, 'utf8'));
84
- } catch {
85
- return {};
86
- }
87
- }
88
-
89
- function pluginHooksDirectory(pluginName) {
90
- return join(REPOSITORY_ROOT, 'plugins', pluginName, 'hooks');
91
- }
92
-
93
- function pluginHooksJsonPath(pluginName) {
94
- return join(pluginHooksDirectory(pluginName), 'hooks.json');
95
- }
96
-
97
- /** Every hooks.json command string across all its events, flattened. */
98
- function commandsIn(hooksJson) {
99
- if (!hooksJson?.hooks) return [];
100
- const commands = [];
101
- for (const entries of Object.values(hooksJson.hooks)) {
102
- for (const entry of entries ?? []) {
103
- for (const hook of entry?.hooks ?? []) {
104
- if (hook?.command) commands.push(hook.command);
105
- }
106
- }
107
- }
108
- return commands;
109
- }
110
-
111
- /** Whether some hooks.json command references this script's basename for this plugin. */
112
- function scriptIsWired(commands, script) {
113
- // Commands look like `node "${CLAUDE_PLUGIN_ROOT}/hooks/gates/x/index.mjs"` — match on
114
- // the script's path-with-forward-slashes so it is independent of the wrapping command
115
- // shape (quoting, the node invocation, timeout, etc.).
116
- const needle = script.replace(/\\/g, '/');
117
- return commands.some((command) =>
118
- command.replace(/\\/g, '/').includes(needle),
119
- );
120
- }
121
-
122
- function checkRegistryVsHooksJson(registry) {
123
- const problems = [];
124
- const hooksJsonCache = new Map();
125
-
126
- for (const family of registry.families ?? []) {
127
- const pluginName = family.plugin ?? DEFAULT_PLUGIN;
128
- if (!hooksJsonCache.has(pluginName)) {
129
- hooksJsonCache.set(pluginName, readJson(pluginHooksJsonPath(pluginName)));
130
- }
131
- const hooksJson = hooksJsonCache.get(pluginName);
132
- const commands = commandsIn(hooksJson);
133
-
134
- for (const gate of family.gates ?? []) {
135
- const scriptPath = join(pluginHooksDirectory(pluginName), gate.script);
136
- if (!existsSync(scriptPath)) {
137
- problems.push(
138
- `gate "${gate.id}": script missing on disk (expected plugins/${pluginName}/hooks/${gate.script})`,
139
- );
140
- continue; // no point checking wiring for a script that isn't even there
141
- }
142
- if (!scriptIsWired(commands, gate.script)) {
143
- problems.push(
144
- `gate "${gate.id}": not wired in plugins/${pluginName}/hooks/hooks.json (${gate.event})`,
145
- );
146
- }
147
- }
148
- }
149
- return problems;
150
- }
151
-
152
- /** Gate folders (gates/<id>/index.mjs) on disk with no registry entry pointing at them. */
153
- function orphanedGateFolders(registry) {
154
- const declaredScripts = new Set(
155
- (registry.families ?? []).flatMap((family) =>
156
- (family.gates ?? [])
157
- .filter((gate) => gate.script.startsWith('gates/'))
158
- .map((gate) => `${family.plugin ?? DEFAULT_PLUGIN}::${gate.script}`),
159
- ),
160
- );
161
-
162
- const problems = [];
163
- const pluginNames = new Set(
164
- (registry.families ?? []).map((family) => family.plugin ?? DEFAULT_PLUGIN),
165
- );
166
- for (const pluginName of pluginNames) {
167
- const gatesDirectory = join(pluginHooksDirectory(pluginName), 'gates');
168
- if (!existsSync(gatesDirectory)) continue;
169
- let entries;
170
- try {
171
- entries = readdirSync(gatesDirectory, { withFileTypes: true });
172
- } catch {
173
- continue;
174
- }
175
- for (const entry of entries) {
176
- const script = `gates/${entry.name}/index.mjs`;
177
- const isRealGateFolder =
178
- entry.isDirectory() &&
179
- existsSync(join(gatesDirectory, entry.name, 'index.mjs'));
180
- if (!isRealGateFolder) continue;
181
- if (!declaredScripts.has(`${pluginName}::${script}`)) {
182
- problems.push(
183
- `plugins/${pluginName}/hooks/gates/${entry.name}/index.mjs has no registry entry`,
184
- );
185
- }
186
- }
187
- }
188
- return problems;
189
- }
190
-
191
- function speak(context) {
192
- process.stdout.write(
193
- JSON.stringify({
194
- hookSpecificOutput: {
195
- hookEventName: SESSION_START_EVENT,
196
- additionalContext: context,
197
- },
198
- }),
199
- );
200
- }
201
-
202
- function main() {
203
- const payload = readPayload();
204
- const cwd = payload.cwd || process.cwd();
205
-
206
- const config = wiringConfig(cwd);
207
- if (config && config.enabled === false) return; // gate turned off for this project
208
-
209
- const registry = readJson(REGISTRY_PATH);
210
- if (!registry?.families) return; // can't read the registry: nothing to report
211
-
212
- const problems = [
213
- ...checkRegistryVsHooksJson(registry),
214
- ...orphanedGateFolders(registry),
215
- ];
216
- if (problems.length === 0) return; // everything consistent: stay silent
217
-
218
- speak(
219
- `[wiring-check] claude-gates wiring inconsistencies found:\n- ${problems.join('\n- ')}`,
220
- );
221
- }
222
-
223
- try {
224
- main();
225
- } catch {
226
- // A wiring-check bug must never block session start.
227
- }
1
+ // wiring-check — SessionStart hook. Cross-checks registry.json, each plugin's hooks.json and
2
+ // the scripts on disk, and warns (never blocks — SessionStart cannot deny) when they
3
+ // disagree. This is the check that would have caught the session family itself being
4
+ // declared in registry.json with no script and no hooks.json wiring: three checks,
5
+ // each catching a different half of a broken install —
6
+ //
7
+ // 1. registry gate -> not wired: a gate.json entry exists but no hooks.json (of the
8
+ // family's plugin) references its script for the declared event.
9
+ // 2. hooks.json command -> script missing: a hooks.json entry points at a script path
10
+ // that does not exist on disk.
11
+ // 3. orphaned gate folder: a `gates/<id>/index.mjs` exists under a plugin's hooks/ dir
12
+ // but registry.json has no gate pointing at it.
13
+ //
14
+ // Consistent (nothing to report) -> silent. Fail-safe: wrapped in try/catch, a bug here
15
+ // must never block session start.
16
+ //
17
+ // justification: no existing tool covers this. `cli/__tests__/registry-gates-consistency
18
+ // .test.mjs` checks registry-vs-disk at CI/dev time; this is the SAME class of check
19
+ // surfaced live, at session start, in an already-installed plugin where the source
20
+ // checkout (and that test) is not present.
21
+
22
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
23
+ import { homedir } from 'node:os';
24
+ import { dirname, join } from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const STDIN_FILE_DESCRIPTOR = 0;
28
+ const SESSION_START_EVENT = 'SessionStart';
29
+ const CONFIG_KEY = 'checkWiringOnStart';
30
+
31
+ const HOOKS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
32
+ const REPOSITORY_ROOT = join(HOOKS_DIRECTORY, '..', '..', '..');
33
+ const REGISTRY_PATH = join(REPOSITORY_ROOT, 'registry.json');
34
+ const DEFAULT_PLUGIN = 'gates';
35
+
36
+ const PROJECT_ROOT_MARKERS = ['.git', '.ai'];
37
+ const PROJECT_CONFIG = join('.ai', 'config.json');
38
+ const GLOBAL_CONFIG = join('.claude', 'claude-gates', 'config.json');
39
+
40
+ const BOM_CHAR_CODE = 0xfeff;
41
+
42
+ /** Strips a UTF-8 BOM a Windows editor/tool may have written before the JSON. */
43
+ function stripBom(text) {
44
+ return text.charCodeAt(0) === BOM_CHAR_CODE ? text.slice(1) : text;
45
+ }
46
+
47
+ function readJson(path) {
48
+ try {
49
+ return JSON.parse(stripBom(readFileSync(path, 'utf8')));
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function projectRootOf(startDirectory) {
56
+ let current = startDirectory;
57
+ while (true) {
58
+ if (
59
+ PROJECT_ROOT_MARKERS.some((marker) => existsSync(join(current, marker)))
60
+ ) {
61
+ return current;
62
+ }
63
+ const parent = dirname(current);
64
+ if (parent === current) return null;
65
+ current = parent;
66
+ }
67
+ }
68
+
69
+ /** The wiring-check gate config entry (project overrides global), or null. */
70
+ function wiringConfig(startDirectory) {
71
+ const root = projectRootOf(startDirectory);
72
+ const projectData = root ? readJson(join(root, PROJECT_CONFIG)) : null;
73
+ const globalData = readJson(join(homedir(), GLOBAL_CONFIG));
74
+ const layer = projectData?.gates ?? globalData?.gates ?? {};
75
+ const entry = layer[CONFIG_KEY];
76
+ if (typeof entry === 'boolean') return { enabled: entry };
77
+ if (entry && typeof entry === 'object') return entry;
78
+ return null;
79
+ }
80
+
81
+ function readPayload() {
82
+ try {
83
+ return JSON.parse(readFileSync(STDIN_FILE_DESCRIPTOR, 'utf8'));
84
+ } catch {
85
+ return {};
86
+ }
87
+ }
88
+
89
+ function pluginHooksDirectory(pluginName) {
90
+ return join(REPOSITORY_ROOT, 'plugins', pluginName, 'hooks');
91
+ }
92
+
93
+ function pluginHooksJsonPath(pluginName) {
94
+ return join(pluginHooksDirectory(pluginName), 'hooks.json');
95
+ }
96
+
97
+ /** Every hooks.json command string across all its events, flattened. */
98
+ function commandsIn(hooksJson) {
99
+ if (!hooksJson?.hooks) return [];
100
+ const commands = [];
101
+ for (const entries of Object.values(hooksJson.hooks)) {
102
+ for (const entry of entries ?? []) {
103
+ for (const hook of entry?.hooks ?? []) {
104
+ if (hook?.command) commands.push(hook.command);
105
+ }
106
+ }
107
+ }
108
+ return commands;
109
+ }
110
+
111
+ /** Whether some hooks.json command references this script's basename for this plugin. */
112
+ function scriptIsWired(commands, script) {
113
+ // Commands look like `node "${CLAUDE_PLUGIN_ROOT}/hooks/gates/x/index.mjs"` — match on
114
+ // the script's path-with-forward-slashes so it is independent of the wrapping command
115
+ // shape (quoting, the node invocation, timeout, etc.).
116
+ const needle = script.replace(/\\/g, '/');
117
+ return commands.some((command) =>
118
+ command.replace(/\\/g, '/').includes(needle),
119
+ );
120
+ }
121
+
122
+ function checkRegistryVsHooksJson(registry) {
123
+ const problems = [];
124
+ const hooksJsonCache = new Map();
125
+
126
+ for (const family of registry.families ?? []) {
127
+ const pluginName = family.plugin ?? DEFAULT_PLUGIN;
128
+ if (!hooksJsonCache.has(pluginName)) {
129
+ hooksJsonCache.set(pluginName, readJson(pluginHooksJsonPath(pluginName)));
130
+ }
131
+ const hooksJson = hooksJsonCache.get(pluginName);
132
+ const commands = commandsIn(hooksJson);
133
+
134
+ for (const gate of family.gates ?? []) {
135
+ const scriptPath = join(pluginHooksDirectory(pluginName), gate.script);
136
+ if (!existsSync(scriptPath)) {
137
+ problems.push(
138
+ `gate "${gate.id}": script missing on disk (expected plugins/${pluginName}/hooks/${gate.script})`,
139
+ );
140
+ continue; // no point checking wiring for a script that isn't even there
141
+ }
142
+ if (!scriptIsWired(commands, gate.script)) {
143
+ problems.push(
144
+ `gate "${gate.id}": not wired in plugins/${pluginName}/hooks/hooks.json (${gate.event})`,
145
+ );
146
+ }
147
+ }
148
+ }
149
+ return problems;
150
+ }
151
+
152
+ /** Gate folders (gates/<id>/index.mjs) on disk with no registry entry pointing at them. */
153
+ function orphanedGateFolders(registry) {
154
+ const declaredScripts = new Set(
155
+ (registry.families ?? []).flatMap((family) =>
156
+ (family.gates ?? [])
157
+ .filter((gate) => gate.script.startsWith('gates/'))
158
+ .map((gate) => `${family.plugin ?? DEFAULT_PLUGIN}::${gate.script}`),
159
+ ),
160
+ );
161
+
162
+ const problems = [];
163
+ const pluginNames = new Set(
164
+ (registry.families ?? []).map((family) => family.plugin ?? DEFAULT_PLUGIN),
165
+ );
166
+ for (const pluginName of pluginNames) {
167
+ const gatesDirectory = join(pluginHooksDirectory(pluginName), 'gates');
168
+ if (!existsSync(gatesDirectory)) continue;
169
+ let entries;
170
+ try {
171
+ entries = readdirSync(gatesDirectory, { withFileTypes: true });
172
+ } catch {
173
+ continue;
174
+ }
175
+ for (const entry of entries) {
176
+ const script = `gates/${entry.name}/index.mjs`;
177
+ const isRealGateFolder =
178
+ entry.isDirectory() &&
179
+ existsSync(join(gatesDirectory, entry.name, 'index.mjs'));
180
+ if (!isRealGateFolder) continue;
181
+ if (!declaredScripts.has(`${pluginName}::${script}`)) {
182
+ problems.push(
183
+ `plugins/${pluginName}/hooks/gates/${entry.name}/index.mjs has no registry entry`,
184
+ );
185
+ }
186
+ }
187
+ }
188
+ return problems;
189
+ }
190
+
191
+ function speak(context) {
192
+ process.stdout.write(
193
+ JSON.stringify({
194
+ hookSpecificOutput: {
195
+ hookEventName: SESSION_START_EVENT,
196
+ additionalContext: context,
197
+ },
198
+ }),
199
+ );
200
+ }
201
+
202
+ function main() {
203
+ const payload = readPayload();
204
+ const cwd = payload.cwd || process.cwd();
205
+
206
+ const config = wiringConfig(cwd);
207
+ if (config && config.enabled === false) return; // gate turned off for this project
208
+
209
+ const registry = readJson(REGISTRY_PATH);
210
+ if (!registry?.families) return; // can't read the registry: nothing to report
211
+
212
+ const problems = [
213
+ ...checkRegistryVsHooksJson(registry),
214
+ ...orphanedGateFolders(registry),
215
+ ];
216
+ if (problems.length === 0) return; // everything consistent: stay silent
217
+
218
+ speak(
219
+ `[wiring-check] claude-gates wiring inconsistencies found:\n- ${problems.join('\n- ')}`,
220
+ );
221
+ }
222
+
223
+ try {
224
+ main();
225
+ } catch {
226
+ // A wiring-check bug must never block session start.
227
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tasks",
3
- "version": "0.3.1",
3
+ "version": "0.6.0",
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"
@@ -1,26 +1,26 @@
1
- {
2
- "hooks": {
3
- "UserPromptSubmit": [
4
- {
5
- "hooks": [
6
- {
7
- "type": "command",
8
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/register-requests.mjs\"",
9
- "timeout": 10
10
- }
11
- ]
12
- }
13
- ],
14
- "SessionStart": [
15
- {
16
- "hooks": [
17
- {
18
- "type": "command",
19
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-tasks.mjs\"",
20
- "timeout": 10
21
- }
22
- ]
23
- }
24
- ]
25
- }
26
- }
1
+ {
2
+ "hooks": {
3
+ "UserPromptSubmit": [
4
+ {
5
+ "hooks": [
6
+ {
7
+ "type": "command",
8
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/register-requests.mjs\"",
9
+ "timeout": 10
10
+ }
11
+ ]
12
+ }
13
+ ],
14
+ "SessionStart": [
15
+ {
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/session-tasks.mjs\"",
20
+ "timeout": 10
21
+ }
22
+ ]
23
+ }
24
+ ]
25
+ }
26
+ }