claude-nomad 0.51.1 → 0.52.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.52.0](https://github.com/funkadelic/claude-nomad/compare/v0.51.1...v0.52.0) (2026-06-19)
4
+
5
+
6
+ ### Added
7
+
8
+ * stop syncing gsd-owned hook entries from settings.base.json ([#317](https://github.com/funkadelic/claude-nomad/issues/317)) ([31f5f79](https://github.com/funkadelic/claude-nomad/commit/31f5f79b16245dd792a12a7fc315661c34bae610))
9
+
3
10
  ## [0.51.1](https://github.com/funkadelic/claude-nomad/compare/v0.51.0...v0.51.1) (2026-06-18)
4
11
 
5
12
 
package/README.md CHANGED
@@ -33,6 +33,10 @@ survives different file paths and your secrets never ride along.
33
33
  churn). Skills sync as a filtered copy: your own skills travel, `gsd-*` skills are excluded (see
34
34
  `SHARED_LINKS` and `src/skills-sync.ts` in `src/config.ts`). Settings merge a shared base with a
35
35
  per-host override, so one machine can run a different model or MCP URL without forking the rest.
36
+ GSD-owned hook entries (scripts whose basename starts with `gsd-`) are filtered out of the
37
+ generated `~/.claude/settings.json` during pull and stripped from `shared/settings.base.json` on
38
+ the next push; GSD reinstalls the correct per-host hook set itself. A non-gsd hook you add to your
39
+ live settings syncs normally via `nomad capture-settings`.
36
40
  - **Every push is secret-scanned.** Only an explicit allow-list of paths ever leaves the machine,
37
41
  credentials never sync, and gitleaks scans the exact files about to be published. The push aborts
38
42
  on any hit, with an interactive menu to redact, allow, or drop the finding.
package/dist/nomad.mjs CHANGED
@@ -1270,6 +1270,102 @@ import { existsSync as existsSync5 } from "node:fs";
1270
1270
  import { join as join7 } from "node:path";
1271
1271
  import { createInterface } from "node:readline/promises";
1272
1272
 
1273
+ // src/hooks-filter.ts
1274
+ init_config();
1275
+ var KNOWN_LAUNCHER_BASENAMES = /* @__PURE__ */ new Set(["node", "bash", "sh"]);
1276
+ function scriptBasename(token) {
1277
+ const lastSlash = Math.max(token.lastIndexOf("/"), token.lastIndexOf("\\"));
1278
+ return lastSlash >= 0 ? token.slice(lastSlash + 1) : token;
1279
+ }
1280
+ function isGsdHookEntry(command) {
1281
+ const tokens = command.trim().split(/\s+/);
1282
+ if (tokens.length === 0 || tokens[0] === "") return false;
1283
+ const envAssign = /^[A-Za-z_]\w*=/;
1284
+ let i = 0;
1285
+ while (i < tokens.length && envAssign.test(tokens[i])) {
1286
+ i++;
1287
+ }
1288
+ const first = tokens[i] ?? "";
1289
+ const firstBase = scriptBasename(first);
1290
+ const firstHasPath = first.includes("/") || first.includes("\\");
1291
+ if (firstHasPath && !KNOWN_LAUNCHER_BASENAMES.has(firstBase) || first.startsWith(GSD_PREFIX)) {
1292
+ return firstBase.startsWith(GSD_PREFIX);
1293
+ }
1294
+ for (let j = i + 1; j < tokens.length; j++) {
1295
+ if (tokens[j].startsWith("-")) continue;
1296
+ return scriptBasename(tokens[j]).startsWith(GSD_PREFIX);
1297
+ }
1298
+ return false;
1299
+ }
1300
+ function filterMatcherEntry(entry) {
1301
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return entry;
1302
+ const entryObj = entry;
1303
+ if (!Array.isArray(entryObj.hooks)) return entryObj;
1304
+ const innerHooks = entryObj.hooks;
1305
+ const kept = innerHooks.filter((h2) => {
1306
+ if (h2 === null || typeof h2 !== "object" || Array.isArray(h2)) return true;
1307
+ const hookObj = h2;
1308
+ const cmd = hookObj.command;
1309
+ return !isGsdHookEntry(typeof cmd === "string" ? cmd : "");
1310
+ });
1311
+ if (kept.length === 0) return null;
1312
+ return { ...entryObj, hooks: kept };
1313
+ }
1314
+ function filterEventMatchers(matchers) {
1315
+ if (!Array.isArray(matchers)) return matchers;
1316
+ const kept = [];
1317
+ for (const entry of matchers) {
1318
+ const result = filterMatcherEntry(entry);
1319
+ if (result != null) kept.push(result);
1320
+ }
1321
+ return kept.length === 0 ? null : kept;
1322
+ }
1323
+ function stripGsdHookEntries(settings) {
1324
+ const out = {};
1325
+ for (const [key, value] of Object.entries(settings)) {
1326
+ if (key !== "hooks") {
1327
+ out[key] = value;
1328
+ continue;
1329
+ }
1330
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
1331
+ out[key] = value;
1332
+ continue;
1333
+ }
1334
+ const hooksObj = value;
1335
+ const filteredHooks = {};
1336
+ for (const [event, matchers] of Object.entries(hooksObj)) {
1337
+ const filtered = filterEventMatchers(matchers);
1338
+ if (filtered !== null) filteredHooks[event] = filtered;
1339
+ }
1340
+ if (Object.keys(filteredHooks).length > 0) out[key] = filteredHooks;
1341
+ }
1342
+ return out;
1343
+ }
1344
+ function matcherHasGsdEntry(entry) {
1345
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return false;
1346
+ const entryObj = entry;
1347
+ if (!Array.isArray(entryObj.hooks)) return false;
1348
+ for (const h2 of entryObj.hooks) {
1349
+ if (h2 === null || typeof h2 !== "object" || Array.isArray(h2)) continue;
1350
+ const hookObj = h2;
1351
+ const cmd = hookObj.command;
1352
+ if (isGsdHookEntry(typeof cmd === "string" ? cmd : "")) return true;
1353
+ }
1354
+ return false;
1355
+ }
1356
+ function baseHasGsdHookEntries(settings) {
1357
+ const hooksVal = settings.hooks;
1358
+ if (hooksVal === null || typeof hooksVal !== "object" || Array.isArray(hooksVal)) return false;
1359
+ const hooksObj = hooksVal;
1360
+ for (const matchers of Object.values(hooksObj)) {
1361
+ if (!Array.isArray(matchers)) continue;
1362
+ for (const entry of matchers) {
1363
+ if (matcherHasGsdEntry(entry)) return true;
1364
+ }
1365
+ }
1366
+ return false;
1367
+ }
1368
+
1273
1369
  // src/commands.capture-settings.core.ts
1274
1370
  function arraysEqual(a, b) {
1275
1371
  if (a.length !== b.length) return false;
@@ -1299,19 +1395,24 @@ function deepEqual(a, b) {
1299
1395
  return false;
1300
1396
  }
1301
1397
  function classifySettingsDrift(merged, settings) {
1398
+ const filteredMerged = stripGsdHookEntries(merged);
1399
+ const filteredSettings = stripGsdHookEntries(settings);
1302
1400
  const behind = [];
1303
1401
  const ahead = [];
1304
1402
  const changed = [];
1305
- const settingsKeys = new Set(Object.keys(settings));
1306
- for (const key of Object.keys(merged)) {
1403
+ const settingsKeys = new Set(Object.keys(filteredSettings));
1404
+ for (const key of Object.keys(filteredMerged)) {
1307
1405
  if (!settingsKeys.has(key)) {
1308
1406
  behind.push(key);
1309
- } else if (!deepEqual(normalizeNodePathsDeep(merged[key]), normalizeNodePathsDeep(settings[key]))) {
1407
+ } else if (!deepEqual(
1408
+ normalizeNodePathsDeep(filteredMerged[key]),
1409
+ normalizeNodePathsDeep(filteredSettings[key])
1410
+ )) {
1310
1411
  changed.push(key);
1311
1412
  }
1312
1413
  }
1313
- const mergedKeys = new Set(Object.keys(merged));
1314
- for (const key of Object.keys(settings)) {
1414
+ const mergedKeys = new Set(Object.keys(filteredMerged));
1415
+ for (const key of Object.keys(filteredSettings)) {
1315
1416
  if (!mergedKeys.has(key)) ahead.push(key);
1316
1417
  }
1317
1418
  const collator = (a, b) => a.localeCompare(b, "en");
@@ -1358,11 +1459,12 @@ function normalizeNodePathsDeep(value) {
1358
1459
  return value;
1359
1460
  }
1360
1461
  function buildCaptureSubset(merged, settings, opts) {
1462
+ const filtered = stripGsdHookEntries(settings);
1361
1463
  const { ahead } = classifySettingsDrift(merged, settings);
1362
1464
  const out = {};
1363
1465
  for (const key of ahead) {
1364
1466
  if (CAPTURE_EXCLUDED_KEYS.has(key)) continue;
1365
- const raw = settings[key];
1467
+ const raw = filtered[key];
1366
1468
  out[key] = opts.normalizeNodePath ? normalizeNodePathsDeep(raw) : raw;
1367
1469
  }
1368
1470
  return out;
@@ -1465,7 +1567,7 @@ function regenerateSettings(ts, opts = {}) {
1465
1567
  return { label: overrideLabel };
1466
1568
  }
1467
1569
  backupBeforeWrite(settingsPath, ts);
1468
- writeJsonAtomic(settingsPath, merged);
1570
+ writeJsonAtomic(settingsPath, stripGsdHookEntries(merged));
1469
1571
  return { label: overrideLabel };
1470
1572
  }
1471
1573
 
@@ -3207,6 +3309,16 @@ function tryReadJson(filePath) {
3207
3309
  return null;
3208
3310
  }
3209
3311
  }
3312
+ function reportHooksBaseSelfCleanNote(section2) {
3313
+ const basePath = join28(repoHome(), "shared", "settings.base.json");
3314
+ const base = tryReadJson(basePath);
3315
+ if (base === null) return;
3316
+ if (!baseHasGsdHookEntries(base)) return;
3317
+ addItem(
3318
+ section2,
3319
+ `${dim(infoGlyph)} gsd now owns hook entries per-host; shared/settings.base.json self-cleans on your next 'nomad push'`
3320
+ );
3321
+ }
3210
3322
  function reportSettingsDriftCheck(section2) {
3211
3323
  const claude = claudeHome();
3212
3324
  const repo = repoHome();
@@ -4338,6 +4450,7 @@ function gatherDoctorSections(opts) {
4338
4450
  const parsedSettings = loadAndReportSettings(settings);
4339
4451
  reportHostOverrides(settings, base, parsedSettings);
4340
4452
  reportSettingsDriftCheck(settings);
4453
+ reportHooksBaseSelfCleanNote(settings);
4341
4454
  const pathMap = section("Path map");
4342
4455
  reportPathMap(pathMap);
4343
4456
  const neverSync = section("Never-sync");
@@ -5991,6 +6104,21 @@ function previewPushLeaks(map) {
5991
6104
  init_utils();
5992
6105
  init_utils_fs();
5993
6106
  init_utils_json();
6107
+ function stripGsdHooksFromBase(repo, backup) {
6108
+ const basePath = join46(repo, "shared", "settings.base.json");
6109
+ if (!existsSync39(basePath)) return;
6110
+ let base;
6111
+ try {
6112
+ base = readJson(basePath);
6113
+ } catch {
6114
+ return;
6115
+ }
6116
+ if (!baseHasGsdHookEntries(base)) return;
6117
+ const stripped = stripGsdHookEntries(base);
6118
+ const ts = freshBackupTs(backup);
6119
+ backupRepoWrite(basePath, ts, repo);
6120
+ writeJsonAtomic(basePath, stripped);
6121
+ }
5994
6122
  function reportSettingsAheadDrift(repo) {
5995
6123
  const basePath = join46(repo, "shared", "settings.base.json");
5996
6124
  if (!existsSync39(basePath)) return;
@@ -6090,7 +6218,10 @@ async function cmdPush(opts = {}) {
6090
6218
  const ts = freshBackupTs(backup);
6091
6219
  const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun }));
6092
6220
  const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
6093
- if (!dryRun) syncSkillsPush();
6221
+ if (!dryRun) {
6222
+ syncSkillsPush();
6223
+ stripGsdHooksFromBase(repo, backup);
6224
+ }
6094
6225
  const st = { dryRun, remap, extras, globalConfig: [] };
6095
6226
  guardGitlinks(repo);
6096
6227
  const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
@@ -6663,7 +6794,7 @@ function parsePushArgs(argv) {
6663
6794
  // package.json
6664
6795
  var package_default = {
6665
6796
  name: "claude-nomad",
6666
- version: "0.51.1",
6797
+ version: "0.52.0",
6667
6798
  type: "module",
6668
6799
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
6669
6800
  keywords: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-nomad",
3
- "version": "0.51.1",
3
+ "version": "0.52.0",
4
4
  "type": "module",
5
5
  "description": "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
6
6
  "keywords": [