claude-nomad 0.50.1 → 0.50.3

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/.gitleaks.toml CHANGED
@@ -6,14 +6,26 @@
6
6
  [extend]
7
7
  useDefault = true
8
8
 
9
+ # Path-scoped: this tool-output noise (gitleaks fingerprints, coverage
10
+ # line-keys, npm audit JSON id hashes, Sonar issue keys) accumulates in Claude
11
+ # Code session transcripts when a conversation runs those tools. Two of these
12
+ # regexes are structurally generic enough that, left global, they could suppress
13
+ # a real credential that happened to sit in a `path:word:digit` or hex-`id`
14
+ # context anywhere in the repo. Scoping to `shared/projects/<logical>/.../*.jsonl`
15
+ # with `condition = "AND"` (matching every other block below) confines the
16
+ # suppression to synced transcripts: a real secret in a source file still fires.
9
17
  [[allowlists]]
10
- description = "claude-nomad: structurally-distinguishable tool-output noise"
18
+ description = "claude-nomad: structurally-distinguishable tool-output noise in synced session transcripts"
11
19
  regexes = [
12
20
  '''AY[A-Za-z0-9_-]{20,}''',
13
21
  '''[\w./-]+\.[A-Za-z0-9]+:[\w-]+:\d+''',
14
22
  '''"id"\s*:\s*"[a-f0-9]{40,64}"''',
15
23
  '''key=[a-f0-9]{8,} [\w./-]+\.\w+:\d+''',
16
24
  ]
25
+ paths = [
26
+ '''^shared/projects/[^/]+/.*\.jsonl$''',
27
+ ]
28
+ condition = "AND"
17
29
 
18
30
  # Path-scoped: the documented test-fixture github-pat literal AND the three
