claude-dev-env 8.43.4 → 8.43.5

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/bin/install.mjs CHANGED
@@ -1363,15 +1363,16 @@ function commandRunsHookOfRootSharingSettings(normalizedCommand, relativePath, s
1363
1363
  while (tailStart !== -1) {
1364
1364
  const characterAfterPath = normalizedCommand[tailStart + hookPathTail.length];
1365
1365
  if (characterAfterPath === undefined || commandArgumentBoundary.test(characterAfterPath)) {
1366
- let rootStart = tailStart;
1366
+ let rootPath;
1367
1367
  if (characterAfterPath === '"' || characterAfterPath === "'") {
1368
- rootStart = normalizedCommand.lastIndexOf(characterAfterPath, tailStart - 1) + 1;
1368
+ rootPath = quotedRootPathBeforeTail(normalizedCommand.slice(0, tailStart), characterAfterPath);
1369
1369
  } else {
1370
+ let rootStart = tailStart;
1370
1371
  while (rootStart > 0 && !commandArgumentBoundary.test(normalizedCommand[rootStart - 1])) {
1371
1372
  rootStart--;
1372
1373
  }
1374
+ rootPath = normalizedCommand.slice(rootStart, tailStart);
1373
1375
  }
1374
- const rootPath = normalizedCommand.slice(rootStart, tailStart);
1375
1376
  const rootSettingsRealPath = rootPath
1376
1377
  ? realPathOrNull(join(rootPath, SETTINGS_FILE_NAME))
1377
1378
  : null;
@@ -1384,6 +1385,41 @@ function commandRunsHookOfRootSharingSettings(normalizedCommand, relativePath, s
1384
1385
  return false;
1385
1386
  }
1386
1387
 
1388
+ /**
1389
+ * How each quote style of a hook path argument joins the segments around an
1390
+ * escaped character, keyed by the quote, in the forward-slash command form.
1391
+ */
1392
+ const QUOTED_ARGUMENT_ESCAPES = {
1393
+ '"': { segmentJoin: '"^%"', joinedCharacter: '%' },
1394
+ "'": { segmentJoin: "'/''", joinedCharacter: "'" },
1395
+ };
1396
+
1397
+ /**
1398
+ * Decode the root part of a quoted argument that `commandArgumentFromPath`
1399
+ * wrote, read from the command text that ends where the hook path tail starts.
1400
+ *
1401
+ * The win32 form splits a path at each `%` into `"a"^%"b"`. The POSIX form
1402
+ * escapes each `'` as `'\''`, which reads `'/''` once backslashes become
1403
+ * forward slashes.
1404
+ *
1405
+ * @param {string} commandBeforeTail Normalized command text up to the hook path tail.
1406
+ * @param {string} closingQuote The quote that closes the argument after the tail.
1407
+ * @returns {string} The root path with each escaped character restored.
1408
+ */
1409
+ function quotedRootPathBeforeTail(commandBeforeTail, closingQuote) {
1410
+ const { segmentJoin, joinedCharacter } = QUOTED_ARGUMENT_ESCAPES[closingQuote];
1411
+ const segments = [];
1412
+ let segmentEnd = commandBeforeTail.length;
1413
+ let openingQuote = commandBeforeTail.lastIndexOf(closingQuote, segmentEnd - 1);
1414
+ while (commandBeforeTail.slice(openingQuote - segmentJoin.length + 1, openingQuote + 1) === segmentJoin) {
1415
+ segments.unshift(commandBeforeTail.slice(openingQuote + 1, segmentEnd));
1416
+ segmentEnd = openingQuote - segmentJoin.length + 1;
1417
+ openingQuote = commandBeforeTail.lastIndexOf(closingQuote, segmentEnd - 1);
1418
+ }
1419
+ segments.unshift(commandBeforeTail.slice(openingQuote + 1, segmentEnd));
1420
+ return segments.join(joinedCharacter);
1421
+ }
1422
+
1387
1423
  function commandTailEndsAtManagedHook(normalizedCommand, relativePath) {
1388
1424
  if (commandHoldsPathAtBoundary(normalizedCommand, `/.claude/hooks/${relativePath}`)) {
1389
1425
  return true;
@@ -243,3 +243,49 @@ test('the merge prunes a quoted hook of a sharing root whose path holds a space'
243
243
  rmSync(sandboxRoot, { recursive: true, force: true });
244
244
  }
245
245
  });
246
+
247
+ for (const { shape, rootDirectoryName, platform } of [
248
+ { shape: 'a percent sign', rootDirectoryName: 'Jane 100% Doe', platform: 'win32' },
249
+ { shape: 'an apostrophe', rootDirectoryName: "Jane O'Doe", platform: 'linux' },
250
+ ]) {
251
+ test(`the merge prunes a ${platform}-quoted hook of a sharing root whose path holds ${shape}`, t => {
252
+ const sandboxRoot = mkdtempSync(join(tmpdir(), 'cdev-shared-settings-escape-'));
253
+ try {
254
+ const installRoot = join(sandboxRoot, 'install-root');
255
+ const sharingRoot = join(sandboxRoot, rootDirectoryName, 'profiles', 'second');
256
+ for (const eachRoot of [installRoot, sharingRoot]) mkdirSync(eachRoot, { recursive: true });
257
+ const settingsPath = join(installRoot, 'settings.json');
258
+ writeFileSync(settingsPath, '{}\n');
259
+ try {
260
+ symlinkSync(settingsPath, join(sharingRoot, 'settings.json'), 'file');
261
+ } catch (linkError) {
262
+ t.skip(`this host cannot create a file symlink (${linkError.code})`);
263
+ return;
264
+ }
265
+ const hooksConfig = {
266
+ hooks: {
267
+ SessionStart: [{
268
+ matcher: '',
269
+ hooks: [{ type: 'command', command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/start.py' }],
270
+ }],
271
+ },
272
+ };
273
+ const forwardSlashed = rootPath => rootPath.replace(/\\/g, '/');
274
+ const sharingHookPath = `${forwardSlashed(sharingRoot)}/hooks/session/start.py`;
275
+ const sharingCommand = `python3 ${commandArgumentFromPath(sharingHookPath, platform)}`;
276
+ const settings = {
277
+ hooks: {
278
+ SessionStart: [{ matcher: '', hooks: [{ type: 'command', command: sharingCommand }] }],
279
+ },
280
+ };
281
+
282
+ mergeHooksIntoSettings(settings, hooksConfig, forwardSlashed(installRoot), 'python3', settingsPath);
283
+
284
+ const allCommands = settings.hooks.SessionStart.flatMap(group => group.hooks.map(hook => hook.command));
285
+ assert.ok(!allCommands.includes(sharingCommand), `the sharing root entry ${sharingCommand} is pruned`);
286
+ assert.equal(allCommands.length, 1, 'only the install root entry stays after the merge');
287
+ } finally {
288
+ rmSync(sandboxRoot, { recursive: true, force: true });
289
+ }
290
+ });
291
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.43.4",
3
+ "version": "8.43.5",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,7 @@ APPROVED_TEST_PATHS_BY_PRODUCTION_PATH: dict[
23
23
  {
24
24
  PurePosixPath("packages/claude-dev-env/bin/install.agents-home.test.mjs"),
25
25
  PurePosixPath("packages/claude-dev-env/bin/install.cursor-rules.test.mjs"),
26
+ PurePosixPath("packages/claude-dev-env/bin/install.shared-settings.test.mjs"),
26
27
  }
27
28
  ),
28
29
  PurePosixPath(
@@ -557,6 +557,19 @@ def test_pairing_accepts_installer_module_with_the_agents_home_suite(
557
557
  assert all_paths == ()
558
558
 
559
559
 
560
+ def test_pairing_accepts_installer_module_with_the_shared_settings_suite(
561
+ tmp_path: Path,
562
+ ) -> None:
563
+ all_paths = _diagnostic_paths(
564
+ tmp_path,
565
+ _body_change_at(_INSTALLER_MODULE_PATH),
566
+ _body_change_at(
567
+ PurePosixPath("packages/claude-dev-env/bin/install.shared-settings.test.mjs")
568
+ ),
569
+ )
570
+ assert all_paths == ()
571
+
572
+
560
573
  _COMMENT_RULES_PRODUCTION_PATH = PurePosixPath(
561
574
  "packages/claude-dev-env/hooks/blocking/code_rules_comments.py"
562
575
  )