claude-nomad 0.51.0 → 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 +14 -0
- package/README.md +4 -0
- package/dist/nomad.mjs +146 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
10
|
+
## [0.51.1](https://github.com/funkadelic/claude-nomad/compare/v0.51.0...v0.51.1) (2026-06-18)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
* **doctor:** handle diverged settings keys honestly instead of advising blind pull ([#315](https://github.com/funkadelic/claude-nomad/issues/315)) ([39f8148](https://github.com/funkadelic/claude-nomad/commit/39f81480285693e08575f5ab4ecbb8b182eeb233))
|
|
16
|
+
|
|
3
17
|
## [0.51.0](https://github.com/funkadelic/claude-nomad/compare/v0.50.3...v0.51.0) (2026-06-18)
|
|
4
18
|
|
|
5
19
|
|
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(
|
|
1306
|
-
for (const key of Object.keys(
|
|
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(
|
|
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(
|
|
1314
|
-
for (const key of Object.keys(
|
|
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");
|
|
@@ -1337,10 +1438,13 @@ function partitionByCaptureExclusion(keys) {
|
|
|
1337
1438
|
}
|
|
1338
1439
|
return { promotable, excluded };
|
|
1339
1440
|
}
|
|
1340
|
-
var BIN_NODE_RE = /^(?:[A-Za-z]:)?[\\/](?:.*[\\/])?bin[\\/]node
|
|
1441
|
+
var BIN_NODE_RE = /^"?(?:[A-Za-z]:)?[\\/](?:.*[\\/])?bin[\\/]node"?$/;
|
|
1442
|
+
var LEADING_BIN_NODE_RE = /^"?(?:[A-Za-z]:)?[\\/](?:[^"\s]*[\\/])?bin[\\/]node"?(?=\s)/;
|
|
1341
1443
|
function normalizeNodePathsDeep(value) {
|
|
1342
1444
|
if (typeof value === "string") {
|
|
1343
|
-
|
|
1445
|
+
if (BIN_NODE_RE.test(value)) return "node";
|
|
1446
|
+
const lead = LEADING_BIN_NODE_RE.exec(value);
|
|
1447
|
+
return lead ? "node" + value.slice(lead[0].length) : value;
|
|
1344
1448
|
}
|
|
1345
1449
|
if (Array.isArray(value)) {
|
|
1346
1450
|
return value.map(normalizeNodePathsDeep);
|
|
@@ -1355,11 +1459,12 @@ function normalizeNodePathsDeep(value) {
|
|
|
1355
1459
|
return value;
|
|
1356
1460
|
}
|
|
1357
1461
|
function buildCaptureSubset(merged, settings, opts) {
|
|
1462
|
+
const filtered = stripGsdHookEntries(settings);
|
|
1358
1463
|
const { ahead } = classifySettingsDrift(merged, settings);
|
|
1359
1464
|
const out = {};
|
|
1360
1465
|
for (const key of ahead) {
|
|
1361
1466
|
if (CAPTURE_EXCLUDED_KEYS.has(key)) continue;
|
|
1362
|
-
const raw =
|
|
1467
|
+
const raw = filtered[key];
|
|
1363
1468
|
out[key] = opts.normalizeNodePath ? normalizeNodePathsDeep(raw) : raw;
|
|
1364
1469
|
}
|
|
1365
1470
|
return out;
|
|
@@ -1462,7 +1567,7 @@ function regenerateSettings(ts, opts = {}) {
|
|
|
1462
1567
|
return { label: overrideLabel };
|
|
1463
1568
|
}
|
|
1464
1569
|
backupBeforeWrite(settingsPath, ts);
|
|
1465
|
-
writeJsonAtomic(settingsPath, merged);
|
|
1570
|
+
writeJsonAtomic(settingsPath, stripGsdHookEntries(merged));
|
|
1466
1571
|
return { label: overrideLabel };
|
|
1467
1572
|
}
|
|
1468
1573
|
|
|
@@ -3204,6 +3309,16 @@ function tryReadJson(filePath) {
|
|
|
3204
3309
|
return null;
|
|
3205
3310
|
}
|
|
3206
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
|
+
}
|
|
3207
3322
|
function reportSettingsDriftCheck(section2) {
|
|
3208
3323
|
const claude = claudeHome();
|
|
3209
3324
|
const repo = repoHome();
|
|
@@ -3258,7 +3373,7 @@ function emitDriftRows(section2, missing, changed, promotable, excluded, hostFil
|
|
|
3258
3373
|
if (changed.length > 0) {
|
|
3259
3374
|
addItem(
|
|
3260
3375
|
section2,
|
|
3261
|
-
`${yellow(warnGlyph)} settings.json drift:
|
|
3376
|
+
`${yellow(warnGlyph)} settings.json drift: ${changed.join(", ")} diverged from the base+host merge (run 'nomad diff' to inspect; 'nomad pull' overwrites local with the repo, or edit the base/host file to keep local)`
|
|
3262
3377
|
);
|
|
3263
3378
|
}
|
|
3264
3379
|
if (promotable.length > 0 && hostFileExists) {
|
|
@@ -4335,6 +4450,7 @@ function gatherDoctorSections(opts) {
|
|
|
4335
4450
|
const parsedSettings = loadAndReportSettings(settings);
|
|
4336
4451
|
reportHostOverrides(settings, base, parsedSettings);
|
|
4337
4452
|
reportSettingsDriftCheck(settings);
|
|
4453
|
+
reportHooksBaseSelfCleanNote(settings);
|
|
4338
4454
|
const pathMap = section("Path map");
|
|
4339
4455
|
reportPathMap(pathMap);
|
|
4340
4456
|
const neverSync = section("Never-sync");
|
|
@@ -5988,6 +6104,21 @@ function previewPushLeaks(map) {
|
|
|
5988
6104
|
init_utils();
|
|
5989
6105
|
init_utils_fs();
|
|
5990
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
|
+
}
|
|
5991
6122
|
function reportSettingsAheadDrift(repo) {
|
|
5992
6123
|
const basePath = join46(repo, "shared", "settings.base.json");
|
|
5993
6124
|
if (!existsSync39(basePath)) return;
|
|
@@ -6087,7 +6218,10 @@ async function cmdPush(opts = {}) {
|
|
|
6087
6218
|
const ts = freshBackupTs(backup);
|
|
6088
6219
|
const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun }));
|
|
6089
6220
|
const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
|
|
6090
|
-
if (!dryRun)
|
|
6221
|
+
if (!dryRun) {
|
|
6222
|
+
syncSkillsPush();
|
|
6223
|
+
stripGsdHooksFromBase(repo, backup);
|
|
6224
|
+
}
|
|
6091
6225
|
const st = { dryRun, remap, extras, globalConfig: [] };
|
|
6092
6226
|
guardGitlinks(repo);
|
|
6093
6227
|
const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
|
|
@@ -6660,7 +6794,7 @@ function parsePushArgs(argv) {
|
|
|
6660
6794
|
// package.json
|
|
6661
6795
|
var package_default = {
|
|
6662
6796
|
name: "claude-nomad",
|
|
6663
|
-
version: "0.
|
|
6797
|
+
version: "0.52.0",
|
|
6664
6798
|
type: "module",
|
|
6665
6799
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
6666
6800
|
keywords: [
|
package/package.json
CHANGED