claude-nomad 0.50.2 → 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,35 @@
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
+
3
33
  ## [0.50.2](https://github.com/funkadelic/claude-nomad/compare/v0.50.1...v0.50.2) (2026-06-15)
4
34
 
5
35
 
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
@@ -451,16 +451,41 @@ function readJson(path) {
451
451
  return data;
452
452
  }
453
453
  function readPathMap(mapPath) {
454
+ let parsed;
454
455
  try {
455
- return readJson(mapPath);
456
+ parsed = readJson(mapPath);
456
457
  } catch (err) {
457
458
  const verb = err instanceof SyntaxError ? "parse" : "read";
458
459
  throw new NomadFatal(`could not ${verb} path-map.json: ${err.message}`);
459
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;
460
484
  }
461
485
  function deepMerge(target, source) {
462
486
  const out = { ...target };
463
487
  for (const [key, value] of Object.entries(source)) {
488
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
464
489
  const existing = out[key];
465
490
  const bothObjects = value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing);
466
491
  out[key] = bothObjects ? deepMerge(existing, value) : value;
@@ -502,7 +527,7 @@ import {
502
527
  symlinkSync,
503
528
  writeFileSync
504
529
  } from "node:fs";
505
- import { dirname, join as join2, relative } from "node:path";
530
+ import { dirname, join as join2, relative, sep } from "node:path";
506
531
  function writeJsonAtomic(path, data) {
507
532
  const mode = existsSync(path) ? statSync(path).mode & 511 : 384;
508
533
  const tmp = `${path}.tmp.${process.pid}`;
@@ -542,33 +567,22 @@ function ensureSymlink(linkPath, target) {
542
567
  symlinkSync(target, linkPath);
543
568
  log(`linked ${linkPath} -> ${target}`);
544
569
  }
545
- function backupBeforeWrite(absPath, ts) {
570
+ function backupUnder(absPath, anchor, destRoot) {
546
571
  if (!existsSync(absPath)) return;
547
- const claude = claudeHome();
548
- const rel = relative(claude, absPath);
549
- if (rel.startsWith("..") || rel === "") return;
550
- const backupRoot = join2(backupBase(), ts);
551
- 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);
552
575
  mkdirSync(dirname(dst), { recursive: true });
553
576
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
554
577
  }
578
+ function backupBeforeWrite(absPath, ts) {
579
+ backupUnder(absPath, claudeHome(), join2(backupBase(), ts));
580
+ }
555
581
  function backupRepoWrite(absPath, ts, repoHome2) {
556
- if (!existsSync(absPath)) return;
557
- const rel = relative(repoHome2, absPath);
558
- if (rel.startsWith("..") || rel === "") return;
559
- const backupRoot = join2(backupBase(), ts, "repo");
560
- const dst = join2(backupRoot, rel);
561
- mkdirSync(dirname(dst), { recursive: true });
562
- cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
582
+ backupUnder(absPath, repoHome2, join2(backupBase(), ts, "repo"));
563
583
  }
564
584
  function backupExtrasWrite(absPath, ts, projectRoot) {
565
- if (!existsSync(absPath)) return;
566
- const rel = relative(projectRoot, absPath);
567
- if (rel.startsWith("..") || rel === "") return;
568
- const backupRoot = join2(backupBase(), ts, "extras");
569
- const dst = join2(backupRoot, encodePath(projectRoot), rel);
570
- mkdirSync(dirname(dst), { recursive: true });
571
- cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
585
+ backupUnder(absPath, projectRoot, join2(backupBase(), ts, "extras", encodePath(projectRoot)));
572
586
  }
573
587
  var init_utils_fs = __esm({
574
588
  "src/utils.fs.ts"() {
@@ -1319,7 +1333,7 @@ init_config();
1319
1333
  init_utils();
1320
1334
  init_utils_json();
1321
1335
  import { cpSync as cpSync3, existsSync as existsSync5, lstatSync as lstatSync4, realpathSync, renameSync as renameSync2, rmSync as rmSync3 } from "node:fs";
1322
- import { join as join6, sep } from "node:path";
1336
+ import { join as join6, sep as sep2 } from "node:path";
1323
1337
  function ejectChecklist() {
1324
1338
  return [
1325
1339
  "Manual steps remaining to finish leaving claude-nomad on this host:",
@@ -1361,7 +1375,7 @@ function resolveSharedRoot(repoHome2) {
1361
1375
  }
1362
1376
  }
1363
1377
  function isManagedTarget(target, sharedRoot) {
1364
- return target.startsWith(sharedRoot + sep);
1378
+ return target.startsWith(sharedRoot + sep2);
1365
1379
  }
1366
1380
  function materializeOne(name, linkPath, sharedRoot) {
1367
1381
  const target = realpathSync(linkPath);
@@ -1841,35 +1855,12 @@ function reportPathMap(section2) {
1841
1855
  }
1842
1856
  const map = readJsonSafe(mapPath, mapPath, section2);
1843
1857
  if (map === null) return;
1844
- const projects = map.projects;
1845
- if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
1846
- addItem(
1847
- section2,
1848
- `${red(failGlyph)} path-map.json invalid schema: "projects" must be an object`
1849
- );
1858
+ const shapeError = validatePathMapShape(map);
1859
+ if (shapeError !== null) {
1860
+ addItem(section2, `${red(failGlyph)} ${shapeError}`);
1850
1861
  process.exitCode = 1;
1851
1862
  return;
1852
1863
  }
1853
- for (const [name, hosts] of Object.entries(projects)) {
1854
- if (hosts === null || typeof hosts !== "object" || Array.isArray(hosts)) {
1855
- addItem(
1856
- section2,
1857
- `${red(failGlyph)} path-map.json invalid schema: project "${name}" hosts must be an object`
1858
- );
1859
- process.exitCode = 1;
1860
- return;
1861
- }
1862
- for (const [hostName, mappedPath] of Object.entries(hosts)) {
1863
- if (typeof mappedPath !== "string") {
1864
- addItem(
1865
- section2,
1866
- `${red(failGlyph)} path-map.json invalid schema: project "${name}" host "${hostName}" path must be a string`
1867
- );
1868
- process.exitCode = 1;
1869
- return;
1870
- }
1871
- }
1872
- }
1873
1864
  reportMappedProjects(section2, map);
1874
1865
  reportUnmappedProjects(section2, map);
1875
1866
  reportPathCollisions(section2, map);
@@ -1883,7 +1874,7 @@ function reportNeverSync(section2) {
1883
1874
  );
1884
1875
  }
1885
1876
 
1886
- // src/commands.doctor.checks.repository.ts
1877
+ // src/commands.doctor.checks.git-state.ts
1887
1878
  init_color();
1888
1879
  init_config();
1889
1880
  import { execFileSync as execFileSync4 } from "node:child_process";
@@ -2203,21 +2194,27 @@ init_config();
2203
2194
  init_utils();
2204
2195
  init_utils_fs();
2205
2196
  init_utils_json();
2206
- import { cpSync as cpSync4, existsSync as existsSync15, mkdirSync as mkdirSync3, readdirSync as readdirSync6, rmSync as rmSync6, statSync as statSync4 } from "node:fs";
2207
- import { join as join19, relative as relative3, sep as sep2 } from "node:path";
2208
- 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);
2209
2204
  rmSync6(dst, { recursive: true, force: true });
2210
- 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 });
2211
2209
  }
2212
2210
  function copyDirJsonlOnly(src, dst) {
2213
- rmSync6(dst, { recursive: true, force: true });
2214
- cpSync4(src, dst, {
2211
+ atomicMirror(src, dst, {
2215
2212
  recursive: true,
2216
2213
  force: true,
2217
2214
  filter: (srcPath) => {
2218
2215
  const rel = relative3(src, srcPath);
2219
2216
  if (rel === "") return true;
2220
- if (rel.split(sep2).length > 1) return true;
2217
+ if (rel.split(sep3).length > 1) return true;
2221
2218
  if (statSync4(srcPath).isDirectory()) return true;
2222
2219
  if (srcPath.endsWith(".jsonl")) return true;
2223
2220
  item(`skip ${rel}: extension not in allowlist`);
@@ -2243,7 +2240,7 @@ function remapPull(ts, opts = {}) {
2243
2240
  emitPreview(opts.onPreview, { kind: "note", text }, text);
2244
2241
  return { unmapped: 0, pulled, wouldPull };
2245
2242
  }
2246
- const map = readJson(mapPath);
2243
+ const map = readPathMap(mapPath);
2247
2244
  const localProjects = join19(claude, "projects");
2248
2245
  if (!dryRun) mkdirSync3(localProjects, { recursive: true });
2249
2246
  for (const [logical, hosts] of Object.entries(map.projects)) {
@@ -2307,13 +2304,14 @@ function remapPush(ts, opts = {}) {
2307
2304
  log("no path-map.json; skipping session export");
2308
2305
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
2309
2306
  }
2310
- const map = readJson(mapPath);
2307
+ const map = readPathMap(mapPath);
2311
2308
  const localProjects = join19(claude, "projects");
2312
2309
  const repoProjects = join19(repo, "shared", "projects");
2313
2310
  const reverse = buildReverseMap(map);
2314
2311
  if (!existsSync15(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
2315
2312
  if (!dryRun) mkdirSync3(repoProjects, { recursive: true });
2316
2313
  for (const dir of readdirSync6(localProjects)) {
2314
+ if (dir.endsWith(TMP_SUFFIX)) continue;
2317
2315
  const logical = reverse.get(dir);
2318
2316
  if (!logical) {
2319
2317
  unmapped++;
@@ -3051,13 +3049,13 @@ import { createInterface } from "node:readline/promises";
3051
3049
  // src/commands.push.recovery.actions.ts
3052
3050
  init_config();
3053
3051
  import { readFileSync as readFileSync12 } from "node:fs";
3054
- import { isAbsolute, resolve as resolve3, sep as sep4 } from "node:path";
3052
+ import { isAbsolute, resolve as resolve3, sep as sep5 } from "node:path";
3055
3053
 
3056
3054
  // src/commands.push.recovery.redact.ts
3057
3055
  init_config();
3058
3056
  init_config_sharedDirs_guard();
3059
3057
  import { cpSync as cpSync5, existsSync as existsSync24, mkdirSync as mkdirSync6, statSync as statSync7 } from "node:fs";
3060
- 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";
3061
3059
 
3062
3060
  // src/commands.redact.ts
3063
3061
  init_config();
@@ -3068,6 +3066,7 @@ import { dirname as dirname5, join as join28 } from "node:path";
3068
3066
  import { existsSync as existsSync22, lstatSync as lstatSync7, readFileSync as readFileSync10, readdirSync as readdirSync9, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
3069
3067
  import { join as join26 } from "node:path";
3070
3068
  init_utils_fs();
3069
+ init_utils();
3071
3070
  function collectFiles(dir, out) {
3072
3071
  if (!existsSync22(dir)) return;
3073
3072
  const st = lstatSync7(dir);
@@ -3110,12 +3109,67 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
3110
3109
  if (!dryRun && total > 0) {
3111
3110
  for (const { path: filePath, findings } of dirty) {
3112
3111
  backupBeforeWrite(filePath, ts);
3113
- 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");
3114
3120
  }
3115
3121
  }
3116
3122
  return { total, dirty };
3117
3123
  }
3118
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
+
3119
3173
  // src/commands.redact.ts
3120
3174
  init_push_gitleaks_scan();
3121
3175
  init_utils_fs();
@@ -3317,6 +3371,7 @@ ${lines}`);
3317
3371
  return;
3318
3372
  }
3319
3373
  log(`redacted ${totalCount} finding(s) in ${localPath} (backup: ${ts})`);
3374
+ warnIfSessionPushed(id, repo);
3320
3375
  } catch (err) {
3321
3376
  if (!(err instanceof NomadFatal)) {
3322
3377
  throw err;
@@ -3391,12 +3446,28 @@ function resolveStagedDir(localPath, map, claude, repo) {
3391
3446
  assertSafeLogical(logical);
3392
3447
  const abs = hostMap[HOST];
3393
3448
  if (abs === void 0) continue;
3394
- if (localPath.startsWith(join29(claude, "projects", encodePath(abs)) + sep3)) {
3449
+ if (localPath.startsWith(join29(claude, "projects", encodePath(abs)) + sep4)) {
3395
3450
  return join29(repo, "shared", "projects", logical);
3396
3451
  }
3397
3452
  }
3398
3453
  return null;
3399
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
+ }
3400
3471
  function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3401
3472
  const refuse = (msg) => {
3402
3473
  log(msg);
@@ -3500,7 +3571,7 @@ function makeDefaultReadLine(repo) {
3500
3571
  try {
3501
3572
  const repoRoot = resolve3(repo);
3502
3573
  const target = resolve3(repoRoot, file);
3503
- if (isAbsolute(file) || target !== repoRoot && !target.startsWith(repoRoot + sep4)) {
3574
+ if (isAbsolute(file) || target !== repoRoot && !target.startsWith(repoRoot + sep5)) {
3504
3575
  return null;
3505
3576
  }
3506
3577
  const content = readFileSync12(target, "utf8");
@@ -3567,6 +3638,22 @@ function dispatchActions(findings, actions, opts) {
3567
3638
  }
3568
3639
  }
3569
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
+ }
3570
3657
  const redactedSids = /* @__PURE__ */ new Set();
3571
3658
  for (const f of findings) {
3572
3659
  const sid = sessionIdFromFinding(f);
@@ -3773,7 +3860,7 @@ function withSpinner(label, fn, deps) {
3773
3860
 
3774
3861
  // src/commands.doctor.gitleaks-version.ts
3775
3862
  init_color();
3776
- import { execFileSync as execFileSync8 } from "node:child_process";
3863
+ import { execFileSync as execFileSync9 } from "node:child_process";
3777
3864
  import { existsSync as existsSync26 } from "node:fs";
3778
3865
  import { join as join32 } from "node:path";
3779
3866
  init_config();
@@ -3796,7 +3883,7 @@ function readGitleaksVersion(run, tomlExists) {
3796
3883
  return null;
3797
3884
  }
3798
3885
  }
3799
- function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists = existsSync26) {
3886
+ function reportGitleaksVersionCheck(section2, run = execFileSync9, tomlExists = existsSync26) {
3800
3887
  const raw = readGitleaksVersion(run, tomlExists);
3801
3888
  if (raw === null) return;
3802
3889
  const local = majorMinorOf(raw);
@@ -3816,7 +3903,7 @@ function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists =
3816
3903
 
3817
3904
  // src/commands.doctor.checks.deps.ts
3818
3905
  init_color();
3819
- import { execFileSync as execFileSync9 } from "node:child_process";
3906
+ import { execFileSync as execFileSync10 } from "node:child_process";
3820
3907
  var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
3821
3908
  var PROBE_TIMEOUT_MS = 3e3;
3822
3909
  var FETCHER_BASE = "HTTP fetcher";
@@ -3853,7 +3940,7 @@ function reportFetcherRow(section2, run) {
3853
3940
  );
3854
3941
  }
3855
3942
  }
3856
- function reportOptionalDeps(section2, run = execFileSync9) {
3943
+ function reportOptionalDeps(section2, run = execFileSync10) {
3857
3944
  const gh = probeOptionalDep("gh", run);
3858
3945
  if (gh.status === "present") {
3859
3946
  addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
@@ -3868,11 +3955,11 @@ function reportOptionalDeps(section2, run = execFileSync9) {
3868
3955
 
3869
3956
  // src/commands.doctor.actions-drift.ts
3870
3957
  init_color();
3871
- import { execFileSync as execFileSync11 } from "node:child_process";
3958
+ import { execFileSync as execFileSync12 } from "node:child_process";
3872
3959
  init_config();
3873
3960
 
3874
3961
  // src/gh-actions.ts
3875
- import { execFileSync as execFileSync10 } from "node:child_process";
3962
+ import { execFileSync as execFileSync11 } from "node:child_process";
3876
3963
  var GH_TIMEOUT_MS = 5e3;
3877
3964
  function parseGitHubRemote(remoteUrl) {
3878
3965
  const normalized = remoteUrl.trim().replace(/\/$/, "");
@@ -3880,7 +3967,7 @@ function parseGitHubRemote(remoteUrl) {
3880
3967
  if (m === null) return null;
3881
3968
  return { owner: m[1], repo: m[2] };
3882
3969
  }
3883
- function ghAuthStatus(run = execFileSync10) {
3970
+ function ghAuthStatus(run = execFileSync11) {
3884
3971
  try {
3885
3972
  run("gh", ["auth", "status"], {
3886
3973
  stdio: ["ignore", "ignore", "ignore"],
@@ -3894,7 +3981,7 @@ function ghAuthStatus(run = execFileSync10) {
3894
3981
  return "gh-probe-error";
3895
3982
  }
3896
3983
  }
3897
- function isRepoPrivate(ref, run = execFileSync10) {
3984
+ function isRepoPrivate(ref, run = execFileSync11) {
3898
3985
  const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
3899
3986
  stdio: ["ignore", "pipe", "ignore"],
3900
3987
  timeout: GH_TIMEOUT_MS
@@ -3902,7 +3989,7 @@ function isRepoPrivate(ref, run = execFileSync10) {
3902
3989
  const parsed = JSON.parse(out);
3903
3990
  return parsed.isPrivate === true;
3904
3991
  }
3905
- function isActionsEnabled(ref, run = execFileSync10) {
3992
+ function isActionsEnabled(ref, run = execFileSync11) {
3906
3993
  const out = run(
3907
3994
  "gh",
3908
3995
  ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
@@ -3910,7 +3997,7 @@ function isActionsEnabled(ref, run = execFileSync10) {
3910
3997
  ).toString().trim();
3911
3998
  return out === "true";
3912
3999
  }
3913
- function disableActions(ref, run = execFileSync10) {
4000
+ function disableActions(ref, run = execFileSync11) {
3914
4001
  run(
3915
4002
  "gh",
3916
4003
  [
@@ -3924,7 +4011,7 @@ function disableActions(ref, run = execFileSync10) {
3924
4011
  { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
3925
4012
  );
3926
4013
  }
3927
- function readOriginRemote(cwd, run = execFileSync10) {
4014
+ function readOriginRemote(cwd, run = execFileSync11) {
3928
4015
  return run("git", ["remote", "get-url", "origin"], {
3929
4016
  cwd,
3930
4017
  stdio: ["ignore", "pipe", "ignore"]
@@ -3932,7 +4019,7 @@ function readOriginRemote(cwd, run = execFileSync10) {
3932
4019
  }
3933
4020
 
3934
4021
  // src/commands.doctor.actions-drift.ts
3935
- function reportActionsDrift(section2, run = execFileSync11) {
4022
+ function reportActionsDrift(section2, run = execFileSync12) {
3936
4023
  let remote;
3937
4024
  try {
3938
4025
  remote = readOriginRemote(repoHome(), run);
@@ -4061,15 +4148,15 @@ function cmdDoctor(opts = {}) {
4061
4148
 
4062
4149
  // src/commands.drop-session.ts
4063
4150
  init_config();
4064
- import { execFileSync as execFileSync13 } from "node:child_process";
4151
+ import { execFileSync as execFileSync14 } from "node:child_process";
4065
4152
  import { existsSync as existsSync29, readdirSync as readdirSync10, statSync as statSync8 } from "node:fs";
4066
4153
  import { join as join35, relative as relative4 } from "node:path";
4067
4154
 
4068
4155
  // src/commands.drop-session.git.ts
4069
- import { execFileSync as execFileSync12 } from "node:child_process";
4156
+ import { execFileSync as execFileSync13 } from "node:child_process";
4070
4157
  function expandStagedDir(dirRel, repo) {
4071
4158
  try {
4072
- const out = execFileSync12("git", ["ls-files", "-z", "--", dirRel], {
4159
+ const out = execFileSync13("git", ["ls-files", "-z", "--", dirRel], {
4073
4160
  cwd: repo,
4074
4161
  stdio: ["ignore", "pipe", "pipe"]
4075
4162
  });
@@ -4080,7 +4167,7 @@ function expandStagedDir(dirRel, repo) {
4080
4167
  }
4081
4168
  function isTrackedInHead(rel, repo) {
4082
4169
  try {
4083
- execFileSync12("git", ["cat-file", "-e", `HEAD:${rel}`], {
4170
+ execFileSync13("git", ["cat-file", "-e", `HEAD:${rel}`], {
4084
4171
  cwd: repo,
4085
4172
  stdio: ["ignore", "pipe", "pipe"]
4086
4173
  });
@@ -4091,7 +4178,7 @@ function isTrackedInHead(rel, repo) {
4091
4178
  }
4092
4179
  function isInIndex(rel, repo) {
4093
4180
  try {
4094
- const out = execFileSync12("git", ["ls-files", "--", rel], {
4181
+ const out = execFileSync13("git", ["ls-files", "--", rel], {
4095
4182
  cwd: repo,
4096
4183
  stdio: ["ignore", "pipe", "pipe"]
4097
4184
  });
@@ -4162,6 +4249,7 @@ function cmdDropSession(id) {
4162
4249
  }
4163
4250
  for (const rel of matches) unstageOne(rel, repo);
4164
4251
  reportScrubHint(id, matches);
4252
+ warnIfSessionPushed(id, repo);
4165
4253
  } catch (err) {
4166
4254
  if (!(err instanceof NomadFatal)) {
4167
4255
  throw err;
@@ -4196,12 +4284,12 @@ function unstageOne(rel, repo) {
4196
4284
  }
4197
4285
  try {
4198
4286
  if (isTrackedInHead(rel, repo)) {
4199
- execFileSync13("git", ["restore", "--staged", "--worktree", "--", rel], {
4287
+ execFileSync14("git", ["restore", "--staged", "--worktree", "--", rel], {
4200
4288
  cwd: repo,
4201
4289
  stdio: ["ignore", "pipe", "pipe"]
4202
4290
  });
4203
4291
  } else {
4204
- execFileSync13("git", ["rm", "--cached", "-f", "--", rel], {
4292
+ execFileSync14("git", ["rm", "--cached", "-f", "--", rel], {
4205
4293
  cwd: repo,
4206
4294
  stdio: ["ignore", "pipe", "pipe"]
4207
4295
  });
@@ -4316,7 +4404,7 @@ import { join as join39 } from "node:path";
4316
4404
 
4317
4405
  // src/extras-sync.diff.ts
4318
4406
  init_utils();
4319
- import { execFileSync as execFileSync14 } from "node:child_process";
4407
+ import { execFileSync as execFileSync15 } from "node:child_process";
4320
4408
  function labelDiffLine(line) {
4321
4409
  const tab = line.indexOf(" ");
4322
4410
  if (tab === -1) return line;
@@ -4331,7 +4419,7 @@ function parseDiffOutput(stdout) {
4331
4419
  }
4332
4420
  function listDivergingFiles(a, b) {
4333
4421
  try {
4334
- const stdout = execFileSync14("git", ["diff", "--no-index", "--name-status", a, b], {
4422
+ const stdout = execFileSync15("git", ["diff", "--no-index", "--name-status", a, b], {
4335
4423
  stdio: ["ignore", "pipe", "pipe"]
4336
4424
  }).toString();
4337
4425
  return parseDiffOutput(stdout);
@@ -4511,10 +4599,10 @@ init_utils_json();
4511
4599
  // src/extras-sync.remap.ts
4512
4600
  init_config();
4513
4601
  import { existsSync as existsSync31, mkdirSync as mkdirSync7, readdirSync as readdirSync12, realpathSync as realpathSync4, rmSync as rmSync11 } from "node:fs";
4514
- 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";
4515
4603
 
4516
4604
  // src/extras-sync.planning-diff.ts
4517
- 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";
4518
4606
  init_utils();
4519
4607
  function processRecord(fields, i, changed, deleted) {
4520
4608
  const status = fields[i];
@@ -4567,7 +4655,7 @@ function planningDeleteTargets(opts) {
4567
4655
  const logicalPrefix = "shared/extras/" + logical + "/";
4568
4656
  const prefix = logicalPrefix + ".planning/";
4569
4657
  const planningRoot = join37(localRoot, ".planning");
4570
- const planningRootBoundary = planningRoot + sep5;
4658
+ const planningRootBoundary = planningRoot + sep6;
4571
4659
  const targets = [];
4572
4660
  for (const repoPath of deleted) {
4573
4661
  if (!repoPath.startsWith(prefix)) {
@@ -4609,7 +4697,7 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
4609
4697
  }
4610
4698
  function pruneEmptyAncestors(target, planningRoot) {
4611
4699
  let dir = dirname7(target);
4612
- while (dir !== planningRoot && dir.startsWith(planningRoot + sep6)) {
4700
+ while (dir !== planningRoot && dir.startsWith(planningRoot + sep7)) {
4613
4701
  try {
4614
4702
  if (readdirSync12(dir).length > 0) break;
4615
4703
  rmSync11(dir, { recursive: true, force: true });
@@ -4627,7 +4715,7 @@ function tryRealpath(dir) {
4627
4715
  }
4628
4716
  }
4629
4717
  function isInsidePlanningRoot(parentReal, rootReal) {
4630
- return parentReal === rootReal || parentReal.startsWith(rootReal + sep6);
4718
+ return parentReal === rootReal || parentReal.startsWith(rootReal + sep7);
4631
4719
  }
4632
4720
  function deletePlanningTarget(target, planningRoot, repoCounterpart) {
4633
4721
  if (existsSync31(repoCounterpart)) return;
@@ -4669,7 +4757,7 @@ function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
4669
4757
  backupExtrasWrite(join38(t.localRoot, t.dirname), ts, t.localRoot);
4670
4758
  const planningRoot = join38(t.localRoot, ".planning");
4671
4759
  for (const target of targets) {
4672
- const relToLocal = target.slice(t.localRoot.length + sep6.length);
4760
+ const relToLocal = target.slice(t.localRoot.length + sep7.length);
4673
4761
  deletePlanningTarget(target, planningRoot, join38(repoExtras, t.logical, relToLocal));
4674
4762
  }
4675
4763
  }
@@ -5274,9 +5362,9 @@ init_config();
5274
5362
  init_commands_pull_wedge();
5275
5363
  init_utils();
5276
5364
  init_utils_fs();
5277
- import { execFileSync as execFileSync15 } from "node:child_process";
5365
+ import { execFileSync as execFileSync16 } from "node:child_process";
5278
5366
  function gitCapture(args, cwd) {
5279
- return execFileSync15("git", args, {
5367
+ return execFileSync16("git", args, {
5280
5368
  cwd,
5281
5369
  stdio: ["ignore", "pipe", "pipe"],
5282
5370
  maxBuffer: 64 * 1024 * 1024
@@ -5593,7 +5681,7 @@ function enforceAllowList(statusPorcelain, map) {
5593
5681
 
5594
5682
  // src/push-global-config.ts
5595
5683
  init_config();
5596
- import { execFileSync as execFileSync16 } from "node:child_process";
5684
+ import { execFileSync as execFileSync17 } from "node:child_process";
5597
5685
  var STATUS_LABELS = {
5598
5686
  A: "add",
5599
5687
  M: "modify",
@@ -5633,7 +5721,7 @@ function isInScope(filePath, exactPrefixes, dirPrefixes) {
5633
5721
  }
5634
5722
  function collectGlobalConfigChanges(repoHome2, hostname2, opts) {
5635
5723
  const args = opts.staged ? ["diff", "--cached", "--name-status", "-z"] : ["diff", "HEAD", "--name-status", "-z"];
5636
- const raw = execFileSync16("git", args, {
5724
+ const raw = execFileSync17("git", args, {
5637
5725
  cwd: repoHome2,
5638
5726
  stdio: ["ignore", "pipe", "pipe"]
5639
5727
  }).toString();
@@ -5862,16 +5950,16 @@ async function cmdPush(opts = {}) {
5862
5950
  }
5863
5951
 
5864
5952
  // src/commands.update.ts
5865
- import { execFileSync as execFileSync17 } from "node:child_process";
5953
+ import { execFileSync as execFileSync18 } from "node:child_process";
5866
5954
  init_utils();
5867
- function readInstalledVersion(run = execFileSync17) {
5955
+ function readInstalledVersion(run = execFileSync18) {
5868
5956
  try {
5869
5957
  return run("nomad", ["--version"], { encoding: "utf8" }).toString().trim() || null;
5870
5958
  } catch {
5871
5959
  return null;
5872
5960
  }
5873
5961
  }
5874
- function cmdUpdate(run = execFileSync17) {
5962
+ function cmdUpdate(run = execFileSync18) {
5875
5963
  console.log("Updating claude-nomad CLI via npm...");
5876
5964
  try {
5877
5965
  run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
@@ -5925,14 +6013,14 @@ import { join as join48 } from "node:path";
5925
6013
 
5926
6014
  // src/init.gh-onboard.ts
5927
6015
  init_config();
5928
- import { execFileSync as execFileSync18 } from "node:child_process";
6016
+ import { execFileSync as execFileSync19 } from "node:child_process";
5929
6017
  init_utils();
5930
6018
  var DEFAULT_REPO_NAME = "claude-nomad-config";
5931
6019
  function isValidRepoName(name) {
5932
6020
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
5933
6021
  }
5934
6022
  var GH_NETWORK_TIMEOUT_MS = 3e4;
5935
- function ensureOriginRepo(repoName, run = execFileSync18) {
6023
+ function ensureOriginRepo(repoName, run = execFileSync19) {
5936
6024
  if (!isValidRepoName(repoName)) {
5937
6025
  die(
5938
6026
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -6145,86 +6233,20 @@ function maybeDisableRepoActions(repoHome2, run) {
6145
6233
  }
6146
6234
  }
6147
6235
 
6148
- // 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
+ }
6149
6243
  function extractFlagValue(argv, i) {
6150
6244
  const val = argv[i + 1];
6151
6245
  if (val === void 0 || val.startsWith("--")) return null;
6152
6246
  return val;
6153
6247
  }
6154
- function applyInitToken(argv, i, st) {
6155
- const token = argv[i];
6156
- if (token === "--snapshot") {
6157
- if (st.sawSnapshot) return { ok: false, advance: 0 };
6158
- st.sawSnapshot = true;
6159
- st.snapshot = true;
6160
- return { ok: true, advance: 1 };
6161
- }
6162
- if (token === "--keep-actions") {
6163
- if (st.sawKeepActions) return { ok: false, advance: 0 };
6164
- st.sawKeepActions = true;
6165
- st.keepActions = true;
6166
- return { ok: true, advance: 1 };
6167
- }
6168
- if (token === "--repo") {
6169
- if (st.sawRepo) return { ok: false, advance: 0 };
6170
- st.sawRepo = true;
6171
- const val = extractFlagValue(argv, i);
6172
- if (val === null) return { ok: false, advance: 0 };
6173
- st.repoName = val;
6174
- return { ok: true, advance: 2 };
6175
- }
6176
- return { ok: false, advance: 0 };
6177
- }
6178
- function parseInitArgs(argv) {
6179
- const st = {
6180
- snapshot: false,
6181
- keepActions: false,
6182
- repoName: void 0,
6183
- sawSnapshot: false,
6184
- sawKeepActions: false,
6185
- sawRepo: false
6186
- };
6187
- let i = 3;
6188
- while (i < argv.length) {
6189
- const { ok: ok2, advance } = applyInitToken(argv, i, st);
6190
- if (!ok2) return null;
6191
- i += advance;
6192
- }
6193
- return { snapshot: st.snapshot, keepActions: st.keepActions, repoName: st.repoName };
6194
- }
6195
- function parseRedactArgs(argv) {
6196
- const id = argv[3];
6197
- if (typeof id !== "string" || !/^\w[\w-]{0,127}$/.test(id)) {
6198
- return null;
6199
- }
6200
- let rule;
6201
- let dryRun = false;
6202
- let sawRule = false;
6203
- let sawDryRun = false;
6204
- let i = 4;
6205
- while (i < argv.length) {
6206
- const token = argv[i];
6207
- if (token === "--dry-run") {
6208
- if (sawDryRun) return null;
6209
- sawDryRun = true;
6210
- dryRun = true;
6211
- i++;
6212
- } else if (token === "--rule") {
6213
- if (sawRule) return null;
6214
- sawRule = true;
6215
- const val = argv[i + 1];
6216
- if (val === void 0 || val.startsWith("--")) return null;
6217
- rule = val;
6218
- i += 2;
6219
- } else {
6220
- return null;
6221
- }
6222
- }
6223
- return { id, rule, dryRun };
6224
- }
6225
6248
 
6226
6249
  // src/nomad.dispatch.clean.ts
6227
- var REJECT = { ok: false, advance: 0 };
6228
6250
  function applyOlderThan(argv, i, st) {
6229
6251
  if (st.olderThan !== void 0) return REJECT;
6230
6252
  const val = extractFlagValue(argv, i);
@@ -6239,11 +6261,6 @@ function applyKeep(argv, i, st) {
6239
6261
  st.keep = Number(val);
6240
6262
  return { ok: true, advance: 2 };
6241
6263
  }
6242
- function applyBool(seen, set) {
6243
- if (seen) return REJECT;
6244
- set();
6245
- return { ok: true, advance: 1 };
6246
- }
6247
6264
  function applyCleanToken(argv, i, st) {
6248
6265
  switch (argv[i]) {
6249
6266
  case "--backups":
@@ -6293,6 +6310,79 @@ function parseEjectArgs(argv) {
6293
6310
  return { dryRun };
6294
6311
  }
6295
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
+
6296
6386
  // src/nomad.dispatch.allow.ts
6297
6387
  function parseAllowArgs(argv) {
6298
6388
  const positionals = argv.slice(3);
@@ -6326,32 +6416,26 @@ function parsePullArgs(argv) {
6326
6416
  }
6327
6417
 
6328
6418
  // src/nomad.dispatch.push.ts
6329
- var REJECT2 = { ok: false, advance: 0 };
6330
- function applyBool2(seen, set) {
6331
- if (seen) return REJECT2;
6332
- set();
6333
- return { ok: true, advance: 1 };
6334
- }
6335
6419
  var RULE_ID_RE = /^\w[\w-]*$/;
6336
6420
  function applyAllow2(argv, i, st) {
6337
- if (st.allowRule !== void 0) return REJECT2;
6421
+ if (st.allowRule !== void 0) return REJECT;
6338
6422
  const val = extractFlagValue(argv, i);
6339
- if (val === null || !RULE_ID_RE.test(val)) return REJECT2;
6423
+ if (val === null || !RULE_ID_RE.test(val)) return REJECT;
6340
6424
  st.allowRule = val;
6341
6425
  return { ok: true, advance: 2 };
6342
6426
  }
6343
6427
  function applyPushToken(argv, i, st) {
6344
6428
  switch (argv[i]) {
6345
6429
  case "--dry-run":
6346
- return applyBool2(st.dryRun, () => st.dryRun = true);
6430
+ return applyBool(st.dryRun, () => st.dryRun = true);
6347
6431
  case "--redact-all":
6348
- return applyBool2(st.redactAll, () => st.redactAll = true);
6432
+ return applyBool(st.redactAll, () => st.redactAll = true);
6349
6433
  case "--allow-all":
6350
- return applyBool2(st.allowAll, () => st.allowAll = true);
6434
+ return applyBool(st.allowAll, () => st.allowAll = true);
6351
6435
  case "--allow":
6352
6436
  return applyAllow2(argv, i, st);
6353
6437
  default:
6354
- return REJECT2;
6438
+ return REJECT;
6355
6439
  }
6356
6440
  }
6357
6441
  function parsePushArgs(argv) {
@@ -6383,7 +6467,7 @@ function parsePushArgs(argv) {
6383
6467
  // package.json
6384
6468
  var package_default = {
6385
6469
  name: "claude-nomad",
6386
- version: "0.50.2",
6470
+ version: "0.50.3",
6387
6471
  type: "module",
6388
6472
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
6389
6473
  keywords: [
@@ -6612,7 +6696,7 @@ function resumeCmd(sessionId) {
6612
6696
  process.exit(1);
6613
6697
  }
6614
6698
  const map = readJson(mapPath);
6615
- const schemaError = validatePathMap(map);
6699
+ const schemaError = validatePathMapShape(map);
6616
6700
  if (schemaError !== null) {
6617
6701
  fail(schemaError);
6618
6702
  process.exit(1);
@@ -6647,26 +6731,6 @@ function extractRecordedCwd(jsonlPath) {
6647
6731
  }
6648
6732
  return null;
6649
6733
  }
6650
- function validatePathMap(raw) {
6651
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
6652
- return "path-map.json invalid schema: top-level value must be an object";
6653
- }
6654
- const projects = raw.projects;
6655
- if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
6656
- return 'path-map.json invalid schema: "projects" must be an object';
6657
- }
6658
- for (const [name, hosts] of Object.entries(projects)) {
6659
- if (hosts === null || typeof hosts !== "object" || Array.isArray(hosts)) {
6660
- return `path-map.json invalid schema: project "${name}" hosts must be an object`;
6661
- }
6662
- for (const [host, value] of Object.entries(hosts)) {
6663
- if (typeof value !== "string") {
6664
- return `path-map.json invalid schema: project "${name}" host "${host}" path must be a string`;
6665
- }
6666
- }
6667
- }
6668
- return null;
6669
- }
6670
6734
  function lookupLocalPath(map, recordedCwd) {
6671
6735
  for (const [logical, hosts] of Object.entries(map.projects)) {
6672
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.2",
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": [