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 +7 -0
- package/README.md +4 -0
- package/dist/nomad.mjs +140 -9
- package/package.json +1 -1
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(
|
|
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");
|
|
@@ -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 =
|
|
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)
|
|
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.
|
|
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