skillrepo 1.6.1 → 1.6.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": "skillrepo",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "Set up SkillRepo in any IDE — one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,7 +37,7 @@ export function writeFileSafe(filePath, content) {
37
37
 
38
38
  /**
39
39
  * Write a file and mark it executable (0o755).
40
- * Used for hook scripts that have shebangs and are invoked directly.
40
+ * Used for the Cursor session hook which is invoked directly via shebang.
41
41
  */
42
42
  export function writeExecutable(filePath, content) {
43
43
  writeFileSafe(filePath, content);
@@ -8,70 +8,59 @@
8
8
  */
9
9
 
10
10
  import { unlinkSync } from "node:fs";
11
- import { readFileSafe, writeFileSafe, writeExecutable } from "../fs-utils.mjs";
11
+ import { readFileSafe, writeFileSafe } from "../fs-utils.mjs";
12
12
  import { claudeSettingsLocal, claudeSyncHook, claudePromptHook, claudePreToolHook, claudeHooksDir } from "../paths.mjs";
13
13
  import { join } from "node:path";
14
14
 
15
- const DEFAULT_SYNC_COMMAND = ".claude/hooks/skillrepo-sync.mjs";
16
- const DEFAULT_PROMPT_COMMAND = ".claude/hooks/skillrepo-prompt-match.mjs";
17
- const DEFAULT_PRETOOL_COMMAND = ".claude/hooks/skillrepo-pretool-match.mjs";
18
-
19
- // Legacy commands used before v1.6.1 — recognized during migration so
20
- // re-running init replaces them rather than duplicating.
21
- const LEGACY_COMMANDS = new Set([
22
- "node .claude/hooks/skillrepo-sync.mjs",
23
- "node .claude/hooks/skillrepo-prompt-match.mjs",
24
- "node .claude/hooks/skillrepo-pretool-match.mjs",
25
- ]);
15
+ // The three hook script paths (relative to project root).
16
+ const HOOK_SCRIPTS = {
17
+ sync: ".claude/hooks/skillrepo-sync.mjs",
18
+ prompt: ".claude/hooks/skillrepo-prompt-match.mjs",
19
+ pretool: ".claude/hooks/skillrepo-pretool-match.mjs",
20
+ };
26
21
 
27
22
  /**
28
- * Extract a hook command from a hooks config group, falling back to a default.
29
- * @param {object} hooksConfig - The hooks config object from the server payload
30
- * @param {string} eventName - e.g. "SessionStart" or "UserPromptSubmit"
31
- * @param {string} fallback - default command if extraction fails
32
- * @returns {string}
23
+ * Build the full command string for a hook script.
24
+ * Uses process.execPath (the absolute path to the current Node binary)
25
+ * so hooks work on nvm/fnm/mise systems where `node` is not on /bin/sh's PATH.
33
26
  */
34
- function extractCommand(hooksConfig, eventName, fallback) {
35
- try {
36
- return hooksConfig[eventName][0].hooks[0].command || fallback;
37
- } catch {
38
- return fallback;
39
- }
27
+ function buildCommand(scriptPath) {
28
+ return `"${process.execPath}" ${scriptPath}`;
40
29
  }
41
30
 
42
31
  /**
43
- * Check if a command is already installed in a hook group array.
44
- * @param {Array} groups
45
- * @param {string} command
46
- * @returns {boolean}
32
+ * Check if a SkillRepo hook for the given script path is already installed,
33
+ * regardless of how node was referenced (bare `node`, shebang, or full path).
47
34
  */
48
- function hasCommand(groups, command) {
35
+ function hasSkillRepoHook(groups, scriptPath) {
49
36
  if (!Array.isArray(groups)) return false;
50
37
  return groups.some(
51
38
  (group) =>
52
39
  Array.isArray(group.hooks) &&
53
- group.hooks.some((h) => h.command === command)
40
+ group.hooks.some((h) => h.command?.includes(scriptPath))
54
41
  );
55
42
  }
56
43
 
57
44
  /**
58
- * Replace legacy `node .claude/hooks/...` commands with the new direct-execution
59
- * format. Returns true if any replacement was made.
45
+ * Replace any existing SkillRepo hook command with the current node path.
46
+ * Handles all legacy formats: `node .claude/hooks/...`, `.claude/hooks/...`,
47
+ * or `/old/path/to/node .claude/hooks/...`.
48
+ * Returns true if any replacement was made.
60
49
  */
61
- function replaceLegacyCommands(groups) {
50
+ function updateExistingCommands(groups, scriptPath) {
62
51
  if (!Array.isArray(groups)) return false;
63
- let replaced = false;
52
+ const newCommand = buildCommand(scriptPath);
53
+ let updated = false;
64
54
  for (const group of groups) {
65
55
  if (!Array.isArray(group.hooks)) continue;
66
56
  for (const hook of group.hooks) {
67
- if (LEGACY_COMMANDS.has(hook.command)) {
68
- // Strip the "node " prefix → ".claude/hooks/..."
69
- hook.command = hook.command.replace(/^node /, "");
70
- replaced = true;
57
+ if (hook.command?.includes(scriptPath) && hook.command !== newCommand) {
58
+ hook.command = newCommand;
59
+ updated = true;
71
60
  }
72
61
  }
73
62
  }
74
- return replaced;
63
+ return updated;
75
64
  }
76
65
 
77
66
  /**
@@ -82,33 +71,32 @@ function replaceLegacyCommands(groups) {
82
71
  * @param {string} [preToolHookContent] - The pretool-match hook script content (optional for backward compat)
83
72
  * @returns {{ results: { path: string; action: string }[] }}
84
73
  */
74
+ // NOTE: The hooksConfig parameter is accepted for backward compatibility but
75
+ // its command strings are not used. All commands are built locally using
76
+ // process.execPath to ensure hooks work on nvm/fnm/mise systems.
85
77
  export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent, preToolHookContent) {
86
- const syncCommand = extractCommand(hooksConfig, "SessionStart", DEFAULT_SYNC_COMMAND);
87
- const promptCommand = extractCommand(hooksConfig, "UserPromptSubmit", DEFAULT_PROMPT_COMMAND);
88
- const preToolCommand = extractCommand(hooksConfig, "PreToolUse", DEFAULT_PRETOOL_COMMAND);
89
78
  const results = [];
90
79
 
91
- // Always write hook scripts (latest version from server) and mark executable.
92
- // Scripts have #!/usr/bin/env node shebangs and are invoked directly (not via `node`).
80
+ // Always write hook scripts (latest version from server).
93
81
  const syncExisted = readFileSafe(claudeSyncHook()) !== null;
94
- writeExecutable(claudeSyncHook(), syncHookContent);
82
+ writeFileSafe(claudeSyncHook(), syncHookContent);
95
83
  results.push({
96
- path: ".claude/hooks/skillrepo-sync.mjs",
84
+ path: HOOK_SCRIPTS.sync,
97
85
  action: syncExisted ? "updated" : "created",
98
86
  });
99
87
 
100
88
  const promptExisted = readFileSafe(claudePromptHook()) !== null;
101
- writeExecutable(claudePromptHook(), promptHookContent);
89
+ writeFileSafe(claudePromptHook(), promptHookContent);
102
90
  results.push({
103
- path: ".claude/hooks/skillrepo-prompt-match.mjs",
91
+ path: HOOK_SCRIPTS.prompt,
104
92
  action: promptExisted ? "updated" : "created",
105
93
  });
106
94
 
107
95
  if (preToolHookContent) {
108
96
  const preToolExisted = readFileSafe(claudePreToolHook()) !== null;
109
- writeExecutable(claudePreToolHook(), preToolHookContent);
97
+ writeFileSafe(claudePreToolHook(), preToolHookContent);
110
98
  results.push({
111
- path: ".claude/hooks/skillrepo-pretool-match.mjs",
99
+ path: HOOK_SCRIPTS.pretool,
112
100
  action: preToolExisted ? "updated" : "created",
113
101
  });
114
102
  }
@@ -140,16 +128,28 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
140
128
  if (!config.hooks) config.hooks = {};
141
129
  let changed = false;
142
130
 
143
- // Migrate legacy "node .claude/hooks/..." commands to direct execution
144
- for (const event of ["SessionStart", "UserPromptSubmit", "PreToolUse"]) {
145
- if (Array.isArray(config.hooks[event]) && replaceLegacyCommands(config.hooks[event])) {
131
+ // Build commands using the absolute node path from this machine.
132
+ // This ensures hooks work on nvm/fnm/mise systems where `node` is
133
+ // not on /bin/sh's PATH.
134
+ const syncCommand = buildCommand(HOOK_SCRIPTS.sync);
135
+ const promptCommand = buildCommand(HOOK_SCRIPTS.prompt);
136
+ const preToolCommand = buildCommand(HOOK_SCRIPTS.pretool);
137
+
138
+ // Update existing SkillRepo hook commands to use the current node path
139
+ // (handles all legacy formats: bare `node`, shebang paths, old full paths)
140
+ for (const [event, scriptPath] of [
141
+ ["SessionStart", HOOK_SCRIPTS.sync],
142
+ ["UserPromptSubmit", HOOK_SCRIPTS.prompt],
143
+ ["PreToolUse", HOOK_SCRIPTS.pretool],
144
+ ]) {
145
+ if (Array.isArray(config.hooks[event]) && updateExistingCommands(config.hooks[event], scriptPath)) {
146
146
  changed = true;
147
147
  }
148
148
  }
149
149
 
150
150
  // Merge SessionStart
151
151
  if (!Array.isArray(config.hooks.SessionStart)) config.hooks.SessionStart = [];
152
- if (!hasCommand(config.hooks.SessionStart, syncCommand)) {
152
+ if (!hasSkillRepoHook(config.hooks.SessionStart, HOOK_SCRIPTS.sync)) {
153
153
  config.hooks.SessionStart.push({
154
154
  matcher: "startup|resume",
155
155
  hooks: [{ type: "command", command: syncCommand }],
@@ -159,7 +159,7 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
159
159
 
160
160
  // Merge UserPromptSubmit
161
161
  if (!Array.isArray(config.hooks.UserPromptSubmit)) config.hooks.UserPromptSubmit = [];
162
- if (!hasCommand(config.hooks.UserPromptSubmit, promptCommand)) {
162
+ if (!hasSkillRepoHook(config.hooks.UserPromptSubmit, HOOK_SCRIPTS.prompt)) {
163
163
  config.hooks.UserPromptSubmit.push({
164
164
  hooks: [{ type: "command", command: promptCommand }],
165
165
  });
@@ -169,7 +169,7 @@ export function mergeHooksConfig(hooksConfig, syncHookContent, promptHookContent
169
169
  // Merge PreToolUse
170
170
  if (preToolHookContent) {
171
171
  if (!Array.isArray(config.hooks.PreToolUse)) config.hooks.PreToolUse = [];
172
- if (!hasCommand(config.hooks.PreToolUse, preToolCommand)) {
172
+ if (!hasSkillRepoHook(config.hooks.PreToolUse, HOOK_SCRIPTS.pretool)) {
173
173
  config.hooks.PreToolUse.push({
174
174
  hooks: [{ type: "command", command: preToolCommand }],
175
175
  });
@@ -187,7 +187,7 @@ function buildSyncHook(baseUrl) {
187
187
  return `#!/usr/bin/env node
188
188
  // SkillRepo SessionStart hook — auto-refreshes skill config files.
189
189
  // Installed by npx skillrepo init. Commit this file to your repo.
190
- import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from "fs";
190
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
191
191
  import { join, dirname } from "path";
192
192
 
193
193
  const SETUP_URL = "${setupUrl}";
@@ -223,11 +223,6 @@ function safeWrite(relPath, content) {
223
223
  writeFileSync(p, content, "utf-8");
224
224
  }
225
225
 
226
- function writeExecutable(relPath, content) {
227
- safeWrite(relPath, content);
228
- chmodSync(join(process.cwd(), relPath), 0o755);
229
- }
230
-
231
226
  try {
232
227
  const res = await fetch(SETUP_URL, {
233
228
  headers: { Authorization: \`Bearer \${API_KEY}\`, Accept: "application/json" },
@@ -239,8 +234,8 @@ try {
239
234
  if (cc.skillrepoMd?.content) safeWrite(cc.skillrepoMd.path, cc.skillrepoMd.content);
240
235
  if (cc.skillIndex?.content) safeWrite(cc.skillIndex.path, cc.skillIndex.content);
241
236
  if (cc.skillrepoConfig?.content) safeWrite(cc.skillrepoConfig.path, cc.skillrepoConfig.content);
242
- if (cc.promptHook?.content) writeExecutable(cc.promptHook.path, cc.promptHook.content);
243
- if (cc.preToolHook?.content) writeExecutable(cc.preToolHook.path, cc.preToolHook.content);
237
+ if (cc.promptHook?.content) safeWrite(cc.promptHook.path, cc.promptHook.content);
238
+ if (cc.preToolHook?.content) safeWrite(cc.preToolHook.path, cc.preToolHook.content);
244
239
  } catch { /* silently fail */ }
245
240
 
246
241
  // Repo profiling
@@ -22,6 +22,7 @@ const SYNC_CONTENT = "// sync hook";
22
22
  const PROMPT_CONTENT = "// prompt hook";
23
23
  const PRETOOL_CONTENT = "// pretool hook";
24
24
 
25
+ // Server payload hook commands (bare script paths — the CLI builds full commands locally)
25
26
  const HOOKS_CONFIG = {
26
27
  SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
27
28
  UserPromptSubmit: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" }] }],
@@ -80,14 +81,15 @@ describe("Hooks settings.local.json merger", () => {
80
81
  assert.equal(config.hooks.UserPromptSubmit.length, 1);
81
82
  });
82
83
 
83
- it("skips merge when hooks are already installed", async () => {
84
+ it("skips merge when hooks are already installed with current node path", async () => {
84
85
  const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
85
86
 
86
- // Pre-existing with both hooks already
87
+ // Pre-existing with hooks using the CURRENT node path (already up to date)
88
+ const nodeCmd = (script) => `"${process.execPath}" ${script}`;
87
89
  const existing = {
88
90
  hooks: {
89
- SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
90
- UserPromptSubmit: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" }] }],
91
+ SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: nodeCmd(".claude/hooks/skillrepo-sync.mjs") }] }],
92
+ UserPromptSubmit: [{ hooks: [{ type: "command", command: nodeCmd(".claude/hooks/skillrepo-prompt-match.mjs") }] }],
91
93
  },
92
94
  };
93
95
  mkdirSync(join(tempDir, ".claude"), { recursive: true });
@@ -119,21 +121,22 @@ describe("Hooks settings.local.json merger", () => {
119
121
  const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
120
122
  assert.equal(config.hooks.SessionStart.length, 1);
121
123
  assert.equal(config.hooks.UserPromptSubmit.length, 1);
122
- assert.equal(config.hooks.UserPromptSubmit[0].hooks[0].command, ".claude/hooks/skillrepo-prompt-match.mjs");
124
+ assert.ok(config.hooks.UserPromptSubmit[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-prompt-match.mjs"), "Command should end with script path");
123
125
  });
124
126
 
125
127
  it("always updates hook scripts even when settings merge is skipped", async () => {
126
128
  const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
127
129
 
128
- // Install once with old scripts
130
+ // Install once with old scripts but current-format commands
131
+ const nodeCmd = (script) => `"${process.execPath}" ${script}`;
129
132
  mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
130
133
  writeFileSync(join(tempDir, ".claude/hooks/skillrepo-sync.mjs"), "// old sync");
131
134
  writeFileSync(join(tempDir, ".claude/hooks/skillrepo-prompt-match.mjs"), "// old prompt");
132
135
  mkdirSync(join(tempDir, ".claude"), { recursive: true });
133
136
  const existing = {
134
137
  hooks: {
135
- SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
136
- UserPromptSubmit: [{ hooks: [{ type: "command", command: ".claude/hooks/skillrepo-prompt-match.mjs" }] }],
138
+ SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: nodeCmd(".claude/hooks/skillrepo-sync.mjs") }] }],
139
+ UserPromptSubmit: [{ hooks: [{ type: "command", command: nodeCmd(".claude/hooks/skillrepo-prompt-match.mjs") }] }],
137
140
  },
138
141
  };
139
142
  writeFileSync(join(tempDir, ".claude/settings.local.json"), JSON.stringify(existing, null, 2));
@@ -149,7 +152,7 @@ describe("Hooks settings.local.json merger", () => {
149
152
  assert.equal(promptResult.action, "updated");
150
153
  assert.equal(readFileSync(join(tempDir, ".claude/hooks/skillrepo-prompt-match.mjs"), "utf-8"), "// new prompt");
151
154
 
152
- // Settings skipped
155
+ // Settings skipped (commands already use current node path)
153
156
  const settingsResult = results.find((r) => r.path.includes("settings.local.json"));
154
157
  assert.equal(settingsResult.action, "skipped");
155
158
  });
@@ -203,7 +206,7 @@ describe("Hooks settings.local.json merger", () => {
203
206
  const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
204
207
  assert.ok(Array.isArray(config.hooks.SessionStart), "SessionStart should be an array");
205
208
  assert.equal(config.hooks.SessionStart.length, 1);
206
- assert.equal(config.hooks.SessionStart[0].hooks[0].command, ".claude/hooks/skillrepo-sync.mjs");
209
+ assert.ok(config.hooks.SessionStart[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-sync.mjs"), "Command should end with script path");
207
210
  });
208
211
 
209
212
  it("documents partial write when JSON parse fails (hook scripts written before throw)", async () => {
@@ -244,7 +247,7 @@ describe("Hooks settings.local.json merger", () => {
244
247
  const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
245
248
  assert.ok(Array.isArray(config.hooks.PreToolUse), "PreToolUse should be an array");
246
249
  assert.equal(config.hooks.PreToolUse.length, 1);
247
- assert.equal(config.hooks.PreToolUse[0].hooks[0].command, ".claude/hooks/skillrepo-pretool-match.mjs");
250
+ assert.ok(config.hooks.PreToolUse[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-pretool-match.mjs"), "Command should end with script path");
248
251
  });
249
252
 
250
253
  it("skips pretool hook when preToolHookContent is not provided", async () => {
@@ -271,10 +274,10 @@ describe("Hooks settings.local.json merger", () => {
271
274
  assert.equal(config.hooks.PreToolUse.length, 1, "should not duplicate PreToolUse entry");
272
275
  });
273
276
 
274
- it("migrates legacy 'node .claude/hooks/...' commands to direct execution", async () => {
277
+ it("migrates any non-current-node-path commands to use process.execPath", async () => {
275
278
  const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
276
279
 
277
- // Pre-existing settings with OLD command format (node .claude/hooks/...)
280
+ // Pre-existing settings with bare `node` command format
278
281
  const oldSettings = {
279
282
  hooks: {
280
283
  SessionStart: [{
@@ -303,10 +306,55 @@ describe("Hooks settings.local.json merger", () => {
303
306
  // Read the result and verify: ONE entry per event, with NEW command format
304
307
  const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
305
308
  assert.equal(config.hooks.SessionStart.length, 1);
306
- assert.equal(config.hooks.SessionStart[0].hooks[0].command, ".claude/hooks/skillrepo-sync.mjs");
309
+ assert.ok(config.hooks.SessionStart[0].hooks[0].command.includes(process.execPath), "Command should contain full node path");
310
+ assert.ok(config.hooks.SessionStart[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-sync.mjs"), "Command should end with script path");
307
311
  assert.equal(config.hooks.UserPromptSubmit.length, 1);
308
- assert.equal(config.hooks.UserPromptSubmit[0].hooks[0].command, ".claude/hooks/skillrepo-prompt-match.mjs");
312
+ assert.ok(config.hooks.UserPromptSubmit[0].hooks[0].command.includes(process.execPath), "Command should contain full node path");
313
+ assert.ok(config.hooks.UserPromptSubmit[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-prompt-match.mjs"), "Command should end with script path");
309
314
  assert.equal(config.hooks.PreToolUse.length, 1);
310
- assert.equal(config.hooks.PreToolUse[0].hooks[0].command, ".claude/hooks/skillrepo-pretool-match.mjs");
315
+ assert.ok(config.hooks.PreToolUse[0].hooks[0].command.includes(process.execPath), "Command should contain full node path");
316
+ assert.ok(config.hooks.PreToolUse[0].hooks[0].command.endsWith(".claude/hooks/skillrepo-pretool-match.mjs"), "Command should end with script path");
317
+ });
318
+
319
+ it("migrates bare shebang-style commands (v1.6.1 format)", async () => {
320
+ const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
321
+
322
+ // v1.6.1 used bare script paths relying on shebang execution
323
+ const oldSettings = {
324
+ hooks: {
325
+ SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: ".claude/hooks/skillrepo-sync.mjs" }] }],
326
+ }
327
+ };
328
+ mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
329
+ writeFileSync(join(tempDir, ".claude/settings.local.json"), JSON.stringify(oldSettings, null, 2));
330
+
331
+ const { results } = mergeHooksConfig(HOOKS_CONFIG, SYNC_CONTENT, PROMPT_CONTENT);
332
+
333
+ const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
334
+ assert.equal(config.hooks.SessionStart.length, 1, "Should not duplicate");
335
+ const cmd = config.hooks.SessionStart[0].hooks[0].command;
336
+ assert.ok(cmd.includes(process.execPath), "Should contain full node path");
337
+ assert.ok(cmd.endsWith(".claude/hooks/skillrepo-sync.mjs"), "Should end with script path");
338
+ });
339
+
340
+ it("migrates old full-path-to-node commands (nvm version change)", async () => {
341
+ const { mergeHooksConfig } = await import("../../lib/mergers/hooks-json.mjs");
342
+
343
+ // User ran init with an older nvm node version
344
+ const oldSettings = {
345
+ hooks: {
346
+ SessionStart: [{ matcher: "startup|resume", hooks: [{ type: "command", command: "/usr/local/bin/node .claude/hooks/skillrepo-sync.mjs" }] }],
347
+ }
348
+ };
349
+ mkdirSync(join(tempDir, ".claude/hooks"), { recursive: true });
350
+ writeFileSync(join(tempDir, ".claude/settings.local.json"), JSON.stringify(oldSettings, null, 2));
351
+
352
+ const { results } = mergeHooksConfig(HOOKS_CONFIG, SYNC_CONTENT, PROMPT_CONTENT);
353
+
354
+ const config = JSON.parse(readFileSync(join(tempDir, ".claude/settings.local.json"), "utf-8"));
355
+ assert.equal(config.hooks.SessionStart.length, 1, "Should not duplicate");
356
+ const cmd = config.hooks.SessionStart[0].hooks[0].command;
357
+ assert.ok(cmd.includes(process.execPath), "Should contain CURRENT node path");
358
+ assert.ok(!cmd.includes("/usr/local/bin/node"), "Should NOT contain old node path");
311
359
  });
312
360
  });