19
31
  # entropy-variant placeholders (zero-suffix, alphabet, repeat-A) accumulate
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.50.3](https://github.com/funkadelic/claude-nomad/compare/v0.50.2...v0.50.3) (2026-06-17)
4
+
5
+
6
+ ### Fixed
7
+
8
+ * **gitleaks:** scope tool-output noise allowlist to session transcripts ([#305](https://github.com/funkadelic/claude-nomad/issues/305)) ([ca76680](https://github.com/funkadelic/claude-nomad/commit/ca766803a425d21747f959606d6436989ce249e6))
9
+ * **push:** make --redact-all all-or-nothing ([#302](https://github.com/funkadelic/claude-nomad/issues/302)) ([f2cffeb](https://github.com/funkadelic/claude-nomad/commit/f2cffeb9204b68fb046ee55c9768e065272fb431))
10
+ * **push:** warn when drop-session/redact target is already in pushed history ([#304](https://github.com/funkadelic/claude-nomad/issues/304)) ([739676f](https://github.com/funkadelic/claude-nomad/commit/739676ffbf1345fc7ff124ac7eafb9802ba09be4))
11
+ * **redact:** warn when a finding match is not located in the file ([#310](https://github.com/funkadelic/claude-nomad/issues/310)) ([db1442b](https://github.com/funkadelic/claude-nomad/commit/db1442b311db04944b655ece91a56ef23a39abe1))
12
+ * **remap:** make session-transcript mirror copy atomic ([4fc518f](https://github.com/funkadelic/claude-nomad/commit/4fc518fd80ce7fffb56ba2a6d135e6f6b795bc91))
13
+ * **utils:** guard deepMerge against prototype pollution ([#299](https://github.com/funkadelic/claude-nomad/issues/299)) ([8e57539](https://github.com/funkadelic/claude-nomad/commit/8e575393a320386dd7329aab8071fd84557ec4e9))
14
+
15
+
16
+ ### Changed
17
+
18
+ * **config:** centralize path-map.json shape validation ([#306](https://github.com/funkadelic/claude-nomad/issues/306)) ([88edda3](https://github.com/funkadelic/claude-nomad/commit/88edda3f45954d4dc89d65e2ec55a7d9d5b6b3f8))
19
+ * **dispatch:** share argv token-parser primitives ([#307](https://github.com/funkadelic/claude-nomad/issues/307)) ([9f62b51](https://github.com/funkadelic/claude-nomad/commit/9f62b5156bbc3b7e03f8a81fe0f844cbeb1da3fa))
20
+ * **doctor:** rename repository check module to git-state ([#309](https://github.com/funkadelic/claude-nomad/issues/309)) ([b6a0d5c](https://github.com/funkadelic/claude-nomad/commit/b6a0d5c8461c2f63af7d77464db9558271d46f46))
21
+ * **utils:** dedup backup helpers and document lock/exit conventions ([#308](https://github.com/funkadelic/claude-nomad/issues/308)) ([4c2d59d](https://github.com/funkadelic/claude-nomad/commit/4c2d59d97c875851733175e4b4346e5714c48a32))
22
+
23
+
24
+ ### Documentation
25
+
26
+ * **recovery:** reflect session-scoped allowlist and redact no-match warning ([#311](https://github.com/funkadelic/claude-nomad/issues/311)) ([7faf564](https://github.com/funkadelic/claude-nomad/commit/7faf5647d7dfd30822950d687fd8be15b5c49910))
27
+
28
+
29
+ ### Testing
30
+
31
+ * **push:** add full-pipeline cmdPush E2E against real git and gitleaks ([#303](https://github.com/funkadelic/claude-nomad/issues/303)) ([3c01262](https://github.com/funkadelic/claude-nomad/commit/3c012629e7b99dcf4c5e679d93acb353ee6c3409))
32
+
33
+ ## [0.50.2](https://github.com/funkadelic/claude-nomad/compare/v0.50.1...v0.50.2) (2026-06-15)
34
+
35
+
36
+ ### Fixed
37
+
38
+ * **push:** skip gsd-dropped hooks and agents from shared push ([#295](https://github.com/funkadelic/claude-nomad/issues/295)) ([b8f6665](https://github.com/funkadelic/claude-nomad/commit/b8f66658dd47dac40bfafd9899206a481d04a87a))
39
+
40
+
41
+ ### Dependencies
42
+
43
+ * bump SonarSource/sonarqube-scan-action from 8.1.0 to 8.2.0 ([#297](https://github.com/funkadelic/claude-nomad/issues/297)) ([fdb681c](https://github.com/funkadelic/claude-nomad/commit/fdb681c4d346b9bd82b34fe6d9dee2d202186473))
44
+ * bump the dev-dependencies group with 5 updates ([#298](https://github.com/funkadelic/claude-nomad/issues/298)) ([0909a02](https://github.com/funkadelic/claude-nomad/commit/0909a025b698162c7a695354465046973ee93ab4))
45
+
3
46
  ## [0.50.1](https://github.com/funkadelic/claude-nomad/compare/v0.50.0...v0.50.1) (2026-06-12)
4
47
 
5
48
 
package/README.md CHANGED
@@ -100,6 +100,8 @@ When `nomad push` detects a potential secret, it drops into an interactive menu
100
100
  a recovery hint (non-TTY/CI). Three non-interactive recovery paths are available without the menu:
101
101
 
102
102
  - `nomad push --redact-all` -- scrub every finding from the local transcript in place, then push.
103
+ All-or-nothing: if any finding cannot be redacted (an active session, or one that does not map to
104
+ a synced transcript), nothing is changed and the push stops so you can handle those sessions.
103
105
  - `nomad push --allow <rule>` -- record findings matching one gitleaks rule id as false positives
104
106
  (appends their fingerprints to `.gitleaksignore`), then re-scan and push.
105
107
  - `nomad push --allow-all` -- record every current finding as a false positive, then re-scan and
package/dist/nomad.mjs CHANGED
@@ -5,11 +5,20 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ var __esm = (fn, res, err) => function __init() {
9
+ if (err) throw err[0];
10
+ try {
11
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
12
+ } catch (e) {
13
+ throw err = [e], e;
14
+ }
10
15
  };
11
16
  var __commonJS = (cb, mod) => function __require() {
12
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
17
+ try {
18
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
19
+ } catch (e) {
20
+ throw mod = 0, e;
21
+ }
13
22
  };
14
23
  var __export = (target, all) => {
15
24
  for (var name in all)
@@ -442,16 +451,41 @@ function readJson(path) {
442
451
  return data;
443
452
  }
444
453
  function readPathMap(mapPath) {
454
+ let parsed;
445
455
  try {
446
- return readJson(mapPath);
456
+ parsed = readJson(mapPath);
447
457
  } catch (err) {
448
458
  const verb = err instanceof SyntaxError ? "parse" : "read";
449
459
  throw new NomadFatal(`could not ${verb} path-map.json: ${err.message}`);
450
460
  }
461
+ const shapeError = validatePathMapShape(parsed);
462
+ if (shapeError !== null) throw new NomadFatal(shapeError);
463
+ return parsed;
464
+ }
465
+ function validatePathMapShape(raw) {
466
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
467
+ return "path-map.json invalid schema: top-level value must be an object";
468
+ }
469
+ const projects = raw.projects;
470
+ if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
471
+ return 'path-map.json invalid schema: "projects" must be an object';
472
+ }
473
+ for (const [name, hosts] of Object.entries(projects)) {
474
+ if (hosts === null || typeof hosts !== "object" || Array.isArray(hosts)) {
475
+ return `path-map.json invalid schema: project "${name}" hosts must be an object`;
476
+ }
477
+ for (const [host, value] of Object.entries(hosts)) {
478
+ if (typeof value !== "string") {
479
+ return `path-map.json invalid schema: project "${name}" host "${host}" path must be a string`;
480
+ }
481
+ }
482
+ }
483
+ return null;
451
484
  }
452
485
  function deepMerge(target, source) {
453
486
  const out = { ...target };
454
487
  for (const [key, value] of Object.entries(source)) {
488
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
455
489
  const existing = out[key];
456
490
  const bothObjects = value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing);
457
491
  out[key] = bothObjects ? deepMerge(existing, value) : value;
@@ -493,7 +527,7 @@ import {
493
527
  symlinkSync,
494
528
  writeFileSync
495
529
  } from "node:fs";
496
- import { dirname, join as join2, relative } from "node:path";
530
+ import { dirname, join as join2, relative, sep } from "node:path";
497
531
  function writeJsonAtomic(path, data) {
498
532
  const mode = existsSync(path) ? statSync(path).mode & 511 : 384;
499
533
  const tmp = `${path}.tmp.${process.pid}`;
@@ -533,33 +567,22 @@ function ensureSymlink(linkPath, target) {
533
567
  symlinkSync(target, linkPath);
534
568
  log(`linked ${linkPath} -> ${target}`);
535
569
  }
536
- function backupBeforeWrite(absPath, ts) {
570
+ function backupUnder(absPath, anchor, destRoot) {
537
571
  if (!existsSync(absPath)) return;
538
- const claude = claudeHome();
539
- const rel = relative(claude, absPath);
540
- if (rel.startsWith("..") || rel === "") return;
541
- const backupRoot = join2(backupBase(), ts);
542
- const dst = join2(backupRoot, rel);
572
+ const rel = relative(anchor, absPath);
573
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`)) return;
574
+ const dst = join2(destRoot, rel);
543
575
  mkdirSync(dirname(dst), { recursive: true });
544
576
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
545
577
  }
578
+ function backupBeforeWrite(absPath, ts) {
579
+ backupUnder(absPath, claudeHome(), join2(backupBase(), ts));
580
+ }
546
581
  function backupRepoWrite(absPath, ts, repoHome2) {
547
- if (!existsSync(absPath)) return;
548
- const rel = relative(repoHome2, absPath);
549
- if (rel.startsWith("..") || rel === "") return;
550
- const backupRoot = join2(backupBase(), ts, "repo");
551
- const dst = join2(backupRoot, rel);
552
- mkdirSync(dirname(dst), { recursive: true });
553
- cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
582
+ backupUnder(absPath, repoHome2, join2(backupBase(), ts, "repo"));
554
583
  }
555
584
  function backupExtrasWrite(absPath, ts, projectRoot) {
556
- if (!existsSync(absPath)) return;
557
- const rel = relative(projectRoot, absPath);
558
- if (rel.startsWith("..") || rel === "") return;
559
- const backupRoot = join2(backupBase(), ts, "extras");
560
- const dst = join2(backupRoot, encodePath(projectRoot), rel);
561
- mkdirSync(dirname(dst), { recursive: true });
562
- cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
585
+ backupUnder(absPath, projectRoot, join2(backupBase(), ts, "extras", encodePath(projectRoot)));
563
586
  }
564
587
  var init_utils_fs = __esm({
565
588
  "src/utils.fs.ts"() {
@@ -1310,7 +1333,7 @@ init_config();
1310
1333
  init_utils();
1311
1334
  init_utils_json();
1312
1335
  import { cpSync as cpSync3, existsSync as existsSync5, lstatSync as lstatSync4, realpathSync, renameSync as renameSync2, rmSync as rmSync3 } from "node:fs";
1313
- import { join as join6, sep } from "node:path";
1336
+ import { join as join6, sep as sep2 } from "node:path";
1314
1337
  function ejectChecklist() {
1315
1338
  return [
1316
1339
  "Manual steps remaining to finish leaving claude-nomad on this host:",
@@ -1352,7 +1375,7 @@ function resolveSharedRoot(repoHome2) {
1352
1375
  }
1353
1376
  }
1354
1377
  function isManagedTarget(target, sharedRoot) {
1355
- return target.startsWith(sharedRoot + sep);
1378
+ return target.startsWith(sharedRoot + sep2);
1356
1379
  }
1357
1380
  function materializeOne(name, linkPath, sharedRoot) {
1358
1381
  const target = realpathSync(linkPath);
@@ -1832,35 +1855,12 @@ function reportPathMap(section2) {
1832
1855
  }
1833
1856
  const map = readJsonSafe(mapPath, mapPath, section2);
1834
1857
  if (map === null) return;
1835
- const projects = map.projects;
1836
- if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
1837
- addItem(
1838
- section2,
1839
- `${red(failGlyph)} path-map.json invalid schema: "projects" must be an object`
1840
- );
1858
+ const shapeError = validatePathMapShape(map);
1859
+ if (shapeError !== null) {
1860
+ addItem(section2, `${red(failGlyph)} ${shapeError}`);
1841
1861
  process.exitCode = 1;
1842
1862
  return;
1843
1863
  }
1844
- for (const [name, hosts] of Object.entries(projects)) {
1845
- if (hosts === null || typeof hosts !== "object" || Array.isArray(hosts)) {
1846
- addItem(
1847
- section2,
1848
- `${red(failGlyph)} path-map.json invalid schema: project "${name}" hosts must be an object`
1849
- );
1850
- process.exitCode = 1;
1851
- return;
1852
- }
1853
- for (const [hostName, mappedPath] of Object.entries(hosts)) {
1854
- if (typeof mappedPath !== "string") {
1855
- addItem(
1856
- section2,
1857
- `${red(failGlyph)} path-map.json invalid schema: project "${name}" host "${hostName}" path must be a string`
1858
- );
1859
- process.exitCode = 1;
1860
- return;
1861
- }
1862
- }
1863
- }
1864
1864
  reportMappedProjects(section2, map);
1865
1865
  reportUnmappedProjects(section2, map);
1866
1866
  reportPathCollisions(section2, map);
@@ -1874,7 +1874,7 @@ function reportNeverSync(section2) {
1874
1874
  );
1875
1875
  }
1876
1876
 
1877
- // src/commands.doctor.checks.repository.ts
1877
+ // src/commands.doctor.checks.git-state.ts
1878
1878
  init_color();
1879
1879
  init_config();
1880
1880
  import { execFileSync as execFileSync4 } from "node:child_process";
@@ -2194,21 +2194,27 @@ init_config();
2194
2194
  init_utils();
2195
2195
  init_utils_fs();
2196
2196
  init_utils_json();
2197
- import { cpSync as cpSync4, existsSync as existsSync15, mkdirSync as mkdirSync3, readdirSync as readdirSync6, rmSync as rmSync6, statSync as statSync4 } from "node:fs";
2198
- import { join as join19, relative as relative3, sep as sep2 } from "node:path";
2199
- function copyDir(src, dst) {
2197
+ import { cpSync as cpSync4, existsSync as existsSync15, mkdirSync as mkdirSync3, readdirSync as readdirSync6, renameSync as renameSync3, rmSync as rmSync6, statSync as statSync4 } from "node:fs";
2198
+ import { join as join19, relative as relative3, sep as sep3 } from "node:path";
2199
+ var TMP_SUFFIX = ".nomad-tmp";
2200
+ function atomicMirror(src, dst, options) {
2201
+ const tmp = `${dst}${TMP_SUFFIX}`;
2202
+ rmSync6(tmp, { recursive: true, force: true });
2203
+ cpSync4(src, tmp, options);
2200
2204
  rmSync6(dst, { recursive: true, force: true });
2201
- cpSync4(src, dst, { recursive: true, force: true });
2205
+ renameSync3(tmp, dst);
2206
+ }
2207
+ function copyDir(src, dst) {
2208
+ atomicMirror(src, dst, { recursive: true, force: true });
2202
2209
  }
2203
2210
  function copyDirJsonlOnly(src, dst) {
2204
- rmSync6(dst, { recursive: true, force: true });
2205
- cpSync4(src, dst, {
2211
+ atomicMirror(src, dst, {
2206
2212
  recursive: true,
2207
2213
  force: true,
2208
2214
  filter: (srcPath) => {
2209
2215
  const rel = relative3(src, srcPath);
2210
2216
  if (rel === "") return true;
2211
- if (rel.split(sep2).length > 1) return true;
2217
+ if (rel.split(sep3).length > 1) return true;
2212
2218
  if (statSync4(srcPath).isDirectory()) return true;
2213
2219
  if (srcPath.endsWith(".jsonl")) return true;
2214
2220
  item(`skip ${rel}: extension not in allowlist`);
@@ -2234,7 +2240,7 @@ function remapPull(ts, opts = {}) {
2234
2240
  emitPreview(opts.onPreview, { kind: "note", text }, text);
2235
2241
  return { unmapped: 0, pulled, wouldPull };
2236
2242
  }
2237
- const map = readJson(mapPath);
2243
+ const map = readPathMap(mapPath);
2238
2244
  const localProjects = join19(claude, "projects");
2239
2245
  if (!dryRun) mkdirSync3(localProjects, { recursive: true });
2240
2246
  for (const [logical, hosts] of Object.entries(map.projects)) {
@@ -2298,13 +2304,14 @@ function remapPush(ts, opts = {}) {
2298
2304
  log("no path-map.json; skipping session export");
2299
2305
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
2300
2306
  }
2301
- const map = readJson(mapPath);
2307
+ const map = readPathMap(mapPath);
2302
2308
  const localProjects = join19(claude, "projects");
2303
2309
  const repoProjects = join19(repo, "shared", "projects");
2304
2310
  const reverse = buildReverseMap(map);
2305
2311
  if (!existsSync15(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
2306
2312
  if (!dryRun) mkdirSync3(repoProjects, { recursive: true });
2307
2313
  for (const dir of readdirSync6(localProjects)) {
2314
+ if (dir.endsWith(TMP_SUFFIX)) continue;
2308
2315
  const logical = reverse.get(dir);
2309
2316
  if (!logical) {
2310
2317
  unmapped++;
@@ -3042,13 +3049,13 @@ import { createInterface } from "node:readline/promises";
3042
3049
  // src/commands.push.recovery.actions.ts
3043
3050
  init_config();
3044
3051
  import { readFileSync as readFileSync12 } from "node:fs";
3045
- import { isAbsolute, resolve as resolve3, sep as sep4 } from "node:path";
3052
+ import { isAbsolute, resolve as resolve3, sep as sep5 } from "node:path";
3046
3053
 
3047
3054
  // src/commands.push.recovery.redact.ts
3048
3055
  init_config();
3049
3056
  init_config_sharedDirs_guard();
3050
3057
  import { cpSync as cpSync5, existsSync as existsSync24, mkdirSync as mkdirSync6, statSync as statSync7 } from "node:fs";
3051
- import { dirname as dirname6, join as join29, sep as sep3 } from "node:path";
3058
+ import { dirname as dirname6, join as join29, sep as sep4 } from "node:path";
3052
3059
 
3053
3060
  // src/commands.redact.ts
3054
3061
  init_config();
@@ -3059,6 +3066,7 @@ import { dirname as dirname5, join as join28 } from "node:path";
3059
3066
  import { existsSync as existsSync22, lstatSync as lstatSync7, readFileSync as readFileSync10, readdirSync as readdirSync9, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
3060
3067
  import { join as join26 } from "node:path";
3061
3068
  init_utils_fs();
3069
+ init_utils();
3062
3070
  function collectFiles(dir, out) {
3063
3071
  if (!existsSync22(dir)) return;
3064
3072
  const st = lstatSync7(dir);
@@ -3101,12 +3109,67 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
3101
3109
  if (!dryRun && total > 0) {
3102
3110
  for (const { path: filePath, findings } of dirty) {
3103
3111
  backupBeforeWrite(filePath, ts);
3104
- writeFileSync3(filePath, applyRedactions(readFileSync10(filePath, "utf8"), findings), "utf8");
3112
+ const before = readFileSync10(filePath, "utf8");
3113
+ const after = applyRedactions(before, findings);
3114
+ if (after === before) {
3115
+ log(
3116
+ `warning: no redaction applied to ${filePath}: finding match values were not located in the file. Inspect it manually; the push re-scan still blocks a real leak.`
3117
+ );
3118
+ }
3119
+ writeFileSync3(filePath, after, "utf8");
3105
3120
  }
3106
3121
  }
3107
3122
  return { total, dirty };
3108
3123
  }
3109
3124
 
3125
+ // src/commands.pushed-history.ts
3126
+ init_utils();
3127
+ import { execFileSync as execFileSync8 } from "node:child_process";
3128
+ function pushedRef(repo) {
3129
+ try {
3130
+ const ref = execFileSync8("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
3131
+ cwd: repo,
3132
+ stdio: ["ignore", "pipe", "ignore"]
3133
+ }).toString().trim();
3134
+ return ref.length > 0 ? ref : null;
3135
+ } catch {
3136
+ return null;
3137
+ }
3138
+ }
3139
+ function sessionInPushedHistory(id, repo) {
3140
+ const ref = pushedRef(repo);
3141
+ if (ref === null) return false;
3142
+ try {
3143
+ const out = execFileSync8(
3144
+ "git",
3145
+ [
3146
+ "log",
3147
+ ref,
3148
+ "--oneline",
3149
+ "-1",
3150
+ "--",
3151
+ `shared/projects/*/${id}.jsonl`,
3152
+ `shared/projects/*/${id}/*`
3153
+ ],
3154
+ { cwd: repo, stdio: ["ignore", "pipe", "ignore"] }
3155
+ ).toString().trim();
3156
+ return out.length > 0;
3157
+ } catch {
3158
+ return false;
3159
+ }
3160
+ }
3161
+ function warnIfSessionPushed(id, repo) {
3162
+ if (!sessionInPushedHistory(id, repo)) return;
3163
+ log(
3164
+ `warning: session ${id} is already in pushed history (origin).
3165
+ This command only changes your local copy and the next push; it does NOT
3166
+ remove the secret from commits already on the remote.
3167
+ To fully remediate a real secret: rotate the credential, then rewrite
3168
+ history (e.g. with git filter-repo) and force-push, coordinating with
3169
+ anyone else who has cloned the repo.`
3170
+ );
3171
+ }
3172
+
3110
3173
  // src/commands.redact.ts
3111
3174
  init_push_gitleaks_scan();
3112
3175
  init_utils_fs();
@@ -3308,6 +3371,7 @@ ${lines}`);
3308
3371
  return;
3309
3372
  }
3310
3373
  log(`redacted ${totalCount} finding(s) in ${localPath} (backup: ${ts})`);
3374
+ warnIfSessionPushed(id, repo);
3311
3375
  } catch (err) {
3312
3376
  if (!(err instanceof NomadFatal)) {
3313
3377
  throw err;
@@ -3382,12 +3446,28 @@ function resolveStagedDir(localPath, map, claude, repo) {
3382
3446
  assertSafeLogical(logical);
3383
3447
  const abs = hostMap[HOST];
3384
3448
  if (abs === void 0) continue;
3385
- if (localPath.startsWith(join29(claude, "projects", encodePath(abs)) + sep3)) {
3449
+ if (localPath.startsWith(join29(claude, "projects", encodePath(abs)) + sep4)) {
3386
3450
  return join29(repo, "shared", "projects", logical);
3387
3451
  }
3388
3452
  }
3389
3453
  return null;
3390
3454
  }
3455
+ function preflightRedactable(f, map, nowMs) {
3456
+ const sid = sessionIdFromFinding(f);
3457
+ if (sid === null) return "a finding has no resolvable session id (not a session transcript)";
3458
+ const localPath = resolveLiveTranscript(sid);
3459
+ if (localPath === null) return `session ${sid}: local transcript not found`;
3460
+ const sessionDir = join29(dirname6(localPath), sid);
3461
+ const subtreeFiles = listSubtreeFiles(sessionDir);
3462
+ const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
3463
+ if (isRecentlyModified(subtreeMtime, nowMs())) {
3464
+ return `session ${sid}: looks active (modified within the last 5 minutes)`;
3465
+ }
3466
+ if (resolveStagedDir(localPath, map, claudeHome(), repoHome()) === null) {
3467
+ return `session ${sid}: not mapped to a staged copy`;
3468
+ }
3469
+ return null;
3470
+ }
3391
3471
  function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3392
3472
  const refuse = (msg) => {
3393
3473
  log(msg);
@@ -3491,7 +3571,7 @@ function makeDefaultReadLine(repo) {
3491
3571
  try {
3492
3572
  const repoRoot = resolve3(repo);
3493
3573
  const target = resolve3(repoRoot, file);
3494
- if (isAbsolute(file) || target !== repoRoot && !target.startsWith(repoRoot + sep4)) {
3574
+ if (isAbsolute(file) || target !== repoRoot && !target.startsWith(repoRoot + sep5)) {
3495
3575
  return null;
3496
3576
  }
3497
3577
  const content = readFileSync12(target, "utf8");
@@ -3558,6 +3638,22 @@ function dispatchActions(findings, actions, opts) {
3558
3638
  }
3559
3639
  }
3560
3640
  function redactAllFindings(findings, ts, map, nowMs, scan = scanFile) {
3641
+ const refusals = [];
3642
+ const preflighted = /* @__PURE__ */ new Set();
3643
+ for (const f of findings) {
3644
+ const dedupeKey = sessionIdFromFinding(f) ?? findingKey(f);
3645
+ if (preflighted.has(dedupeKey)) continue;
3646
+ preflighted.add(dedupeKey);
3647
+ const reason = preflightRedactable(f, map, nowMs);
3648
+ if (reason !== null) refusals.push(reason);
3649
+ }
3650
+ if (refusals.length > 0) {
3651
+ throw new NomadFatal(
3652
+ `--redact-all cannot redact every finding, so no changes were made:
3653
+ ` + refusals.map((r) => ` - ${r}`).join("\n") + `
3654
+ Re-run without --redact-all to triage these interactively (Drop session / Skip), or end any active session and retry.`
3655
+ );
3656
+ }
3561
3657
  const redactedSids = /* @__PURE__ */ new Set();
3562
3658
  for (const f of findings) {
3563
3659
  const sid = sessionIdFromFinding(f);
@@ -3764,7 +3860,7 @@ function withSpinner(label, fn, deps) {
3764
3860
 
3765
3861
  // src/commands.doctor.gitleaks-version.ts
3766
3862
  init_color();
3767
- import { execFileSync as execFileSync8 } from "node:child_process";
3863
+ import { execFileSync as execFileSync9 } from "node:child_process";
3768
3864
  import { existsSync as existsSync26 } from "node:fs";
3769
3865
  import { join as join32 } from "node:path";
3770
3866
  init_config();
@@ -3787,7 +3883,7 @@ function readGitleaksVersion(run, tomlExists) {
3787
3883
  return null;
3788
3884
  }
3789
3885
  }
3790
- function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists = existsSync26) {
3886
+ function reportGitleaksVersionCheck(section2, run = execFileSync9, tomlExists = existsSync26) {
3791
3887
  const raw = readGitleaksVersion(run, tomlExists);
3792
3888
  if (raw === null) return;
3793
3889
  const local = majorMinorOf(raw);
@@ -3807,7 +3903,7 @@ function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists =
3807
3903
 
3808
3904
  // src/commands.doctor.checks.deps.ts
3809
3905
  init_color();
3810
- import { execFileSync as execFileSync9 } from "node:child_process";
3906
+ import { execFileSync as execFileSync10 } from "node:child_process";
3811
3907
  var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
3812
3908
  var PROBE_TIMEOUT_MS = 3e3;
3813
3909
  var FETCHER_BASE = "HTTP fetcher";
@@ -3844,7 +3940,7 @@ function reportFetcherRow(section2, run) {
3844
3940
  );
3845
3941
  }
3846
3942
  }
3847
- function reportOptionalDeps(section2, run = execFileSync9) {
3943
+ function reportOptionalDeps(section2, run = execFileSync10) {
3848
3944
  const gh = probeOptionalDep("gh", run);
3849
3945
  if (gh.status === "present") {
3850
3946
  addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
@@ -3859,11 +3955,11 @@ function reportOptionalDeps(section2, run = execFileSync9) {
3859
3955
 
3860
3956
  // src/commands.doctor.actions-drift.ts
3861
3957
  init_color();
3862
- import { execFileSync as execFileSync11 } from "node:child_process";
3958
+ import { execFileSync as execFileSync12 } from "node:child_process";
3863
3959
  init_config();
3864
3960
 
3865
3961
  // src/gh-actions.ts
3866
- import { execFileSync as execFileSync10 } from "node:child_process";
3962
+ import { execFileSync as execFileSync11 } from "node:child_process";
3867
3963
  var GH_TIMEOUT_MS = 5e3;
3868
3964
  function parseGitHubRemote(remoteUrl) {
3869
3965
  const normalized = remoteUrl.trim().replace(/\/$/, "");
@@ -3871,7 +3967,7 @@ function parseGitHubRemote(remoteUrl) {
3871
3967
  if (m === null) return null;
3872
3968
  return { owner: m[1], repo: m[2] };
3873
3969
  }
3874
- function ghAuthStatus(run = execFileSync10) {
3970
+ function ghAuthStatus(run = execFileSync11) {
3875
3971
  try {
3876
3972
  run("gh", ["auth", "status"], {
3877
3973
  stdio: ["ignore", "ignore", "ignore"],
@@ -3885,7 +3981,7 @@ function ghAuthStatus(run = execFileSync10) {
3885
3981
  return "gh-probe-error";
3886
3982
  }
3887
3983
  }
3888
- function isRepoPrivate(ref, run = execFileSync10) {
3984
+ function isRepoPrivate(ref, run = execFileSync11) {
3889
3985
  const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
3890
3986
  stdio: ["ignore", "pipe", "ignore"],
3891
3987
  timeout: GH_TIMEOUT_MS
@@ -3893,7 +3989,7 @@ function isRepoPrivate(ref, run = execFileSync10) {
3893
3989
  const parsed = JSON.parse(out);
3894
3990
  return parsed.isPrivate === true;
3895
3991
  }
3896
- function isActionsEnabled(ref, run = execFileSync10) {
3992
+ function isActionsEnabled(ref, run = execFileSync11) {
3897
3993
  const out = run(
3898
3994
  "gh",
3899
3995
  ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
@@ -3901,7 +3997,7 @@ function isActionsEnabled(ref, run = execFileSync10) {
3901
3997
  ).toString().trim();
3902
3998
  return out === "true";
3903
3999
  }
3904
- function disableActions(ref, run = execFileSync10) {
4000
+ function disableActions(ref, run = execFileSync11) {
3905
4001
  run(
3906
4002
  "gh",
3907
4003
  [
@@ -3915,7 +4011,7 @@ function disableActions(ref, run = execFileSync10) {
3915
4011
  { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
3916
4012
  );
3917
4013
  }
3918
- function readOriginRemote(cwd, run = execFileSync10) {
4014
+ function readOriginRemote(cwd, run = execFileSync11) {
3919
4015
  return run("git", ["remote", "get-url", "origin"], {
3920
4016
  cwd,
3921
4017
  stdio: ["ignore", "pipe", "ignore"]
@@ -3923,7 +4019,7 @@ function readOriginRemote(cwd, run = execFileSync10) {
3923
4019
  }
3924
4020
 
3925
4021
  // src/commands.doctor.actions-drift.ts
3926
- function reportActionsDrift(section2, run = execFileSync11) {
4022
+ function reportActionsDrift(section2, run = execFileSync12) {
3927
4023
  let remote;
3928
4024
  try {
3929
4025
  remote = readOriginRemote(repoHome(), run);
@@ -4052,15 +4148,15 @@ function cmdDoctor(opts = {}) {
4052
4148
 
4053
4149
  // src/commands.drop-session.ts
4054
4150
  init_config();
4055
- import { execFileSync as execFileSync13 } from "node:child_process";
4151
+ import { execFileSync as execFileSync14 } from "node:child_process";
4056
4152
  import { existsSync as existsSync29, readdirSync as readdirSync10, statSync as statSync8 } from "node:fs";
4057
4153
  import { join as join35, relative as relative4 } from "node:path";
4058
4154
 
4059
4155
  // src/commands.drop-session.git.ts
4060
- import { execFileSync as execFileSync12 } from "node:child_process";
4156
+ import { execFileSync as execFileSync13 } from "node:child_process";
4061
4157
  function expandStagedDir(dirRel, repo) {
4062
4158
  try {
4063
- const out = execFileSync12("git", ["ls-files", "-z", "--", dirRel], {
4159
+ const out = execFileSync13("git", ["ls-files", "-z", "--", dirRel], {
4064
4160
  cwd: repo,
4065
4161
  stdio: ["ignore", "pipe", "pipe"]
4066
4162
  });
@@ -4071,7 +4167,7 @@ function expandStagedDir(dirRel, repo) {
4071
4167
  }
4072
4168
  function isTrackedInHead(rel, repo) {
4073
4169
  try {
4074
- execFileSync12("git", ["cat-file", "-e", `HEAD:${rel}`], {
4170
+ execFileSync13("git", ["cat-file", "-e", `HEAD:${rel}`], {
4075
4171
  cwd: repo,
4076
4172
  stdio: ["ignore", "pipe", "pipe"]
4077
4173
  });
@@ -4082,7 +4178,7 @@ function isTrackedInHead(rel, repo) {
4082
4178
  }
4083
4179
  function isInIndex(rel, repo) {
4084
4180
  try {
4085
- const out = execFileSync12("git", ["ls-files", "--", rel], {
4181
+ const out = execFileSync13("git", ["ls-files", "--", rel], {
4086
4182
  cwd: repo,
4087
4183
  stdio: ["ignore", "pipe", "pipe"]
4088
4184
  });
@@ -4153,6 +4249,7 @@ function cmdDropSession(id) {
4153
4249
  }
4154
4250
  for (const rel of matches) unstageOne(rel, repo);
4155
4251
  reportScrubHint(id, matches);
4252
+ warnIfSessionPushed(id, repo);
4156
4253
  } catch (err) {
4157
4254
  if (!(err instanceof NomadFatal)) {
4158
4255
  throw err;
@@ -4187,12 +4284,12 @@ function unstageOne(rel, repo) {
4187
4284
  }
4188
4285
  try {
4189
4286
  if (isTrackedInHead(rel, repo)) {
4190
- execFileSync13("git", ["restore", "--staged", "--worktree", "--", rel], {
4287
+ execFileSync14("git", ["restore", "--staged", "--worktree", "--", rel], {
4191
4288
  cwd: repo,
4192
4289
  stdio: ["ignore", "pipe", "pipe"]
4193
4290
  });
4194
4291
  } else {
4195
- execFileSync13("git", ["rm", "--cached", "-f", "--", rel], {
4292
+ execFileSync14("git", ["rm", "--cached", "-f", "--", rel], {
4196
4293
  cwd: repo,
4197
4294
  stdio: ["ignore", "pipe", "pipe"]
4198
4295
  });
@@ -4307,7 +4404,7 @@ import { join as join39 } from "node:path";
4307
4404
 
4308
4405
  // src/extras-sync.diff.ts
4309
4406
  init_utils();
4310
- import { execFileSync as execFileSync14 } from "node:child_process";
4407
+ import { execFileSync as execFileSync15 } from "node:child_process";
4311
4408
  function labelDiffLine(line) {
4312
4409
  const tab = line.indexOf(" ");
4313
4410
  if (tab === -1) return line;
@@ -4322,7 +4419,7 @@ function parseDiffOutput(stdout) {
4322
4419
  }
4323
4420
  function listDivergingFiles(a, b) {
4324
4421
  try {
4325
- const stdout = execFileSync14("git", ["diff", "--no-index", "--name-status", a, b], {
4422
+ const stdout = execFileSync15("git", ["diff", "--no-index", "--name-status", a, b], {
4326
4423
  stdio: ["ignore", "pipe", "pipe"]
4327
4424
  }).toString();
4328
4425
  return parseDiffOutput(stdout);
@@ -4502,10 +4599,10 @@ init_utils_json();
4502
4599
  // src/extras-sync.remap.ts
4503
4600
  init_config();
4504
4601
  import { existsSync as existsSync31, mkdirSync as mkdirSync7, readdirSync as readdirSync12, realpathSync as realpathSync4, rmSync as rmSync11 } from "node:fs";
4505
- import { dirname as dirname7, join as join38, sep as sep6 } from "node:path";
4602
+ import { dirname as dirname7, join as join38, sep as sep7 } from "node:path";
4506
4603
 
4507
4604
  // src/extras-sync.planning-diff.ts
4508
- import { join as join37, normalize as normalize2, sep as sep5 } from "node:path";
4605
+ import { join as join37, normalize as normalize2, sep as sep6 } from "node:path";
4509
4606
  init_utils();
4510
4607
  function processRecord(fields, i, changed, deleted) {
4511
4608
  const status = fields[i];
@@ -4558,7 +4655,7 @@ function planningDeleteTargets(opts) {
4558
4655
  const logicalPrefix = "shared/extras/" + logical + "/";
4559
4656
  const prefix = logicalPrefix + ".planning/";
4560
4657
  const planningRoot = join37(localRoot, ".planning");
4561
- const planningRootBoundary = planningRoot + sep5;
4658
+ const planningRootBoundary = planningRoot + sep6;
4562
4659
  const targets = [];
4563
4660
  for (const repoPath of deleted) {
4564
4661
  if (!repoPath.startsWith(prefix)) {
@@ -4600,7 +4697,7 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
4600
4697
  }
4601
4698
  function pruneEmptyAncestors(target, planningRoot) {
4602
4699
  let dir = dirname7(target);
4603
- while (dir !== planningRoot && dir.startsWith(planningRoot + sep6)) {
4700
+ while (dir !== planningRoot && dir.startsWith(planningRoot + sep7)) {
4604
4701
  try {
4605
4702
  if (readdirSync12(dir).length > 0) break;
4606
4703
  rmSync11(dir, { recursive: true, force: true });
@@ -4618,7 +4715,7 @@ function tryRealpath(dir) {
4618
4715
  }
4619
4716
  }
4620
4717
  function isInsidePlanningRoot(parentReal, rootReal) {
4621
- return parentReal === rootReal || parentReal.startsWith(rootReal + sep6);
4718
+ return parentReal === rootReal || parentReal.startsWith(rootReal + sep7);
4622
4719
  }
4623
4720
  function deletePlanningTarget(target, planningRoot, repoCounterpart) {
4624
4721
  if (existsSync31(repoCounterpart)) return;
@@ -4660,7 +4757,7 @@ function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
4660
4757
  backupExtrasWrite(join38(t.localRoot, t.dirname), ts, t.localRoot);
4661
4758
  const planningRoot = join38(t.localRoot, ".planning");
4662
4759
  for (const target of targets) {
4663
- const relToLocal = target.slice(t.localRoot.length + sep6.length);
4760
+ const relToLocal = target.slice(t.localRoot.length + sep7.length);
4664
4761
  deletePlanningTarget(target, planningRoot, join38(repoExtras, t.logical, relToLocal));
4665
4762
  }
4666
4763
  }
@@ -5265,9 +5362,9 @@ init_config();
5265
5362
  init_commands_pull_wedge();
5266
5363
  init_utils();
5267
5364
  init_utils_fs();
5268
- import { execFileSync as execFileSync15 } from "node:child_process";
5365
+ import { execFileSync as execFileSync16 } from "node:child_process";
5269
5366
  function gitCapture(args, cwd) {
5270
- return execFileSync15("git", args, {
5367
+ return execFileSync16("git", args, {
5271
5368
  cwd,
5272
5369
  stdio: ["ignore", "pipe", "pipe"],
5273
5370
  maxBuffer: 64 * 1024 * 1024
@@ -5521,6 +5618,19 @@ function isNeverSync(path) {
5521
5618
  }
5522
5619
  return false;
5523
5620
  }
5621
+ var GSD_HOOKS_SUPPORT_FILES = /* @__PURE__ */ new Set(["managed-hooks-registry.cjs", "package.json"]);
5622
+ var GSD_HOOKS_SUPPORT_DIR = "lib";
5623
+ function isGsdDropped(path) {
5624
+ const segments = path.split("/");
5625
+ if (segments[0] !== "shared" || segments.length < 3) return false;
5626
+ const dirName = segments[1];
5627
+ if (!GSD_DROPPED_NAMES.includes(dirName)) return false;
5628
+ const childName = segments[2];
5629
+ if (childName.startsWith(GSD_PREFIX)) return true;
5630
+ if (dirName !== "hooks") return false;
5631
+ if (segments.length === 3) return GSD_HOOKS_SUPPORT_FILES.has(childName);
5632
+ return childName === GSD_HOOKS_SUPPORT_DIR;
5633
+ }
5524
5634
  function parsePorcelainZ2(statusPorcelain) {
5525
5635
  const records = statusPorcelain.split("\0");
5526
5636
  const paths = [];
@@ -5554,6 +5664,7 @@ function enforceAllowList(statusPorcelain, map) {
5554
5664
  for (const path of parsePorcelainZ2(statusPorcelain)) {
5555
5665
  if (isNeverSync(path)) {
5556
5666
  neverSyncHits.push(path);
5667
+ } else if (isGsdDropped(path)) {
5557
5668
  } else if (!isAllowed(path, allowed)) {
5558
5669
  violations.push(path);
5559
5670
  }
@@ -5570,7 +5681,7 @@ function enforceAllowList(statusPorcelain, map) {
5570
5681
 
5571
5682
  // src/push-global-config.ts
5572
5683
  init_config();
5573
- import { execFileSync as execFileSync16 } from "node:child_process";
5684
+ import { execFileSync as execFileSync17 } from "node:child_process";
5574
5685
  var STATUS_LABELS = {
5575
5686
  A: "add",
5576
5687
  M: "modify",
@@ -5610,7 +5721,7 @@ function isInScope(filePath, exactPrefixes, dirPrefixes) {
5610
5721
  }
5611
5722
  function collectGlobalConfigChanges(repoHome2, hostname2, opts) {
5612
5723
  const args = opts.staged ? ["diff", "--cached", "--name-status", "-z"] : ["diff", "HEAD", "--name-status", "-z"];
5613
- const raw = execFileSync16("git", args, {
5724
+ const raw = execFileSync17("git", args, {
5614
5725
  cwd: repoHome2,
5615
5726
  stdio: ["ignore", "pipe", "pipe"]
5616
5727
  }).toString();
@@ -5745,6 +5856,16 @@ function guardGitlinks(repo) {
5745
5856
  }
5746
5857
  async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule, repo) {
5747
5858
  gitOrFatal(["add", "-A"], "git add", repo);
5859
+ const staged = parsePorcelainZ2(gitStatusPorcelainZ(repo));
5860
+ const toDrop = staged.filter((p) => isGsdDropped(p));
5861
+ if (toDrop.length > 0) {
5862
+ gitOrFatal(["restore", "--staged", "--", ...toDrop], "git restore --staged", repo);
5863
+ }
5864
+ if (staged.length === toDrop.length) {
5865
+ log("nothing to commit");
5866
+ renderNoScanTree(st);
5867
+ return;
5868
+ }
5748
5869
  st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: true });
5749
5870
  let verdict = withSpinner("Scanning for secrets", () => scanPushVerdict(repo));
5750
5871
  if (verdict.leak) {
@@ -5829,16 +5950,16 @@ async function cmdPush(opts = {}) {
5829
5950
  }
5830
5951
 
5831
5952
  // src/commands.update.ts
5832
- import { execFileSync as execFileSync17 } from "node:child_process";
5953
+ import { execFileSync as execFileSync18 } from "node:child_process";
5833
5954
  init_utils();
5834
- function readInstalledVersion(run = execFileSync17) {
5955
+ function readInstalledVersion(run = execFileSync18) {
5835
5956
  try {
5836
5957
  return run("nomad", ["--version"], { encoding: "utf8" }).toString().trim() || null;
5837
5958
  } catch {
5838
5959
  return null;
5839
5960
  }
5840
5961
  }
5841
- function cmdUpdate(run = execFileSync17) {
5962
+ function cmdUpdate(run = execFileSync18) {
5842
5963
  console.log("Updating claude-nomad CLI via npm...");
5843
5964
  try {
5844
5965
  run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
@@ -5892,14 +6013,14 @@ import { join as join48 } from "node:path";
5892
6013
 
5893
6014
  // src/init.gh-onboard.ts
5894
6015
  init_config();
5895
- import { execFileSync as execFileSync18 } from "node:child_process";
6016
+ import { execFileSync as execFileSync19 } from "node:child_process";
5896
6017
  init_utils();
5897
6018
  var DEFAULT_REPO_NAME = "claude-nomad-config";
5898
6019
  function isValidRepoName(name) {
5899
6020
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
5900
6021
  }
5901
6022
  var GH_NETWORK_TIMEOUT_MS = 3e4;
5902
- function ensureOriginRepo(repoName, run = execFileSync18) {
6023
+ function ensureOriginRepo(repoName, run = execFileSync19) {
5903
6024
  if (!isValidRepoName(repoName)) {
5904
6025
  die(
5905
6026
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -6112,86 +6233,20 @@ function maybeDisableRepoActions(repoHome2, run) {
6112
6233
  }
6113
6234
  }
6114
6235
 
6115
- // src/nomad.dispatch.ts
6236
+ // src/nomad.dispatch.helpers.ts
6237
+ var REJECT = { ok: false, advance: 0 };
6238
+ function applyBool(seen, set) {
6239
+ if (seen) return REJECT;
6240
+ set();
6241
+ return { ok: true, advance: 1 };
6242
+ }
6116
6243
  function extractFlagValue(argv, i) {
6117
6244
  const val = argv[i + 1];
6118
6245
  if (val === void 0 || val.startsWith("--")) return null;
6119
6246
  return val;
6120
6247
  }
6121
- function applyInitToken(argv, i, st) {
6122
- const token = argv[i];
6123
- if (token === "--snapshot") {
6124
- if (st.sawSnapshot) return { ok: false, advance: 0 };
6125
- st.sawSnapshot = true;
6126
- st.snapshot = true;
6127
- return { ok: true, advance: 1 };
6128
- }
6129
- if (token === "--keep-actions") {
6130
- if (st.sawKeepActions) return { ok: false, advance: 0 };
6131
- st.sawKeepActions = true;
6132
- st.keepActions = true;
6133
- return { ok: true, advance: 1 };
6134
- }
6135
- if (token === "--repo") {
6136
- if (st.sawRepo) return { ok: false, advance: 0 };
6137
- st.sawRepo = true;
6138
- const val = extractFlagValue(argv, i);
6139
- if (val === null) return { ok: false, advance: 0 };
6140
- st.repoName = val;
6141
- return { ok: true, advance: 2 };
6142
- }
6143
- return { ok: false, advance: 0 };
6144
- }
6145
- function parseInitArgs(argv) {
6146
- const st = {
6147
- snapshot: false,
6148
- keepActions: false,
6149
- repoName: void 0,
6150
- sawSnapshot: false,
6151
- sawKeepActions: false,
6152
- sawRepo: false
6153
- };
6154
- let i = 3;
6155
- while (i < argv.length) {
6156
- const { ok: ok2, advance } = applyInitToken(argv, i, st);
6157
- if (!ok2) return null;
6158
- i += advance;
6159
- }
6160
- return { snapshot: st.snapshot, keepActions: st.keepActions, repoName: st.repoName };
6161
- }
6162
- function parseRedactArgs(argv) {
6163
- const id = argv[3];
6164
- if (typeof id !== "string" || !/^\w[\w-]{0,127}$/.test(id)) {
6165
- return null;
6166
- }
6167
- let rule;
6168
- let dryRun = false;
6169
- let sawRule = false;
6170
- let sawDryRun = false;
6171
- let i = 4;
6172
- while (i < argv.length) {
6173
- const token = argv[i];
6174
- if (token === "--dry-run") {
6175
- if (sawDryRun) return null;
6176
- sawDryRun = true;
6177
- dryRun = true;
6178
- i++;
6179
- } else if (token === "--rule") {
6180
- if (sawRule) return null;
6181
- sawRule = true;
6182
- const val = argv[i + 1];
6183
- if (val === void 0 || val.startsWith("--")) return null;
6184
- rule = val;
6185
- i += 2;
6186
- } else {
6187
- return null;
6188
- }
6189
- }
6190
- return { id, rule, dryRun };
6191
- }
6192
6248
 
6193
6249
  // src/nomad.dispatch.clean.ts
6194
- var REJECT = { ok: false, advance: 0 };
6195
6250
  function applyOlderThan(argv, i, st) {
6196
6251
  if (st.olderThan !== void 0) return REJECT;
6197
6252
  const val = extractFlagValue(argv, i);
@@ -6206,11 +6261,6 @@ function applyKeep(argv, i, st) {
6206
6261
  st.keep = Number(val);
6207
6262
  return { ok: true, advance: 2 };
6208
6263
  }
6209
- function applyBool(seen, set) {
6210
- if (seen) return REJECT;
6211
- set();
6212
- return { ok: true, advance: 1 };
6213
- }
6214
6264
  function applyCleanToken(argv, i, st) {
6215
6265
  switch (argv[i]) {
6216
6266
  case "--backups":
@@ -6260,6 +6310,79 @@ function parseEjectArgs(argv) {
6260
6310
  return { dryRun };
6261
6311
  }
6262
6312
 
6313
+ // src/nomad.dispatch.ts
6314
+ function applyInitToken(argv, i, st) {
6315
+ const token = argv[i];
6316
+ if (token === "--snapshot") {
6317
+ if (st.sawSnapshot) return REJECT;
6318
+ st.sawSnapshot = true;
6319
+ st.snapshot = true;
6320
+ return { ok: true, advance: 1 };
6321
+ }
6322
+ if (token === "--keep-actions") {
6323
+ if (st.sawKeepActions) return REJECT;
6324
+ st.sawKeepActions = true;
6325
+ st.keepActions = true;
6326
+ return { ok: true, advance: 1 };
6327
+ }
6328
+ if (token === "--repo") {
6329
+ if (st.sawRepo) return REJECT;
6330
+ st.sawRepo = true;
6331
+ const val = extractFlagValue(argv, i);
6332
+ if (val === null) return REJECT;
6333
+ st.repoName = val;
6334
+ return { ok: true, advance: 2 };
6335
+ }
6336
+ return REJECT;
6337
+ }
6338
+ function parseInitArgs(argv) {
6339
+ const st = {
6340
+ snapshot: false,
6341
+ keepActions: false,
6342
+ repoName: void 0,
6343
+ sawSnapshot: false,
6344
+ sawKeepActions: false,
6345
+ sawRepo: false
6346
+ };
6347
+ let i = 3;
6348
+ while (i < argv.length) {
6349
+ const { ok: ok2, advance } = applyInitToken(argv, i, st);
6350
+ if (!ok2) return null;
6351
+ i += advance;
6352
+ }
6353
+ return { snapshot: st.snapshot, keepActions: st.keepActions, repoName: st.repoName };
6354
+ }
6355
+ function parseRedactArgs(argv) {
6356
+ const id = argv[3];
6357
+ if (typeof id !== "string" || !/^\w[\w-]{0,127}$/.test(id)) {
6358
+ return null;
6359
+ }
6360
+ let rule;
6361
+ let dryRun = false;
6362
+ let sawRule = false;
6363
+ let sawDryRun = false;
6364
+ let i = 4;
6365
+ while (i < argv.length) {
6366
+ const token = argv[i];
6367
+ if (token === "--dry-run") {
6368
+ if (sawDryRun) return null;
6369
+ sawDryRun = true;
6370
+ dryRun = true;
6371
+ i++;
6372
+ } else if (token === "--rule") {
6373
+ if (sawRule) return null;
6374
+ sawRule = true;
6375
+ const val = extractFlagValue(argv, i);
6376
+ if (val === null) return null;
6377
+ rule = val;
6378
+ i += 2;
6379
+ } else {
6380
+ return null;
6381
+ }
6382
+ }
6383
+ return { id, rule, dryRun };
6384
+ }
6385
+
6263
6386
  // src/nomad.dispatch.allow.ts
6264
6387
  function parseAllowArgs(argv) {
6265
6388
  const positionals = argv.slice(3);
@@ -6293,32 +6416,26 @@ function parsePullArgs(argv) {
6293
6416
  }
6294
6417
 
6295
6418
  // src/nomad.dispatch.push.ts
6296
- var REJECT2 = { ok: false, advance: 0 };
6297
- function applyBool2(seen, set) {
6298
- if (seen) return REJECT2;
6299
- set();
6300
- return { ok: true, advance: 1 };
6301
- }
6302
6419
  var RULE_ID_RE = /^\w[\w-]*$/;
6303
6420
  function applyAllow2(argv, i, st) {
6304
- if (st.allowRule !== void 0) return REJECT2;
6421
+ if (st.allowRule !== void 0) return REJECT;
6305
6422
  const val = extractFlagValue(argv, i);
6306
- if (val === null || !RULE_ID_RE.test(val)) return REJECT2;
6423
+ if (val === null || !RULE_ID_RE.test(val)) return REJECT;
6307
6424
  st.allowRule = val;
6308
6425
  return { ok: true, advance: 2 };
6309
6426
  }
6310
6427
  function applyPushToken(argv, i, st) {
6311
6428
  switch (argv[i]) {
6312
6429
  case "--dry-run":
6313
- return applyBool2(st.dryRun, () => st.dryRun = true);
6430
+ return applyBool(st.dryRun, () => st.dryRun = true);
6314
6431
  case "--redact-all":
6315
- return applyBool2(st.redactAll, () => st.redactAll = true);
6432
+ return applyBool(st.redactAll, () => st.redactAll = true);
6316
6433
  case "--allow-all":
6317
- return applyBool2(st.allowAll, () => st.allowAll = true);
6434
+ return applyBool(st.allowAll, () => st.allowAll = true);
6318
6435
  case "--allow":
6319
6436
  return applyAllow2(argv, i, st);
6320
6437
  default:
6321
- return REJECT2;
6438
+ return REJECT;
6322
6439
  }
6323
6440
  }
6324
6441
  function parsePushArgs(argv) {
@@ -6350,7 +6467,7 @@ function parsePushArgs(argv) {
6350
6467
  // package.json
6351
6468
  var package_default = {
6352
6469
  name: "claude-nomad",
6353
- version: "0.50.1",
6470
+ version: "0.50.3",
6354
6471
  type: "module",
6355
6472
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
6356
6473
  keywords: [
@@ -6579,7 +6696,7 @@ function resumeCmd(sessionId) {
6579
6696
  process.exit(1);
6580
6697
  }
6581
6698
  const map = readJson(mapPath);
6582
- const schemaError = validatePathMap(map);
6699
+ const schemaError = validatePathMapShape(map);
6583
6700
  if (schemaError !== null) {
6584
6701
  fail(schemaError);
6585
6702
  process.exit(1);
@@ -6614,26 +6731,6 @@ function extractRecordedCwd(jsonlPath) {
6614
6731
  }
6615
6732
  return null;
6616
6733
  }
6617
- function validatePathMap(raw) {
6618
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
6619
- return "path-map.json invalid schema: top-level value must be an object";
6620
- }
6621
- const projects = raw.projects;
6622
- if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
6623
- return 'path-map.json invalid schema: "projects" must be an object';
6624
- }
6625
- for (const [name, hosts] of Object.entries(projects)) {
6626
- if (hosts === null || typeof hosts !== "object" || Array.isArray(hosts)) {
6627
- return `path-map.json invalid schema: project "${name}" hosts must be an object`;
6628
- }
6629
- for (const [host, value] of Object.entries(hosts)) {
6630
- if (typeof value !== "string") {
6631
- return `path-map.json invalid schema: project "${name}" host "${host}" path must be a string`;
6632
- }
6633
- }
6634
- }
6635
- return null;
6636
- }
6637
6734
  function lookupLocalPath(map, recordedCwd) {
6638
6735
  for (const [logical, hosts] of Object.entries(map.projects)) {
6639
6736
  const isUnderMappedPath = Object.values(hosts).some(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-nomad",
3
- "version": "0.50.1",
3
+ "version": "0.50.3",
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": [