claude-nomad 0.54.0 → 0.55.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.55.1](https://github.com/funkadelic/claude-nomad/compare/v0.55.0...v0.55.1) (2026-06-29)
4
+
5
+
6
+ ### Fixed
7
+
8
+ * **config:** raise gitleaks scan timeout from 5 min to 15 min ([#367](https://github.com/funkadelic/claude-nomad/issues/367)) ([30e7344](https://github.com/funkadelic/claude-nomad/commit/30e73443be6d58330716418dced44aa6cf772623))
9
+ * **doctor:** de-backtrack super-linear regexes and group test hook ([#363](https://github.com/funkadelic/claude-nomad/issues/363)) ([4fdbdaa](https://github.com/funkadelic/claude-nomad/commit/4fdbdaa595d3e62cf87cab8e147da130c574f272))
10
+ * **push:** bound gitleaks scans and show dry-run leak-scan spinner ([#365](https://github.com/funkadelic/claude-nomad/issues/365)) ([a04d06a](https://github.com/funkadelic/claude-nomad/commit/a04d06a9624f9fa60d68cc01c69009fb0908e05f))
11
+ * **remap:** match Claude Code's transcript-dir path encoder ([#360](https://github.com/funkadelic/claude-nomad/issues/360)) ([77a4835](https://github.com/funkadelic/claude-nomad/commit/77a4835629a04fbf045930b1938b2404f0b00922))
12
+
13
+
14
+ ### Documentation
15
+
16
+ * correct path-encoding rule and note Windows-style paths ([#362](https://github.com/funkadelic/claude-nomad/issues/362)) ([970aae4](https://github.com/funkadelic/claude-nomad/commit/970aae42ebba734a878f9712ac304f0c393b9398))
17
+
18
+
19
+ ### Testing
20
+
21
+ * use specific assertions for length and exit-code checks ([#364](https://github.com/funkadelic/claude-nomad/issues/364)) ([95549d4](https://github.com/funkadelic/claude-nomad/commit/95549d4694e5f0dc09ac1aa13d4240e01cb264f9))
22
+
23
+
24
+ ### Dependencies
25
+
26
+ * **deps-dev:** bump the dev-dependencies group with 6 updates ([#366](https://github.com/funkadelic/claude-nomad/issues/366)) ([70d1057](https://github.com/funkadelic/claude-nomad/commit/70d10578eb8d58010b160b88a6d61eaa9e8405a0))
27
+
28
+ ## [0.55.0](https://github.com/funkadelic/claude-nomad/compare/v0.54.0...v0.55.0) (2026-06-28)
29
+
30
+
31
+ ### Added
32
+
33
+ * **doctor:** warn on misaligned host key when NOMAD_HOST is unset ([#356](https://github.com/funkadelic/claude-nomad/issues/356)) ([a13e833](https://github.com/funkadelic/claude-nomad/commit/a13e833a09636f98f1e9c51e9c41f01a4323dea2))
34
+ * **init:** offer to snapshot an existing ~/.claude on init ([#357](https://github.com/funkadelic/claude-nomad/issues/357)) ([92e90b1](https://github.com/funkadelic/claude-nomad/commit/92e90b1977fff51d565fa5d11b3c640790ee1b57))
35
+
36
+
37
+ ### Fixed
38
+
39
+ * make sync warning messages user-friendly ([#345](https://github.com/funkadelic/claude-nomad/issues/345)) ([002f57d](https://github.com/funkadelic/claude-nomad/commit/002f57de240dfb1349742fa4c2b4ac5e51b5f7ba))
40
+
41
+
42
+ ### Documentation
43
+
44
+ * clarify onboarding (two-dir model, session sync, push order) ([#355](https://github.com/funkadelic/claude-nomad/issues/355)) ([396fbb8](https://github.com/funkadelic/claude-nomad/commit/396fbb875b4331800ee74834944da17decbcad92))
45
+
3
46
  ## [0.54.0](https://github.com/funkadelic/claude-nomad/compare/v0.53.3...v0.54.0) (2026-06-27)
4
47
 
5
48
 
package/README.md CHANGED
@@ -39,7 +39,9 @@ survives different file paths and your secrets never ride along.
39
39
  live settings syncs normally via `nomad capture-settings`.
40
40
  - **Every push is secret-scanned.** Only an explicit allow-list of paths ever leaves the machine,
41
41
  credentials never sync, and gitleaks scans the exact files about to be published. The push aborts
42
- on any hit, with an interactive menu to redact, allow, or drop the finding.
42
+ on any hit, with an interactive menu to redact, allow, or drop the finding. Always publish through
43
+ `nomad push`: the sync repo is an ordinary Git repo, so a manual `git push` from it skips the scan
44
+ entirely and can leak a secret that `nomad push` would have caught.
43
45
  - **Preview before you trust it.** `nomad diff` shows offline what a pull would change (gsd-owned
44
46
  hook churn is filtered the same as on pull, so the preview matches what a real pull writes), and
45
47
  `--dry-run` on pull and push prints the plan without writing anything.
@@ -47,13 +49,16 @@ survives different file paths and your secrets never ride along.
47
49
  repo, broken hook references, hooks that would crash on session start because of a missing
48
50
  `--preserve-symlinks-main` flag, version drift, oversized backup cache, missing git committer
49
51
  identity in the sync repo (a push fails at commit time without one), path-map entries whose local
50
- project folder no longer exists on this machine, synced skills with local edits that differ from
51
- the shared copy, and settings drift in both directions: keys present in the repo merge but absent
52
- from your live `settings.json` (behind; the next `nomad pull` will restore them, fix:
53
- `nomad pull`) and keys present locally but not yet in the repo (ahead; local-only additions, fix:
54
- `nomad capture-settings`). Each issue includes a fix hint. By default the report is compact: it
55
- shows only checks that need action plus a one-line verdict. Add `--verbose` (or `--all` / `-v`) to
56
- see the full per-check tree, including everything that passed.
52
+ project folder no longer exists on this machine, a multi-host repo where this machine's
53
+ hostname-derived key matches no `hosts/<HOST>.json` or path-map entry (a sign `NOMAD_HOST` is
54
+ unset here, so per-host settings and session sync will not line up with the other hosts), synced
55
+ skills with local edits that differ from the shared copy, and settings drift in both directions:
56
+ keys present in the repo merge but absent from your live `settings.json` (behind; the next
57
+ `nomad pull` will restore them, fix: `nomad pull`) and keys present locally but not yet in the
58
+ repo (ahead; local-only additions, fix: `nomad capture-settings`). Each issue includes a fix hint.
59
+ By default the report is compact: it shows only checks that need action plus a one-line verdict.
60
+ Add `--verbose` (or `--all` / `-v`) to see the full per-check tree, including everything that
61
+ passed.
57
62
  - **Self-healing sync.** Every overwrite is backed up first, and `nomad pull --force-remote`
58
63
  recovers two kinds of stuck sync repo: a repo stuck mid-rebase or mid-merge (aborts the operation,
59
64
  parks stranded work on a branch, refuses if shared config is at risk), and a repo where the rebase
@@ -68,15 +73,27 @@ opt-in per-project sync, transcript redaction, backup pruning, and more.
68
73
 
69
74
  ## Quickstart
70
75
 
76
+ nomad works with two directories, and the difference is the one thing worth learning up front:
77
+
78
+ - **`~/claude-nomad/`** is your private sync repo. This is the one you edit.
79
+ - **`~/.claude/`** is Claude Code's live config. nomad regenerates it on every `pull`.
80
+
81
+ Edit the repo, never the live config. In particular, never hand-edit `~/.claude/settings.json`: it
82
+ is rebuilt from the repo on every pull and your changes are lost. Change `shared/settings.base.json`
83
+ (or `hosts/<HOST>.json`) in the repo instead, or run `nomad capture-settings` to pull local changes
84
+ back into the repo (see [Changing settings](#changing-settings)).
85
+
71
86
  **First host** (once, ever):
72
87
 
73
88
  ```bash
74
89
  # 1. Install the CLI.
75
90
  $ npm i -g claude-nomad
76
91
 
77
- # 2. Create your private sync repo and scaffold it.
92
+ # 2. Create your private sync repo and scaffold it. If you already have a
93
+ # ~/.claude/ worth keeping, init offers to seed the repo from it.
78
94
  $ nomad init # prompts for a repo name (default: claude-nomad-config)
79
- $ nomad init --repo my-config # non-interactive
95
+ $ nomad init --repo my-config # set the repo name without the prompt
96
+ $ nomad init --snapshot # seed from existing ~/.claude/ without being asked
80
97
 
81
98
  # 3. Add a stable host label to ~/.zshrc or ~/.bashrc, then reload.
82
99
  export NOMAD_HOST=<your-host-label>
@@ -102,6 +119,45 @@ $ nomad pull # apply config to ~/.claude/
102
119
  $ nomad push # publish local changes (sessions, settings)
103
120
  ```
104
121
 
122
+ Pull before you push whenever both machines may have changed. Sync is last-write-wins, so pushing
123
+ stale local state over newer remote state silently overwrites it. The
124
+ [FAQ](https://funkadelic.github.io/claude-nomad/faq/) covers the full push/pull order when both
125
+ sides have changed.
126
+
127
+ ### Make your sessions follow you
128
+
129
+ Session history only syncs for projects you list in `path-map.json`, and a fresh `init` starts with
130
+ none, so no sessions sync until you add a mapping. Each entry maps a logical project name to the
131
+ absolute path it lives at on each host:
132
+
133
+ ```json
134
+ {
135
+ "projects": {
136
+ "my-app": {
137
+ "laptop": "/Users/you/code/my-app",
138
+ "desktop": "/home/you/projects/my-app"
139
+ }
140
+ }
141
+ }
142
+ ```
143
+
144
+ The host keys (`laptop`, `desktop`) are the same labels you set in `NOMAD_HOST` on each machine.
145
+ After editing `path-map.json`, `nomad push` publishes the matching sessions and `nomad pull` on
146
+ another host copies them into place, rewriting the embedded file paths so `claude --resume` finds
147
+ them at that host's path.
148
+
149
+ ### Changing settings
150
+
151
+ There are two ways a settings change reaches the repo, and the right one depends on where you made
152
+ it:
153
+
154
+ - **You are deciding the change:** edit `shared/settings.base.json` (shared by every host) or
155
+ `hosts/<HOST>.json` (one machine only) in the repo, then `nomad push`.
156
+ - **Something else already wrote it** (Claude Code or a tool added keys to your live
157
+ `~/.claude/settings.json`): run `nomad capture-settings` to promote those keys into the repo
158
+ before the next `nomad pull` overwrites them. Add `--host` to land machine-specific values (such
159
+ as absolute paths) in `hosts/<HOST>.json` instead of the shared base.
160
+
105
161
  During `nomad push` and `nomad pull`, long-running steps (rebase, secret scan, git push, session
106
162
  sync) show an animated progress indicator on an interactive terminal so the CLI does not look hung.
107
163
  In CI and when output is piped, only plain text lines are printed, with no ANSI control codes, so
package/dist/nomad.mjs CHANGED
@@ -423,7 +423,7 @@ function allSharedLinks(map) {
423
423
  }
424
424
  return [...SHARED_LINKS, ...extras];
425
425
  }
426
- var SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, HOST, SHARED_LINKS, GSD_PREFIX, GSD_DROPPED_NAMES, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, PUSH_ALLOWED_STATIC;
426
+ var SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, GITLEAKS_SCAN_TIMEOUT_MS, HOST, SHARED_LINKS, GSD_PREFIX, GSD_DROPPED_NAMES, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, PUSH_ALLOWED_STATIC;
427
427
  var init_config = __esm({
428
428
  "src/config.ts"() {
429
429
  "use strict";
@@ -434,6 +434,7 @@ var init_config = __esm({
434
434
  SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
435
435
  NPM_REGISTRY_LATEST_URL = "https://registry.npmjs.org/claude-nomad/latest";
436
436
  GITLEAKS_PINNED_VERSION = "8.30.1";
437
+ GITLEAKS_SCAN_TIMEOUT_MS = 9e5;
437
438
  HOST = (process.env.NOMAD_HOST || hostname()).toLowerCase();
438
439
  SHARED_LINKS = ["CLAUDE.md", "commands", "rules", "my-statusline.cjs"];
439
440
  GSD_PREFIX = "gsd-";
@@ -523,13 +524,23 @@ function sortKeysDeep(value) {
523
524
  }
524
525
  return value;
525
526
  }
526
- var encodePath;
527
+ var ENCODE_MAX, hashPath, encodePath;
527
528
  var init_utils_json = __esm({
528
529
  "src/utils.json.ts"() {
529
530
  "use strict";
530
531
  init_config();
531
532
  init_utils();
532
- encodePath = (absPath) => absPath.replaceAll("/", "-");
533
+ ENCODE_MAX = 200;
534
+ hashPath = (s) => {
535
+ let h2 = 0;
536
+ for (let i = 0; i < s.length; i++) h2 = (h2 << 5) - h2 + s.charCodeAt(i) | 0;
537
+ return h2;
538
+ };
539
+ encodePath = (absPath) => {
540
+ const enc = absPath.replace(/[^a-zA-Z0-9]/g, "-");
541
+ if (enc.length <= ENCODE_MAX) return enc;
542
+ return `${enc.slice(0, ENCODE_MAX)}-${Math.abs(hashPath(absPath)).toString(36)}`;
543
+ };
533
544
  }
534
545
  });
535
546
 
@@ -769,7 +780,7 @@ var init_push_gitleaks_config = __esm({
769
780
  "use strict";
770
781
  init_config();
771
782
  init_utils();
772
- OVERLAY_EXTEND_RE = /^\s*(?:\[\s*extend\s*\]|extend\s*[.=])/m;
783
+ OVERLAY_EXTEND_RE = /^[ \t]*(?:\[[ \t]*extend[ \t]*\]|extend[ \t]*[.=])/m;
773
784
  TABLE_HEADER_RE = /^\s*\[/;
774
785
  OVERLAY_ALLOWLIST_HEADER_RE = /^\s*\[\[?\s*allowlists?\s*\]\]?/;
775
786
  OVERLAY_INLINE_ALLOWLIST_RE = /^[ \t]*allowlists?[ \t]*[.=]/m;
@@ -780,7 +791,7 @@ var init_push_gitleaks_config = __esm({
780
791
 
781
792
  // src/push-checks.ts
782
793
  import { execFileSync as execFileSync4 } from "node:child_process";
783
- import { readdirSync as readdirSync4, rmSync as rmSync5 } from "node:fs";
794
+ import { readdirSync as readdirSync5, rmSync as rmSync5 } from "node:fs";
784
795
  import { homedir as homedir2, platform } from "node:os";
785
796
  import { join as join17 } from "node:path";
786
797
  function gitleaksInstallHint() {
@@ -820,7 +831,7 @@ function findGitlinks(dir) {
820
831
  function walk(current) {
821
832
  let entries;
822
833
  try {
823
- entries = readdirSync4(current, { withFileTypes: true });
834
+ entries = readdirSync5(current, { withFileTypes: true });
824
835
  } catch {
825
836
  return;
826
837
  }
@@ -912,10 +923,11 @@ function scanStagedTree(repoDir, forwardStreams = false) {
912
923
  ];
913
924
  if (toml !== null) args.push("--config", toml);
914
925
  const opts = { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] };
926
+ const gitleaksOpts = { ...opts, timeout: GITLEAKS_SCAN_TIMEOUT_MS };
915
927
  try {
916
928
  execFileSync8("git", ["init", "-q"], opts);
917
929
  execFileSync8("git", ["add", "-A"], opts);
918
- execFileSync8("gitleaks", args, opts);
930
+ execFileSync8("gitleaks", args, gitleaksOpts);
919
931
  return [];
920
932
  } catch (err) {
921
933
  const e = err;
@@ -945,7 +957,10 @@ function scanFile(filePath, forwardStreams = false) {
945
957
  `--report-path=${reportPath}`
946
958
  ];
947
959
  if (toml !== null) args.push("--config", toml);
948
- const opts = { stdio: ["ignore", "pipe", "pipe"] };
960
+ const opts = {
961
+ stdio: ["ignore", "pipe", "pipe"],
962
+ timeout: GITLEAKS_SCAN_TIMEOUT_MS
963
+ };
949
964
  try {
950
965
  execFileSync8("gitleaks", args, opts);
951
966
  return [];
@@ -966,6 +981,7 @@ function scanFile(filePath, forwardStreams = false) {
966
981
  var init_push_gitleaks_scan = __esm({
967
982
  "src/push-gitleaks.scan.ts"() {
968
983
  "use strict";
984
+ init_config();
969
985
  init_push_gitleaks_config();
970
986
  init_utils_fs();
971
987
  }
@@ -1505,6 +1521,14 @@ function partitionByCaptureExclusion(keys) {
1505
1521
  }
1506
1522
  return { promotable, excluded };
1507
1523
  }
1524
+ function describeSettings(keys) {
1525
+ const one = keys.length === 1;
1526
+ return {
1527
+ phrase: `${keys.length} ${one ? "setting" : "settings"} (${keys.join(", ")})`,
1528
+ pronoun: one ? "it" : "them",
1529
+ verb: one ? "is" : "are"
1530
+ };
1531
+ }
1508
1532
  var BIN_NODE_RE = /^"?(?:[A-Za-z]:)?[\\/](?:.*[\\/])?bin[\\/]node"?$/;
1509
1533
  var LEADING_BIN_NODE_RE = /^"?(?:[A-Za-z]:)?[\\/](?:[^"\s]*[\\/])?bin[\\/]node"?(?=\s)/;
1510
1534
  function normalizeNodePathsDeep(value) {
@@ -1614,14 +1638,16 @@ function regenerateSettings(ts, opts = {}) {
1614
1638
  const existing = readJson(settingsPath);
1615
1639
  const drift = classifySettingsDrift(merged, existing);
1616
1640
  if (drift.behind.length > 0) {
1641
+ const { phrase, pronoun } = describeSettings(drift.behind);
1617
1642
  warn(
1618
- `existing settings.json is missing merged keys ${JSON.stringify(drift.behind)}. Run 'nomad pull' to restore them.`
1643
+ `your settings.json is missing ${phrase} that the synced copy has; run 'nomad pull' to restore ${pronoun}.`
1619
1644
  );
1620
1645
  }
1621
1646
  const { promotable } = partitionByCaptureExclusion(drift.ahead);
1622
1647
  if (promotable.length > 0) {
1648
+ const { phrase, pronoun, verb } = describeSettings(promotable);
1623
1649
  warn(
1624
- `existing settings.json has local-only keys ${JSON.stringify(promotable)}. Run 'nomad capture-settings' to promote them into the repo before they are overwritten.`
1650
+ `your settings.json has ${phrase} that ${verb} not yet synced; run 'nomad capture-settings' to save ${pronoun} to the repo before the next pull overwrites ${pronoun}.`
1625
1651
  );
1626
1652
  }
1627
1653
  } catch {
@@ -2069,7 +2095,7 @@ import { join as join36 } from "node:path";
2069
2095
  // src/commands.doctor.checks.repo.ts
2070
2096
  init_color();
2071
2097
  init_config();
2072
- import { existsSync as existsSync9, lstatSync as lstatSync6, statSync as statSync3 } from "node:fs";
2098
+ import { existsSync as existsSync9, lstatSync as lstatSync6, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
2073
2099
  import { join as join11 } from "node:path";
2074
2100
 
2075
2101
  // src/commands.doctor.format.ts
@@ -2189,6 +2215,7 @@ function reasonForPartial(repoHome2, host) {
2189
2215
  }
2190
2216
 
2191
2217
  // src/commands.doctor.checks.repo.ts
2218
+ init_utils_json();
2192
2219
  function isOverrideActive() {
2193
2220
  return Boolean(process.env.NOMAD_REPO);
2194
2221
  }
@@ -2206,6 +2233,46 @@ function reportHostAndPaths(section2) {
2206
2233
  `${existsSync9(claude) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(claude)}`
2207
2234
  );
2208
2235
  }
2236
+ function pathMapHostKeys() {
2237
+ const mapPath = join11(repoHome(), "path-map.json");
2238
+ if (!existsSync9(mapPath)) return /* @__PURE__ */ new Set();
2239
+ let raw;
2240
+ try {
2241
+ raw = readJson(mapPath);
2242
+ } catch {
2243
+ return /* @__PURE__ */ new Set();
2244
+ }
2245
+ if (validatePathMapShape(raw) !== null) return /* @__PURE__ */ new Set();
2246
+ const keys = /* @__PURE__ */ new Set();
2247
+ for (const hosts of Object.values(raw.projects)) {
2248
+ for (const key of Object.keys(hosts)) keys.add(key);
2249
+ }
2250
+ return keys;
2251
+ }
2252
+ function hostOverrideLabels() {
2253
+ let entries;
2254
+ try {
2255
+ entries = readdirSync2(join11(repoHome(), "hosts"));
2256
+ } catch {
2257
+ return /* @__PURE__ */ new Set();
2258
+ }
2259
+ const labels = /* @__PURE__ */ new Set();
2260
+ for (const entry of entries) {
2261
+ if (entry.endsWith(".json")) labels.add(entry.slice(0, -".json".length));
2262
+ }
2263
+ return labels;
2264
+ }
2265
+ function reportHostKeyAlignment(section2) {
2266
+ if (process.env.NOMAD_HOST) return;
2267
+ const overrideLabels = hostOverrideLabels();
2268
+ const mapKeys = pathMapHostKeys();
2269
+ if (overrideLabels.has(HOST) || mapKeys.has(HOST)) return;
2270
+ if (overrideLabels.size === 0 && mapKeys.size === 0) return;
2271
+ addItem(
2272
+ section2,
2273
+ `${yellow(warnGlyph)} NOMAD_HOST unset: this repo configures other hosts, but the hostname key ${cyan(HOST)} matches no hosts/${HOST}.json or path-map entry; set NOMAD_HOST to the label this host should use so per-host settings and session sync line up`
2274
+ );
2275
+ }
2209
2276
  function reportRepoState(section2) {
2210
2277
  const repo = repoHome();
2211
2278
  const state = classifyRepoState(repo, HOST);
@@ -2301,7 +2368,7 @@ function reportDroppedNamesMigration(section2) {
2301
2368
  // src/commands.doctor.checks.settings.ts
2302
2369
  init_color();
2303
2370
  init_config();
2304
- import { existsSync as existsSync10, readdirSync as readdirSync2 } from "node:fs";
2371
+ import { existsSync as existsSync10, readdirSync as readdirSync3 } from "node:fs";
2305
2372
  import { join as join12 } from "node:path";
2306
2373
  function loadBaseSettings(section2) {
2307
2374
  const basePath = join12(repoHome(), "shared", "settings.base.json");
@@ -2347,7 +2414,7 @@ function reportHostOverrides(section2, base, settings) {
2347
2414
  );
2348
2415
  const hostsDir = join12(repo, "hosts");
2349
2416
  if (existsSync10(hostsDir)) {
2350
- const cands = readdirSync2(hostsDir).filter((f) => f.endsWith(".json"));
2417
+ const cands = readdirSync3(hostsDir).filter((f) => f.endsWith(".json"));
2351
2418
  if (cands.length > 0) addItem(section2, `${dim(infoGlyph)} candidates: ${cands.join(", ")}`);
2352
2419
  }
2353
2420
  process.exitCode = 1;
@@ -2362,7 +2429,7 @@ function reportHostOverrides(section2, base, settings) {
2362
2429
  // src/commands.doctor.checks.pathmap.ts
2363
2430
  init_color();
2364
2431
  init_config();
2365
- import { existsSync as existsSync11, readdirSync as readdirSync3 } from "node:fs";
2432
+ import { existsSync as existsSync11, readdirSync as readdirSync4 } from "node:fs";
2366
2433
  import { join as join13 } from "node:path";
2367
2434
  init_utils_json();
2368
2435
  function reportMappedProjects(section2, map) {
@@ -2377,7 +2444,7 @@ function reportUnmappedProjects(section2, map) {
2377
2444
  if (!existsSync11(localProjects)) return;
2378
2445
  let localDirs;
2379
2446
  try {
2380
- localDirs = readdirSync3(localProjects);
2447
+ localDirs = readdirSync4(localProjects);
2381
2448
  } catch {
2382
2449
  return;
2383
2450
  }
@@ -2666,13 +2733,13 @@ function reportGitIdentity(section2) {
2666
2733
 
2667
2734
  // src/commands.doctor.checks.backups.ts
2668
2735
  init_color();
2669
- import { existsSync as existsSync16, lstatSync as lstatSync7, readdirSync as readdirSync5 } from "node:fs";
2736
+ import { existsSync as existsSync16, lstatSync as lstatSync7, readdirSync as readdirSync6 } from "node:fs";
2670
2737
  import { join as join19 } from "node:path";
2671
2738
  init_config();
2672
2739
  var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
2673
2740
  function safeReaddir(dir) {
2674
2741
  try {
2675
- return readdirSync5(dir);
2742
+ return readdirSync6(dir);
2676
2743
  } catch {
2677
2744
  return [];
2678
2745
  }
@@ -2848,7 +2915,7 @@ function reportCheckSchema(section2) {
2848
2915
  init_color();
2849
2916
  import { randomBytes } from "node:crypto";
2850
2917
  import { execFileSync as execFileSync9 } from "node:child_process";
2851
- import { existsSync as existsSync19, mkdirSync as mkdirSync5, readdirSync as readdirSync7, rmSync as rmSync8 } from "node:fs";
2918
+ import { existsSync as existsSync19, mkdirSync as mkdirSync5, readdirSync as readdirSync8, rmSync as rmSync8 } from "node:fs";
2852
2919
  import { homedir as homedir4 } from "node:os";
2853
2920
  import { join as join24 } from "node:path";
2854
2921
 
@@ -2947,7 +3014,7 @@ init_config();
2947
3014
 
2948
3015
  // src/remap.ts
2949
3016
  init_config_sharedDirs_guard();
2950
- import { cpSync as cpSync4, existsSync as existsSync18, mkdirSync as mkdirSync4, readdirSync as readdirSync6, renameSync as renameSync3, rmSync as rmSync7, statSync as statSync4 } from "node:fs";
3017
+ import { cpSync as cpSync4, existsSync as existsSync18, mkdirSync as mkdirSync4, readdirSync as readdirSync7, renameSync as renameSync3, rmSync as rmSync7, statSync as statSync4 } from "node:fs";
2951
3018
  import { join as join23, relative as relative3, sep as sep3 } from "node:path";
2952
3019
 
2953
3020
  // src/extras-sync.guards.ts
@@ -3087,7 +3154,7 @@ function remapPush(ts, opts = {}) {
3087
3154
  const reverse = buildReverseMap(map);
3088
3155
  if (!existsSync18(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
3089
3156
  if (!dryRun) mkdirSync4(repoProjects, { recursive: true });
3090
- for (const dir of readdirSync6(localProjects)) {
3157
+ for (const dir of readdirSync7(localProjects)) {
3091
3158
  if (dir.endsWith(TMP_SUFFIX)) continue;
3092
3159
  const logical = reverse.get(dir);
3093
3160
  if (!logical) {
@@ -3133,7 +3200,7 @@ function buildScanTree(tmpRoot) {
3133
3200
  }
3134
3201
  const localProjects = join24(claudeHome(), "projects");
3135
3202
  if (!existsSync19(localProjects)) return { logicalToEncoded, staged, malformed: false };
3136
- for (const dir of readdirSync7(localProjects)) {
3203
+ for (const dir of readdirSync8(localProjects)) {
3137
3204
  const logical = reverse.get(dir);
3138
3205
  if (!logical) continue;
3139
3206
  copyDirJsonlOnly(join24(localProjects, dir), join24(tmpRoot, "shared", "projects", logical));
@@ -3192,7 +3259,7 @@ function reportCheckShared(section2, gitleaksReady) {
3192
3259
 
3193
3260
  // src/commands.doctor.checks.hooks.scope.ts
3194
3261
  init_color();
3195
- import { existsSync as existsSync20, readFileSync as readFileSync6, readdirSync as readdirSync8, realpathSync as realpathSync2 } from "node:fs";
3262
+ import { existsSync as existsSync20, readFileSync as readFileSync6, readdirSync as readdirSync9, realpathSync as realpathSync2 } from "node:fs";
3196
3263
  import { dirname as dirname3, extname, join as join25 } from "node:path";
3197
3264
  init_config();
3198
3265
  function typeFromPackageJson(pkgPath) {
@@ -3234,7 +3301,7 @@ function stripCommentsAndStrings(src) {
3234
3301
  function classifySource(src) {
3235
3302
  const code = stripCommentsAndStrings(src);
3236
3303
  const cjs = /\brequire\s*\(/.test(code) || /\bmodule\.exports\b/.test(code) || /\bexports\.\w/.test(code);
3237
- const esm = /^[^\S\r\n]*import\s/m.test(code) || /^[^\S\r\n]*export\s/m.test(code) || /\bimport\.meta\b/.test(code);
3304
+ const esm = /^[ \t]*import\s/m.test(code) || /^[ \t]*export\s/m.test(code) || /\bimport\.meta\b/.test(code);
3238
3305
  if (cjs && !esm) return "cjs";
3239
3306
  if (esm && !cjs) return "esm";
3240
3307
  if (cjs && esm) return "cjs";
@@ -3245,7 +3312,7 @@ function remedy(family) {
3245
3312
  }
3246
3313
  function safeReaddir2(dir) {
3247
3314
  try {
3248
- return readdirSync8(dir);
3315
+ return readdirSync9(dir);
3249
3316
  } catch {
3250
3317
  return [];
3251
3318
  }
@@ -3293,8 +3360,12 @@ function expandHome(token) {
3293
3360
  const h2 = home();
3294
3361
  return token.replace(/^\$\{HOME\}/, h2).replace(/^\$HOME/, h2).replace(/^~/, h2);
3295
3362
  }
3363
+ var TRAILING_SHELL_PUNCT = /* @__PURE__ */ new Set(["'", '"', "`", ";", ")", "|", "&", ">"]);
3296
3364
  function stripShellPunctuation(token) {
3297
- return token.replace(/^['"]+/, "").replace(/['"`;)|&>]+$/, "");
3365
+ const stripped = token.replace(/^['"]+/, "");
3366
+ let end = stripped.length;
3367
+ while (end > 0 && TRAILING_SHELL_PUNCT.has(stripped[end - 1])) end--;
3368
+ return stripped.slice(0, end);
3298
3369
  }
3299
3370
  function* claudePathsIn(command) {
3300
3371
  const claudePrefix = `${claudeHome()}/`;
@@ -3811,7 +3882,7 @@ import { existsSync as existsSync26, statSync as statSync6 } from "node:fs";
3811
3882
  import { dirname as dirname5, join as join31 } from "node:path";
3812
3883
 
3813
3884
  // src/commands.redact.subtree.ts
3814
- import { existsSync as existsSync25, lstatSync as lstatSync8, readFileSync as readFileSync11, readdirSync as readdirSync9, statSync as statSync5, writeFileSync as writeFileSync4 } from "node:fs";
3885
+ import { existsSync as existsSync25, lstatSync as lstatSync8, readFileSync as readFileSync11, readdirSync as readdirSync10, statSync as statSync5, writeFileSync as writeFileSync4 } from "node:fs";
3815
3886
  import { join as join30 } from "node:path";
3816
3887
  init_utils_fs();
3817
3888
  init_utils();
@@ -3819,7 +3890,7 @@ function collectFiles(dir, out) {
3819
3890
  if (!existsSync25(dir)) return;
3820
3891
  const st = lstatSync8(dir);
3821
3892
  if (!st.isDirectory()) return;
3822
- for (const entry of readdirSync9(dir)) {
3893
+ for (const entry of readdirSync10(dir)) {
3823
3894
  const abs = join30(dir, entry);
3824
3895
  const lst = lstatSync8(abs);
3825
3896
  if (lst.isSymbolicLink()) continue;
@@ -4580,7 +4651,7 @@ function reportOptionalDeps(section2, run = execFileSync12) {
4580
4651
  } else {
4581
4652
  addItem(
4582
4653
  section2,
4583
- `${yellow(warnGlyph)} gh: not installed (optional; needed for nomad init Actions auto-disable + the Actions-drift check)`
4654
+ `${yellow(warnGlyph)} gh: not installed (optional; nomad init needs it to create the sync repo and auto-disable Actions, and the Actions-drift check uses it)`
4584
4655
  );
4585
4656
  }
4586
4657
  reportFetcherRow(section2, run);
@@ -4736,6 +4807,7 @@ function compactSections(sections) {
4736
4807
  function gatherDoctorSections(opts) {
4737
4808
  const host = section("Environment");
4738
4809
  reportHostAndPaths(host);
4810
+ reportHostKeyAlignment(host);
4739
4811
  reportRepoState(host);
4740
4812
  const links = section("Shared links");
4741
4813
  const mapPath = join36(repoHome(), "path-map.json");
@@ -4836,7 +4908,7 @@ function parseDoctorArgs(args) {
4836
4908
  // src/commands.drop-session.ts
4837
4909
  init_config();
4838
4910
  import { execFileSync as execFileSync16 } from "node:child_process";
4839
- import { existsSync as existsSync32, readdirSync as readdirSync10, statSync as statSync8 } from "node:fs";
4911
+ import { existsSync as existsSync32, readdirSync as readdirSync11, statSync as statSync8 } from "node:fs";
4840
4912
  import { join as join38, relative as relative4 } from "node:path";
4841
4913
 
4842
4914
  // src/commands.drop-session.git.ts
@@ -4949,7 +5021,7 @@ function cmdDropSession(id) {
4949
5021
  }
4950
5022
  function collectMatches(repoProjects, id, repo) {
4951
5023
  const matches = [];
4952
- for (const logical of readdirSync10(repoProjects)) {
5024
+ for (const logical of readdirSync11(repoProjects)) {
4953
5025
  const candidate = join38(repoProjects, logical, `${id}.jsonl`);
4954
5026
  if (existsSync32(candidate)) {
4955
5027
  matches.push(relative4(repo, candidate));
@@ -5086,12 +5158,12 @@ init_config();
5086
5158
 
5087
5159
  // src/extras-sync.ts
5088
5160
  init_config();
5089
- import { existsSync as existsSync35 } from "node:fs";
5161
+ import { existsSync as existsSync35, statSync as statSync9 } from "node:fs";
5090
5162
  import { join as join42 } from "node:path";
5091
5163
 
5092
5164
  // src/extras-sync.core.ts
5093
5165
  init_config();
5094
- import { cpSync as cpSync6, existsSync as existsSync33, lstatSync as lstatSync9, readdirSync as readdirSync11, rmSync as rmSync11 } from "node:fs";
5166
+ import { cpSync as cpSync6, existsSync as existsSync33, lstatSync as lstatSync9, readdirSync as readdirSync12, rmSync as rmSync11 } from "node:fs";
5095
5167
  import { basename, join as join39 } from "node:path";
5096
5168
  init_utils();
5097
5169
  init_utils_json();
@@ -5132,7 +5204,7 @@ function* eachExtrasTarget(v, counts) {
5132
5204
  }
5133
5205
  function stripCollidingDstSymlinks(src, dst, isExcluded) {
5134
5206
  if (!existsSync33(dst)) return;
5135
- for (const name of readdirSync11(src)) {
5207
+ for (const name of readdirSync12(src)) {
5136
5208
  if (isExcluded(name)) continue;
5137
5209
  const dstPath = join39(dst, name);
5138
5210
  const dstStat = lstatSync9(dstPath, { throwIfNoEntry: false });
@@ -5180,7 +5252,7 @@ function copyExtrasFiltered(src, dst, blockSet) {
5180
5252
  });
5181
5253
  }
5182
5254
  function prunePreservingDenied(src, dst, blockSet) {
5183
- for (const name of readdirSync11(dst)) {
5255
+ for (const name of readdirSync12(dst)) {
5184
5256
  if (isDeniedName(blockSet, name)) continue;
5185
5257
  const dstPath = join39(dst, name);
5186
5258
  const srcStat = lstatSync9(join39(src, name), { throwIfNoEntry: false });
@@ -5211,7 +5283,7 @@ function copyExtrasFilteredPreserving(src, dst, blockSet) {
5211
5283
  });
5212
5284
  }
5213
5285
  function prunePreservingBy(src, dst, isPreserved) {
5214
- for (const name of readdirSync11(dst)) {
5286
+ for (const name of readdirSync12(dst)) {
5215
5287
  if (isPreserved(name)) continue;
5216
5288
  const dstPath = join39(dst, name);
5217
5289
  const srcStat = lstatSync9(join39(src, name), { throwIfNoEntry: false });
@@ -5248,7 +5320,7 @@ init_utils_json();
5248
5320
 
5249
5321
  // src/extras-sync.remap.ts
5250
5322
  init_config();
5251
- import { existsSync as existsSync34, mkdirSync as mkdirSync7, readdirSync as readdirSync12, realpathSync as realpathSync4, rmSync as rmSync12 } from "node:fs";
5323
+ import { existsSync as existsSync34, mkdirSync as mkdirSync7, readdirSync as readdirSync13, realpathSync as realpathSync4, rmSync as rmSync12 } from "node:fs";
5252
5324
  import { dirname as dirname7, join as join41, sep as sep7 } from "node:path";
5253
5325
 
5254
5326
  // src/extras-sync.planning-diff.ts
@@ -5349,7 +5421,7 @@ function pruneEmptyAncestors(target, planningRoot) {
5349
5421
  let dir = dirname7(target);
5350
5422
  while (dir !== planningRoot && dir.startsWith(planningRoot + sep7)) {
5351
5423
  try {
5352
- if (readdirSync12(dir).length > 0) break;
5424
+ if (readdirSync13(dir).length > 0) break;
5353
5425
  rmSync12(dir, { recursive: true, force: true });
5354
5426
  } catch {
5355
5427
  break;
@@ -5482,6 +5554,15 @@ function remapExtrasPull(ts, opts = {}) {
5482
5554
  }
5483
5555
 
5484
5556
  // src/extras-sync.ts
5557
+ function divergenceWarnLine(o) {
5558
+ const kind = o.isDir ? "folder" : "file";
5559
+ const name = o.isDir ? `${o.dirname}/` : o.dirname;
5560
+ const one = o.count === 1;
5561
+ const fileCount = one ? "1 file" : `${o.count} files`;
5562
+ const them = one ? "it" : "them";
5563
+ const yours = one ? "your current file is" : "your current files are";
5564
+ return `local ${kind} ${name} in repo ${o.logical} differs from the synced copy in ${fileCount}; the next pull step will overwrite ${them} with the synced version (${yours} backed up to ${o.projectBackupRoot}/)`;
5565
+ }
5485
5566
  function divergenceCheckExtras(ts) {
5486
5567
  const v = loadValidatedExtras({});
5487
5568
  if (v === null) return;
@@ -5496,7 +5577,13 @@ function divergenceCheckExtras(ts) {
5496
5577
  if (diff.length === 0) continue;
5497
5578
  const projectBackupRoot = join42(backupRoot, encodePath(localRoot));
5498
5579
  warn(
5499
- `local ${dirname8} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will merge changes (.planning overlays, .claude/.CLAUDE.md mirror; backups at ${projectBackupRoot}/)`
5580
+ divergenceWarnLine({
5581
+ dirname: dirname8,
5582
+ logical,
5583
+ isDir: statSync9(local).isDirectory(),
5584
+ count: diff.length,
5585
+ projectBackupRoot
5586
+ })
5500
5587
  );
5501
5588
  for (const f of diff) warn(` ${f}`);
5502
5589
  }
@@ -5504,7 +5591,7 @@ function divergenceCheckExtras(ts) {
5504
5591
 
5505
5592
  // src/skills-sync.ts
5506
5593
  init_config();
5507
- import { existsSync as existsSync36, lstatSync as lstatSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync13, rmSync as rmSync13 } from "node:fs";
5594
+ import { existsSync as existsSync36, lstatSync as lstatSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync14, rmSync as rmSync13 } from "node:fs";
5508
5595
  import { join as join43 } from "node:path";
5509
5596
  init_utils_fs();
5510
5597
  function isGsdOwned(name) {
@@ -5514,7 +5601,7 @@ function isSkillExcluded(name) {
5514
5601
  return isGsdOwned(name) || isDeniedName(ALWAYS_NEVER_SYNC, name);
5515
5602
  }
5516
5603
  function copySkillsPush(src, dst) {
5517
- const srcNames = readdirSync13(src, { encoding: "utf8" });
5604
+ const srcNames = readdirSync14(src, { encoding: "utf8" });
5518
5605
  const blockSet = /* @__PURE__ */ new Set([
5519
5606
  ...srcNames.filter((n) => isGsdOwned(n)),
5520
5607
  ...ALWAYS_NEVER_SYNC
@@ -6316,7 +6403,7 @@ init_color();
6316
6403
  init_config();
6317
6404
  init_config_sharedDirs_guard();
6318
6405
  import { randomBytes as randomBytes2 } from "node:crypto";
6319
- import { copyFileSync, existsSync as existsSync39, mkdirSync as mkdirSync10, readdirSync as readdirSync14, rmSync as rmSync14 } from "node:fs";
6406
+ import { copyFileSync, existsSync as existsSync39, mkdirSync as mkdirSync10, readdirSync as readdirSync15, rmSync as rmSync14 } from "node:fs";
6320
6407
  import { homedir as homedir5 } from "node:os";
6321
6408
  import { join as join46 } from "node:path";
6322
6409
  init_push_leak_verdict();
@@ -6336,7 +6423,7 @@ function stageSessions(tmpRoot, map) {
6336
6423
  const localProjects = join46(claudeHome(), "projects");
6337
6424
  if (!existsSync39(localProjects)) return 0;
6338
6425
  let staged = 0;
6339
- for (const dir of readdirSync14(localProjects)) {
6426
+ for (const dir of readdirSync15(localProjects)) {
6340
6427
  const logical = reverse.get(dir);
6341
6428
  if (!logical) continue;
6342
6429
  copyDirJsonlOnly(join46(localProjects, dir), join46(tmpRoot, "shared", "projects", logical));
@@ -6427,9 +6514,9 @@ function reportSettingsAheadDrift(repo) {
6427
6514
  const { ahead } = classifySettingsDrift(merged, settings);
6428
6515
  const { promotable } = partitionByCaptureExclusion(ahead);
6429
6516
  if (promotable.length === 0) return;
6430
- const keys = JSON.stringify(promotable);
6517
+ const { phrase, pronoun, verb } = describeSettings(promotable);
6431
6518
  warn(
6432
- `settings.json has local-only keys ${keys} not in the repo. Run 'nomad capture-settings' to promote them (or 'nomad capture-settings --host' for host-specific values).`
6519
+ `your settings.json has ${phrase} that ${verb} not yet in the repo; run 'nomad capture-settings' to save ${pronoun} (or 'nomad capture-settings --host' for host-specific values).`
6433
6520
  );
6434
6521
  } catch {
6435
6522
  }
@@ -6474,7 +6561,7 @@ function runDryRunPreview(st, map, repo) {
6474
6561
  renderNoScanTree(st, { noMapHint: true });
6475
6562
  return;
6476
6563
  }
6477
- const verdict = previewPushLeaks(map);
6564
+ const verdict = withSpinner("Scanning for secrets", () => previewPushLeaks(map));
6478
6565
  renderPushTree(st, verdict);
6479
6566
  if (verdict.recovery !== null) fail(verdict.recovery);
6480
6567
  }
@@ -6697,7 +6784,7 @@ init_config();
6697
6784
  init_utils();
6698
6785
  init_utils_fs();
6699
6786
  init_utils_json();
6700
- import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync42, rmSync as rmSync15, statSync as statSync9 } from "node:fs";
6787
+ import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync42, rmSync as rmSync15, statSync as statSync10 } from "node:fs";
6701
6788
  import { join as join49 } from "node:path";
6702
6789
  function snapshotIntoShared(map) {
6703
6790
  const repo = repoHome();
@@ -6706,7 +6793,7 @@ function snapshotIntoShared(map) {
6706
6793
  const src = join49(claude, name);
6707
6794
  if (!existsSync42(src)) continue;
6708
6795
  const dst = join49(repo, "shared", name);
6709
- if (statSync9(src).isDirectory()) {
6796
+ if (statSync10(src).isDirectory()) {
6710
6797
  const gk = join49(dst, ".gitkeep");
6711
6798
  if (existsSync42(gk)) rmSync15(gk);
6712
6799
  cpSync7(src, dst, { recursive: true, force: false, errorOnExist: true });
@@ -6747,6 +6834,9 @@ function preflightConflict(repoHome2) {
6747
6834
  }
6748
6835
  return null;
6749
6836
  }
6837
+ function isAlreadyInitialized(repoHome2) {
6838
+ return preflightConflict(repoHome2) !== null;
6839
+ }
6750
6840
  function cmdInit(opts = {}) {
6751
6841
  const snapshot = opts.snapshot === true;
6752
6842
  const keepActions = opts.keepActions === true;
@@ -6839,6 +6929,47 @@ function maybeDisableRepoActions(repoHome2, run) {
6839
6929
  }
6840
6930
  }
6841
6931
 
6932
+ // src/init.prompt.ts
6933
+ init_config();
6934
+ init_utils();
6935
+ import { existsSync as existsSync44, readdirSync as readdirSync16, statSync as statSync11 } from "node:fs";
6936
+ import { join as join51 } from "node:path";
6937
+ import { createInterface as createInterface3 } from "node:readline/promises";
6938
+ function nonEmptyExists(path) {
6939
+ if (!existsSync44(path)) return false;
6940
+ try {
6941
+ if (statSync11(path).isDirectory()) return readdirSync16(path).length > 0;
6942
+ return true;
6943
+ } catch {
6944
+ return false;
6945
+ }
6946
+ }
6947
+ function hasExistingClaudeConfig(claudeHome2) {
6948
+ if (existsSync44(join51(claudeHome2, "settings.json"))) return true;
6949
+ return SHARED_LINKS.some((name) => nonEmptyExists(join51(claudeHome2, name)));
6950
+ }
6951
+ async function confirmSnapshotDefault(claudeHome2) {
6952
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
6953
+ log(
6954
+ `tip: ${claudeHome2} already has config; re-run 'nomad init --snapshot' to seed the repo from it.`
6955
+ );
6956
+ return false;
6957
+ }
6958
+ log(`Found existing config in ${claudeHome2}.`);
6959
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
6960
+ try {
6961
+ const answer = await rl.question("Seed the new repo from it? [Y/n] ");
6962
+ return !/^n(o)?$/i.test(answer.trim());
6963
+ } finally {
6964
+ rl.close();
6965
+ }
6966
+ }
6967
+ async function resolveSnapshotChoice(flagSnapshot, claudeHome2, confirm = confirmSnapshotDefault) {
6968
+ if (flagSnapshot) return true;
6969
+ if (!hasExistingClaudeConfig(claudeHome2)) return false;
6970
+ return confirm(claudeHome2);
6971
+ }
6972
+
6842
6973
  // src/nomad.dispatch.helpers.ts
6843
6974
  var REJECT = { ok: false, advance: 0 };
6844
6975
  function applyBool(seen, set) {
@@ -7098,7 +7229,7 @@ function parsePushArgs(argv) {
7098
7229
  // package.json
7099
7230
  var package_default = {
7100
7231
  name: "claude-nomad",
7101
- version: "0.54.0",
7232
+ version: "0.55.1",
7102
7233
  type: "module",
7103
7234
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
7104
7235
  keywords: [
@@ -7230,7 +7361,8 @@ var DEFAULT_HELP = [
7230
7361
  " init",
7231
7362
  "Create a private GitHub repo via gh (if none exists), scaffold shared/, hosts/, path-map."
7232
7363
  ),
7233
- row(" --snapshot", "Overlay the current ~/.claude/ into shared/ as the initial seed."),
7364
+ row(" --snapshot", "Seed shared/ from the current ~/.claude/. Without this flag, init"),
7365
+ cont("offers the same seed interactively when it detects existing config."),
7234
7366
  row(" --keep-actions", "Skip auto-disabling GitHub Actions on the private repo."),
7235
7367
  row(
7236
7368
  " --repo <name>",
@@ -7308,7 +7440,8 @@ var DEFAULT_HELP = [
7308
7440
  "",
7309
7441
  row(" --version", "Print the installed CLI version as bare semver to stdout; exits 0."),
7310
7442
  "",
7311
- "Run `nomad doctor` to validate your setup. Edit shared/ or hosts/<HOST>.json",
7443
+ "You edit the sync repo (~/claude-nomad/); nomad regenerates ~/.claude/ on every",
7444
+ "pull. Run `nomad doctor` to validate your setup. Edit shared/ or hosts/<HOST>.json",
7312
7445
  "in the repo, never ~/.claude/settings.json directly (it is regenerated on",
7313
7446
  "every pull)."
7314
7447
  ].join("\n");
@@ -7317,15 +7450,15 @@ var DEFAULT_HELP = [
7317
7450
  init_config();
7318
7451
  init_utils();
7319
7452
  init_utils_json();
7320
- import { existsSync as existsSync44, readFileSync as readFileSync14, readdirSync as readdirSync15 } from "node:fs";
7321
- import { join as join51 } from "node:path";
7453
+ import { existsSync as existsSync45, readFileSync as readFileSync14, readdirSync as readdirSync17 } from "node:fs";
7454
+ import { join as join52 } from "node:path";
7322
7455
  function resumeCmd(sessionId) {
7323
7456
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
7324
7457
  fail(`invalid session id: ${sessionId}`);
7325
7458
  process.exit(1);
7326
7459
  }
7327
- const projectsRoot = join51(claudeHome(), "projects");
7328
- if (!existsSync44(projectsRoot)) {
7460
+ const projectsRoot = join52(claudeHome(), "projects");
7461
+ if (!existsSync45(projectsRoot)) {
7329
7462
  fail(`${projectsRoot} does not exist`);
7330
7463
  process.exit(1);
7331
7464
  }
@@ -7339,8 +7472,8 @@ function resumeCmd(sessionId) {
7339
7472
  fail(`no cwd field found in ${jsonlPath}`);
7340
7473
  process.exit(1);
7341
7474
  }
7342
- const mapPath = join51(repoHome(), "path-map.json");
7343
- if (!existsSync44(mapPath)) {
7475
+ const mapPath = join52(repoHome(), "path-map.json");
7476
+ if (!existsSync45(mapPath)) {
7344
7477
  fail("path-map.json missing");
7345
7478
  process.exit(1);
7346
7479
  }
@@ -7362,9 +7495,9 @@ function resumeCmd(sessionId) {
7362
7495
  console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
7363
7496
  }
7364
7497
  function findTranscriptPath(projectsRoot, sessionId) {
7365
- for (const dir of readdirSync15(projectsRoot)) {
7366
- const candidate = join51(projectsRoot, dir, `${sessionId}.jsonl`);
7367
- if (existsSync44(candidate)) return candidate;
7498
+ for (const dir of readdirSync17(projectsRoot)) {
7499
+ const candidate = join52(projectsRoot, dir, `${sessionId}.jsonl`);
7500
+ if (existsSync45(candidate)) return candidate;
7368
7501
  }
7369
7502
  return null;
7370
7503
  }
@@ -7447,8 +7580,9 @@ try {
7447
7580
  console.error("usage: nomad init [--snapshot] [--keep-actions] [--repo <name>]");
7448
7581
  process.exit(1);
7449
7582
  }
7583
+ const snapshot = isAlreadyInitialized(repoHome()) ? initArgs.snapshot : await resolveSnapshotChoice(initArgs.snapshot, claudeHome());
7450
7584
  cmdInit({
7451
- snapshot: initArgs.snapshot,
7585
+ snapshot,
7452
7586
  keepActions: initArgs.keepActions,
7453
7587
  repoName: initArgs.repoName
7454
7588
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-nomad",
3
- "version": "0.54.0",
3
+ "version": "0.55.1",
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": [