claude-nomad 0.67.2 → 0.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,74 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.68.0](https://github.com/funkadelic/claude-nomad/compare/v0.67.3...v0.68.0) (2026-09-03)
4
+
5
+
6
+ ### What's new
7
+
8
+ **On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and this does not apply to it.
9
+
10
+ - **A never-sync warning during `nomad pull` now suggests commands you can paste as they are.** Before a pull, nomad warns when a file that should never travel between machines turns up in your sync repo, and the warning names the git commands to deal with it. A filename containing a space split the suggested command in two, so pasting it acted on neither half. The paths are now quoted.
11
+
12
+ **On every platform.**
13
+
14
+ - **`nomad doctor` now warns when your sync repo is carrying a file that should never sync**, such as a credential or a per-machine settings file. Until now one of those was only noticed while it was new or changed, so a file that was committed and then left alone went unmentioned, and the check that did look ran only on Windows. Doctor names the file and the command that stops tracking it without deleting your copy. It only warns.
15
+ - **`nomad adopt` no longer fails on a deeply nested folder.** Adopting a shared folder scans it first, and a very deep tree could crash that scan. The scan handles any depth now.
16
+ - **A backup nomad cannot write is now reported as a plain error.** Nomad copies anything it is about to overwrite into a backup first. When that copy fails, say a full disk or a permissions problem, you get one message naming what it was copying, where it was going, and why it failed, instead of a crash report file. The message also says a partial copy may be left behind.
17
+
18
+ Everything else in this release is dependency updates.
19
+
20
+
21
+ ### Added
22
+
23
+ * **doctor:** warn when the sync repo tracks a never-synced file ([#562](https://github.com/funkadelic/claude-nomad/issues/562)) ([a1b3f71](https://github.com/funkadelic/claude-nomad/commit/a1b3f71a0a3deecac81e0bcc9e8ee214c1e50a4e))
24
+
25
+
26
+ ### Fixed
27
+
28
+ * **adopt:** handle deep directories and report a failed backup ([#560](https://github.com/funkadelic/claude-nomad/issues/560)) ([2b19ccb](https://github.com/funkadelic/claude-nomad/commit/2b19ccba5959efb7b42d16ee0db862df4d92a0ac))
29
+ * **backup:** report a failed backup instead of writing a crash report ([#561](https://github.com/funkadelic/claude-nomad/issues/561)) ([9d8484c](https://github.com/funkadelic/claude-nomad/commit/9d8484cdb59eb5a5905dfb0a3e16ae952acead5f))
30
+ * **pull:** quote file paths in the never-sync warning's suggested commands ([#559](https://github.com/funkadelic/claude-nomad/issues/559)) ([070cc17](https://github.com/funkadelic/claude-nomad/commit/070cc1709263d46cc50d01e99c345b3955d729ed))
31
+
32
+
33
+ ### Dependencies
34
+
35
+ * bump fast-uri from 3.1.5 to 3.1.7 ([#563](https://github.com/funkadelic/claude-nomad/issues/563)) ([7bf62e3](https://github.com/funkadelic/claude-nomad/commit/7bf62e3e481246b7c8125e31fb2df6654241dd54))
36
+ * bump the codeql-action group across 1 directory with 2 updates ([#556](https://github.com/funkadelic/claude-nomad/issues/556)) ([ad3ae73](https://github.com/funkadelic/claude-nomad/commit/ad3ae739043ae24f560d0ddd56baff79bb6b6ad6))
37
+ * bump the dev-dependencies group across 1 directory with 3 updates ([#555](https://github.com/funkadelic/claude-nomad/issues/555)) ([02aebe2](https://github.com/funkadelic/claude-nomad/commit/02aebe2a6407dcd4be67fe9e33723c735a1c44cd))
38
+ * bump the prod-dependencies group in /docs-site with 3 updates ([#557](https://github.com/funkadelic/claude-nomad/issues/557)) ([b6b5c8a](https://github.com/funkadelic/claude-nomad/commit/b6b5c8aebebcc8e4c73ac12d511bc972bda553d7))
39
+
40
+ ## [0.67.3](https://github.com/funkadelic/claude-nomad/compare/v0.67.2...v0.67.3) (2026-08-29)
41
+
42
+ ### What's new
43
+
44
+ **On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and this does not apply to it.
45
+
46
+ - **`nomad pull` now warns about a file that should never be synced, even when your gitignore hides it.** Before a pull, nomad checks your sync repo for files that have no business travelling between machines, such as credentials or per-host settings, and warns when one turns up under `shared/`. A file covered by a gitignore rule was invisible to that check, so it could sit there unmentioned. Nomad now names the file, along with the `git check-ignore` command that tells you which rule hid it, which is often a rule in your global git config rather than anything you wrote for this repo. It only warns. Nothing is deleted, because nomad cannot publish a file git ignores, so there is nothing to undo.
47
+
48
+ The rest of this release is internal: a regression test for the Windows `nomad sync` path, a stricter check on the test suite's own mock handling, and test-result reporting in CI. Nothing else changes what nomad does.
49
+
50
+
51
+
52
+ ### Fixed
53
+
54
+ * **pull:** report gitignored denylisted paths under shared/ ([#553](https://github.com/funkadelic/claude-nomad/issues/553)) ([b226902](https://github.com/funkadelic/claude-nomad/commit/b226902e4361a3325a2fd0495e5abbd532f7d806))
55
+
56
+
57
+ ### Changed
58
+
59
+ * report test results to Codecov Test Analytics ([#551](https://github.com/funkadelic/claude-nomad/issues/551)) ([a1fb555](https://github.com/funkadelic/claude-nomad/commit/a1fb5558185e6a97a8a026eeb05d3ef6a9834f28))
60
+
61
+
62
+ ### Documentation
63
+
64
+ * clean up the documentation prose ([#549](https://github.com/funkadelic/claude-nomad/issues/549)) ([cd8c40d](https://github.com/funkadelic/claude-nomad/commit/cd8c40d2f2877a868876e9ab338f4884060e90d9))
65
+
66
+
67
+ ### Testing
68
+
69
+ * scope the doMock/doUnmock symmetry guard to every describe ([#552](https://github.com/funkadelic/claude-nomad/issues/552)) ([ebe4af9](https://github.com/funkadelic/claude-nomad/commit/ebe4af988fed37b9ced1a3a42dd736fb1901c888))
70
+ * **sync:** add a cross-platform parity journey for nomad sync ([f87464d](https://github.com/funkadelic/claude-nomad/commit/f87464dcf7bf23341f1b8b8d4ae2eaefe0cc4e08))
71
+
3
72
  ## [0.67.2](https://github.com/funkadelic/claude-nomad/compare/v0.67.1...v0.67.2) (2026-08-24)
4
73
 
5
74
  ### What's new
@@ -528,14 +597,7 @@ All of this applies on macOS, Linux, WSL2, and native Windows.
528
597
 
529
598
  * **pull:** retain unpushed local work and surface it in the preview ([#376](https://github.com/funkadelic/claude-nomad/issues/376)) ([3f70019](https://github.com/funkadelic/claude-nomad/commit/3f70019c89ccd77726e6330d9623e106bb9ff69c))
530
599
 
531
- What this means: `nomad pull` no longer discards work you have not pushed
532
- yet. Chat sessions, subagent history, and memory files that exist only on
533
- this machine now survive a pull, and a synced file you have edited locally
534
- (a project's `.planning` notes or its `CLAUDE.md`) is kept instead of being
535
- overwritten when it differs from the incoming copy, with a note to push and
536
- reconcile. `nomad diff` and `pull --dry-run` now show the count of retained
537
- local-only items and the keep-local warning, where before they reported
538
- `clean` / `0 collisions`.
600
+ What this means: `nomad pull` no longer discards work you have not pushed yet. Chat sessions, subagent history, and memory files that exist only on this machine now survive a pull, and a synced file you have edited locally (a project's `.planning` notes or its `CLAUDE.md`) is kept instead of being overwritten when it differs from the incoming copy, with a note to push and reconcile. `nomad diff` and `pull --dry-run` now show the count of retained local-only items and the keep-local warning, where before they reported `clean` / `0 collisions`.
539
601
 
540
602
  ## [0.56.1](https://github.com/funkadelic/claude-nomad/compare/v0.56.0...v0.56.1) (2026-06-30)
541
603
 
package/README.md CHANGED
@@ -17,8 +17,8 @@ private Git repo you control. Run `nomad sync` on any machine and everything is
17
17
  included; it pulls in your latest config first, then publishes your local changes, so you never have
18
18
  to remember which one to run first.
19
19
 
20
- Not dotfiles, not rsync. **claude-nomad** understands Claude Code's state, so your session history
21
- survives different file paths and your secrets never ride along.
20
+ Dotfiles managers and rsync copy files. **claude-nomad** understands Claude Code's state, so your
21
+ session history survives different file paths and your secrets never ride along.
22
22
 
23
23
  **Full documentation: <https://funkadelic.github.io/claude-nomad/>**
24
24
 
@@ -54,18 +54,20 @@ survives different file paths and your secrets never ride along.
54
54
  - **One command tells you what is wrong.** `nomad doctor` is a read-only health check: wedged sync
55
55
  repo, broken hook references, hooks that would crash on session start because of a missing
56
56
  `--preserve-symlinks-main` flag, version drift, oversized backup cache, missing git committer
57
- identity in the sync repo (a push fails at commit time without one), path-map entries whose local
58
- project folder no longer exists on this machine, a multi-host repo where this machine's
59
- hostname-derived key matches no `hosts/<HOST>.json` or path-map entry (a sign `NOMAD_HOST` is
60
- unset here, so per-host settings and session sync will not line up with the other hosts), synced
61
- skills with local edits that differ from the shared copy, a native Windows shared-config copy that
62
- has drifted from the repo's copy (a warning, not a failure), and settings drift in both
63
- directions: keys present in the repo merge but absent from your live `settings.json` (behind; the
64
- next `nomad pull` will restore them, fix: `nomad pull`) and keys present locally but not yet in
65
- the repo (ahead; local-only additions, fix: `nomad capture-settings`). Each issue includes a fix
66
- hint. By default the report is compact: it shows only checks that need action plus a one-line
67
- verdict. Add `--verbose` (or `--all` / `-v`) to see the full per-check tree, including everything
68
- that passed.
57
+ identity in the sync repo (a push fails at commit time without one), a never-synced file such as a
58
+ credential or a per-host settings file that Git already tracks inside the repo's `shared/` folder
59
+ (once it is committed the pull-side guard can no longer see it, so doctor names the file and the
60
+ folder name that blocks it), path-map entries whose local project folder no longer exists on this
61
+ machine, a multi-host repo where this machine's hostname-derived key matches no
62
+ `hosts/<HOST>.json` or path-map entry (a sign `NOMAD_HOST` is unset here, so per-host settings and
63
+ session sync will not line up with the other hosts), synced skills with local edits that differ
64
+ from the shared copy, a native Windows shared-config copy that has drifted from the repo's copy (a
65
+ warning, not a failure), and settings drift in both directions: keys present in the repo merge but
66
+ absent from your live `settings.json` (behind; the next `nomad pull` will restore them, fix:
67
+ `nomad pull`) and keys present locally but not yet in the repo (ahead; local-only additions, fix:
68
+ `nomad capture-settings`). Each issue includes a fix hint. By default the report is compact: it
69
+ shows only checks that need action plus a one-line verdict. Add `--verbose` (or `--all` / `-v`) to
70
+ see the full per-check tree, including everything that passed.
69
71
  - **Self-healing sync.** Every overwrite is backed up first, and `nomad pull --force-remote`
70
72
  recovers two kinds of stuck sync repo: a repo stuck mid-rebase or mid-merge (aborts the operation,
71
73
  parks stranded work on a branch, refuses if shared config is at risk), and a repo where the rebase
@@ -81,7 +83,7 @@ opt-in per-project sync, transcript redaction, backup pruning, and more.
81
83
 
82
84
  ## Quickstart
83
85
 
84
- nomad works with two directories, and the difference is the one thing worth learning up front:
86
+ nomad works with two directories:
85
87
 
86
88
  - **`~/claude-nomad/`** is your private sync repo. This is the one you edit.
87
89
  - **`~/.claude/`** is Claude Code's live config. nomad regenerates it on every `pull`.
@@ -222,18 +224,17 @@ log output remains grep-stable.
222
224
  When `nomad push` detects a potential secret, it drops into an interactive menu (TTY) or aborts with
223
225
  a recovery hint (non-TTY/CI). Three non-interactive recovery paths are available without the menu:
224
226
 
225
- - `nomad push --full-scan` -- ignore the per-host push manifest and rescan all transcripts, then
227
+ - `nomad push --full-scan`: ignore the per-host push manifest and rescan all transcripts, then
226
228
  rewrite the manifest on success. Use this after upgrading gitleaks, after editing a gitleaks
227
229
  config file, or whenever you want to be certain nothing slipped through an incremental scan.
228
230
  Composes freely with `--dry-run` and all resolution modes.
229
- - `nomad push --redact-all` -- scrub every finding from the local transcript in place, then push.
231
+ - `nomad push --redact-all`: scrub every finding from the local transcript in place, then push.
230
232
  All-or-nothing: if any finding cannot be redacted (an active session, or one that does not map to
231
233
  a synced transcript), nothing is changed and the push stops so you can handle those sessions.
232
- - `nomad push --allow <rule>` -- record findings matching one gitleaks rule id as false positives
234
+ - `nomad push --allow <rule>`: record findings matching one gitleaks rule id as false positives
233
235
  (appends their fingerprints to `.gitleaksignore`), then re-scan and push.
234
- - `nomad push --allow-all` -- record every current finding as a false positive, then re-scan and
235
- push.
236
- - `nomad allow <fingerprint>...` -- pre-record specific fingerprints in `.gitleaksignore` without
236
+ - `nomad push --allow-all`: record every current finding as a false positive, then re-scan and push.
237
+ - `nomad allow <fingerprint>...`: pre-record specific fingerprints in `.gitleaksignore` without
237
238
  going through a push cycle.
238
239
 
239
240
  All allow paths always re-scan after writing the allowlist; a surviving finding still aborts the
@@ -314,16 +315,16 @@ false alarm from a concurrent run. Value `3` is reserved for future use.
314
315
 
315
316
  ## Crash reports
316
317
 
317
- If `nomad` ever hits an unexpected bug, it no longer dumps a raw stack trace at you. Instead it
318
- prints a short "this looks like a bug" banner (with a link to the issue tracker) and writes a small
319
- report you can attach to a bug report if you choose. The exit code is unchanged: an unexpected crash
320
- exits `1`, and a documented failure still exits with its own code from the table above. A prompt you
321
- cancel yourself is not a crash: it exits `130` and writes no report.
318
+ If `nomad` ever hits an unexpected bug, it does not dump a raw stack trace. It prints a short "this
319
+ looks like a bug" banner (with a link to the issue tracker) and writes a small report you can attach
320
+ to a bug report if you choose. The exit code is unchanged: an unexpected crash exits `1`, and a
321
+ documented failure still exits with its own code from the table above. A prompt you cancel yourself
322
+ is not a crash: it exits `130` and writes no report.
322
323
 
323
324
  What this means for you:
324
325
 
325
326
  - **The report stays on your machine.** It is written to `~/.cache/claude-nomad/crash/` and nothing
326
- is ever uploaded anywhere. The file just sits there until you decide to share it or delete it.
327
+ is ever uploaded anywhere. The file sits there until you decide to share it or delete it.
327
328
  - **The saved report is scrubbed twice.** First a structural pass rewrites your home directory to
328
329
  `~` and your hostname to a placeholder (absolute paths are personal information a secret scanner
329
330
  would not catch), and redacts credential-shaped tokens it recognizes (GitHub, GitLab, Slack, and
@@ -342,24 +343,24 @@ What this means for you:
342
343
  contents of any file.
343
344
  - **The directory manages itself.** Only the most recent reports are kept; older ones are pruned
344
345
  automatically on the next crash. There is no `nomad clean` flag for the crash directory, by
345
- design: it self-manages.
346
+ design.
346
347
 
347
348
  ## Learn more
348
349
 
349
- - [How it works](https://funkadelic.github.io/claude-nomad/how-it-works/) -- path remapping,
350
- settings merge, what syncs and what doesn't
351
- - [GSD-aware sync](https://funkadelic.github.io/claude-nomad/gsd-aware-sync/) -- what nomad does for
350
+ - [How it works](https://funkadelic.github.io/claude-nomad/how-it-works/): path remapping, settings
351
+ merge, what syncs and what doesn't
352
+ - [GSD-aware sync](https://funkadelic.github.io/claude-nomad/gsd-aware-sync/): what nomad does for
352
353
  GSD users out of the box
353
- - [Setup and migration](https://funkadelic.github.io/claude-nomad/quickstart/) -- full setup
354
+ - [Setup and migration](https://funkadelic.github.io/claude-nomad/quickstart/): full setup
354
355
  walkthrough, migrating an existing `~/.claude/`
355
- - [Recipes](https://funkadelic.github.io/claude-nomad/recipes/) -- copy-pasteable example configs
356
- for common setups, from scratch to cross-OS remapping and GSD integration
357
- - [Commands reference](https://funkadelic.github.io/claude-nomad/commands/) -- all CLI flags
358
- - [Claude Code plugin](https://funkadelic.github.io/claude-nomad/plugin/) -- /nomad slash commands
359
- and the session-start drift check
360
- - [Recovery flows](https://funkadelic.github.io/claude-nomad/recovery/) -- backups, drop-session,
356
+ - [Recipes](https://funkadelic.github.io/claude-nomad/recipes/): copy-pasteable example configs for
357
+ common setups, from scratch to cross-OS remapping and GSD integration
358
+ - [Commands reference](https://funkadelic.github.io/claude-nomad/commands/): all CLI flags
359
+ - [Claude Code plugin](https://funkadelic.github.io/claude-nomad/plugin/): /nomad slash commands and
360
+ the session-start drift check
361
+ - [Recovery flows](https://funkadelic.github.io/claude-nomad/recovery/): backups, drop-session,
361
362
  redact, gitleaks allowlist, non-interactive allow
362
- - [FAQ](https://funkadelic.github.io/claude-nomad/faq/) -- common questions, like the right
363
- push/pull order when both sides have changes
363
+ - [FAQ](https://funkadelic.github.io/claude-nomad/faq/): common questions, like the right push/pull
364
+ order when both sides have changes
364
365
  - [Contributing](https://funkadelic.github.io/claude-nomad/contributing/)
365
366
  - [Security policy](https://funkadelic.github.io/claude-nomad/security/)
package/dist/nomad.mjs CHANGED
@@ -721,6 +721,21 @@ var init_config = __esm({
721
721
  }
722
722
  });
723
723
 
724
+ // src/error-text.ts
725
+ function errorText(err) {
726
+ try {
727
+ const message = err?.message;
728
+ return typeof message === "string" ? message : String(err);
729
+ } catch {
730
+ return "unprintable error value";
731
+ }
732
+ }
733
+ var init_error_text = __esm({
734
+ "src/error-text.ts"() {
735
+ "use strict";
736
+ }
737
+ });
738
+
724
739
  // src/utils.json.ts
725
740
  import { readFileSync } from "node:fs";
726
741
  function readJson(path) {
@@ -886,17 +901,22 @@ function ensureSymlink(linkPath, target) {
886
901
  symlinkSync(target, linkPath);
887
902
  log(`linked ${linkPath} -> ${target}`);
888
903
  }
889
- function backupUnder(absPath, anchor, destRoot) {
904
+ function backupUnder(absPath, anchor, destRoot, hint) {
890
905
  if (!existsSync3(absPath)) return false;
891
906
  const rel = relative3(anchor, absPath);
892
907
  if (rel === "" || rel === ".." || rel.startsWith(`..${sep2}`)) return false;
893
908
  const dst = join5(destRoot, rel);
894
- mkdirSync(dirname(dst), { recursive: true });
895
- cpSync3(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
909
+ try {
910
+ mkdirSync(dirname(dst), { recursive: true });
911
+ cpSync3(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
912
+ } catch (err) {
913
+ const fragment = `could not back up ${absPath} into ${dst} (${errorText(err)}); a partial copy may be left there`;
914
+ throw new NomadFatal(hint === void 0 ? fragment : `${fragment}. ${hint}`);
915
+ }
896
916
  return true;
897
917
  }
898
- function backupBeforeWrite(absPath, ts) {
899
- return backupUnder(absPath, claudeHome(), join5(backupBase(), ts));
918
+ function backupBeforeWrite(absPath, ts, hint) {
919
+ return backupUnder(absPath, claudeHome(), join5(backupBase(), ts), hint);
900
920
  }
901
921
  function backupRepoWrite(absPath, ts, repoHome2) {
902
922
  backupUnder(absPath, repoHome2, join5(backupBase(), ts, "repo"));
@@ -909,6 +929,7 @@ var init_utils_fs = __esm({
909
929
  "src/utils.fs.ts"() {
910
930
  "use strict";
911
931
  init_config();
932
+ init_error_text();
912
933
  init_utils_json();
913
934
  init_utils();
914
935
  RENAME_RETRY_MAX_ATTEMPTS = 5;
@@ -1134,7 +1155,7 @@ var init_autostash_guard = __esm({
1134
1155
  });
1135
1156
 
1136
1157
  // src/push-checks.ts
1137
- import { execFileSync as execFileSync4 } from "node:child_process";
1158
+ import { execFileSync as execFileSync5 } from "node:child_process";
1138
1159
  import { readdirSync as readdirSync8, rmSync as rmSync7 } from "node:fs";
1139
1160
  import { homedir as homedir2, platform } from "node:os";
1140
1161
  import { delimiter, join as join23 } from "node:path";
@@ -1201,7 +1222,7 @@ function probeGitleaks() {
1201
1222
  const args = ["version"];
1202
1223
  if (toml !== null) args.push("--config", toml);
1203
1224
  try {
1204
- return execFileSync4("gitleaks", args, { stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
1225
+ return execFileSync5("gitleaks", args, { stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
1205
1226
  } catch (err) {
1206
1227
  const e = err;
1207
1228
  if (e.code === "ENOENT") throw new NomadFatal(gitleaksInstallHint());
@@ -1221,7 +1242,7 @@ function rebaseBeforePush(repo) {
1221
1242
  const wedge = classifyWedge(repo);
1222
1243
  if (wedge !== null) throw new NomadFatal(wedgePreflight(wedge), { code: EXIT.CONFLICT });
1223
1244
  try {
1224
- execFileSync4("git", ["pull", "--rebase", "--autostash"], {
1245
+ execFileSync5("git", ["pull", "--rebase", "--autostash"], {
1225
1246
  cwd: repo,
1226
1247
  stdio: ["ignore", "pipe", "pipe"]
1227
1248
  });
@@ -1246,7 +1267,7 @@ var init_push_checks = __esm({
1246
1267
  });
1247
1268
 
1248
1269
  // src/push-gitleaks.scan.ts
1249
- import { execFileSync as execFileSync8 } from "node:child_process";
1270
+ import { execFileSync as execFileSync9 } from "node:child_process";
1250
1271
  import { mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, rmSync as rmSync8 } from "node:fs";
1251
1272
  import { homedir as homedir3 } from "node:os";
1252
1273
  import { join as join27 } from "node:path";
@@ -1313,9 +1334,9 @@ function scanStagedTree(repoDir, forwardStreams = false) {
1313
1334
  const opts = { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] };
1314
1335
  const gitleaksOpts = { ...opts, timeout: GITLEAKS_SCAN_TIMEOUT_MS };
1315
1336
  try {
1316
- execFileSync8("git", ["init", "-q"], opts);
1317
- execFileSync8("git", ["add", "-A"], opts);
1318
- execFileSync8("gitleaks", args, gitleaksOpts);
1337
+ execFileSync9("git", ["init", "-q"], opts);
1338
+ execFileSync9("git", ["add", "-A"], opts);
1339
+ execFileSync9("gitleaks", args, gitleaksOpts);
1319
1340
  return [];
1320
1341
  } catch (err) {
1321
1342
  const e = err;
@@ -1348,7 +1369,7 @@ function scanFile(filePath, forwardStreams = false, timeoutMs = GITLEAKS_SCAN_TI
1348
1369
  stdio: ["ignore", "pipe", "pipe"],
1349
1370
  timeout: timeoutMs
1350
1371
  };
1351
- execFileSync8("gitleaks", args, opts);
1372
+ execFileSync9("gitleaks", args, opts);
1352
1373
  return [];
1353
1374
  } catch (err) {
1354
1375
  const e = err;
@@ -1567,43 +1588,37 @@ import { dirname as dirname2, join as join7, resolve as resolve2 } from "node:pa
1567
1588
 
1568
1589
  // src/commands.adopt.scan.ts
1569
1590
  init_config();
1570
- import { lstatSync, readdirSync } from "node:fs";
1571
- import { join as join2, relative } from "node:path";
1572
-
1573
- // src/error-text.ts
1574
- function errorText(err) {
1575
- try {
1576
- const message = err?.message;
1577
- return typeof message === "string" ? message : String(err);
1578
- } catch {
1579
- return "unprintable error value";
1580
- }
1581
- }
1582
-
1583
- // src/commands.adopt.scan.ts
1591
+ init_error_text();
1584
1592
  init_exit_codes();
1585
1593
  init_utils();
1594
+ import { lstatSync, readdirSync } from "node:fs";
1595
+ import { join as join2, relative } from "node:path";
1586
1596
  function toForwardSlash(p) {
1587
1597
  return p.replaceAll("\\", "/");
1588
1598
  }
1589
- function walk(root, dir, out) {
1590
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
1591
- const match = matchDeniedName(ALWAYS_NEVER_SYNC, entry.name);
1592
- if (match !== null) {
1593
- out.push({
1594
- path: toForwardSlash(relative(root, join2(dir, entry.name))),
1595
- matched: match.axis === "name" ? match.entry : null
1596
- });
1597
- continue;
1599
+ function walk(root) {
1600
+ const out = [];
1601
+ const stack = [root];
1602
+ while (stack.length > 0) {
1603
+ const dir = stack.pop();
1604
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
1605
+ const match = matchDeniedName(ALWAYS_NEVER_SYNC, entry.name);
1606
+ if (match !== null) {
1607
+ out.push({
1608
+ path: toForwardSlash(relative(root, join2(dir, entry.name))),
1609
+ matched: match.axis === "name" ? match.entry : null
1610
+ });
1611
+ continue;
1612
+ }
1613
+ if (entry.isDirectory()) stack.push(join2(dir, entry.name));
1598
1614
  }
1599
- if (entry.isDirectory()) walk(root, join2(dir, entry.name), out);
1600
1615
  }
1616
+ return out;
1601
1617
  }
1602
1618
  function scanDeniedEntries(root) {
1603
1619
  const stat = lstatSync(root, { throwIfNoEntry: false });
1604
1620
  if (!stat?.isDirectory()) return [];
1605
- const out = [];
1606
- walk(root, root, out);
1621
+ const out = walk(root);
1607
1622
  out.sort((a, b) => a.path.localeCompare(b.path));
1608
1623
  return out;
1609
1624
  }
@@ -1652,6 +1667,7 @@ function refuseDeniedEntries(name, root) {
1652
1667
 
1653
1668
  // src/commands.adopt.recover.ts
1654
1669
  init_config();
1670
+ init_error_text();
1655
1671
  init_exit_codes();
1656
1672
 
1657
1673
  // src/extras-sync.core.ts
@@ -2689,7 +2705,11 @@ function adoptStopsEarly(name, linkPath, sharedTarget) {
2689
2705
  }
2690
2706
  function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
2691
2707
  const ts = freshBackupTs(backup);
2692
- const snapshotted = backupBeforeWrite(linkPath, ts);
2708
+ const snapshotted = backupBeforeWrite(
2709
+ linkPath,
2710
+ ts,
2711
+ `Nothing was removed from ${linkPath} and nothing was written to the repo. Fix what the error names, then run \`nomad adopt ${name}\` again.`
2712
+ );
2693
2713
  const rel = join9("shared", name);
2694
2714
  const stage = () => gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
2695
2715
  copyIntoSharedOrFatal(name, linkPath, sharedTarget, repo);
@@ -3996,15 +4016,34 @@ function reportSkillsDivergence(section2) {
3996
4016
  // src/commands.doctor.checks.git-state.ts
3997
4017
  init_color();
3998
4018
  init_config();
3999
- import { execFileSync as execFileSync5 } from "node:child_process";
4019
+ import { execFileSync as execFileSync6 } from "node:child_process";
4000
4020
  import { existsSync as existsSync17 } from "node:fs";
4001
4021
  import { join as join24, relative as relative5 } from "node:path";
4002
4022
  init_commands_pull_wedge();
4023
+
4024
+ // src/git-probe.ts
4025
+ import { execFileSync as execFileSync4 } from "node:child_process";
4026
+ var PROBE_TIMEOUT_MS = 1e4;
4027
+ var PROBE_MAX_BUFFER = 64 * 1024 * 1024;
4028
+ function gitProbe(args, repo) {
4029
+ try {
4030
+ return execFileSync4("git", args, {
4031
+ cwd: repo,
4032
+ stdio: ["ignore", "pipe", "pipe"],
4033
+ timeout: PROBE_TIMEOUT_MS,
4034
+ maxBuffer: PROBE_MAX_BUFFER
4035
+ }).toString();
4036
+ } catch {
4037
+ return null;
4038
+ }
4039
+ }
4040
+
4041
+ // src/commands.doctor.checks.git-state.ts
4003
4042
  init_push_checks();
4004
4043
  init_utils();
4005
4044
  function reportGitleaksProbe(section2) {
4006
4045
  try {
4007
- execFileSync5("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
4046
+ execFileSync6("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
4008
4047
  return true;
4009
4048
  } catch (err) {
4010
4049
  if (err.code === "ENOENT") {
@@ -4040,7 +4079,7 @@ function reportGitlinks(section2) {
4040
4079
  }
4041
4080
  function reportRemote(section2) {
4042
4081
  try {
4043
- const url = execFileSync5("git", ["remote", "get-url", "origin"], {
4082
+ const url = execFileSync6("git", ["remote", "get-url", "origin"], {
4044
4083
  cwd: repoHome(),
4045
4084
  stdio: ["ignore", "pipe", "pipe"]
4046
4085
  }).toString().trim();
@@ -4097,7 +4136,7 @@ function reportGitIdentity(section2) {
4097
4136
  const missing = [];
4098
4137
  for (const field of ["user.name", "user.email"]) {
4099
4138
  try {
4100
- const val = execFileSync5("git", ["config", field], {
4139
+ const val = execFileSync6("git", ["config", field], {
4101
4140
  cwd: repo,
4102
4141
  stdio: ["ignore", "pipe", "pipe"]
4103
4142
  }).toString().trim();
@@ -4117,6 +4156,45 @@ function reportGitIdentity(section2) {
4117
4156
  addItem(section2, `${green(okGlyph)} git identity: user.name and user.email configured`);
4118
4157
  }
4119
4158
  }
4159
+ var MAX_TRACKED_DENIED_ROWS = 5;
4160
+ var CONTROL_CHARS = /[\x00-\x1f\x7f-\x9f]/g;
4161
+ function renderable(text) {
4162
+ return text.replace(CONTROL_CHARS, " ");
4163
+ }
4164
+ function reportTrackedDeniedShared(section2) {
4165
+ const out = gitProbe(["ls-files", "-z", "--", ":(icase)shared/"], repoHome());
4166
+ if (out === null) {
4167
+ addItem(
4168
+ section2,
4169
+ `${dim(infoGlyph)} never-sync scan: git could not list shared/, so nothing was scanned`
4170
+ );
4171
+ return;
4172
+ }
4173
+ const hits = /* @__PURE__ */ new Map();
4174
+ for (const path of out.split("\0")) {
4175
+ const segment = path === "" ? null : deniedSegmentFor(path);
4176
+ if (segment !== null) hits.set(path, segment);
4177
+ }
4178
+ if (hits.size === 0) {
4179
+ addItem(
4180
+ section2,
4181
+ `${green(okGlyph)} never-sync scan: no tracked path under shared/ is denylisted`
4182
+ );
4183
+ return;
4184
+ }
4185
+ for (const [path, segment] of [...hits].slice(0, MAX_TRACKED_DENIED_ROWS)) {
4186
+ addItem(
4187
+ section2,
4188
+ `${yellow(warnGlyph)} never-sync: git tracks ${blue(renderable(path))} (the path segment "${renderable(segment)}" is blocked by the never-sync boundary)`
4189
+ );
4190
+ }
4191
+ const overflow = hits.size - MAX_TRACKED_DENIED_ROWS;
4192
+ const more = overflow > 0 ? `${overflow} more not listed, run git ls-files -- ":(icase)shared/" in the sync repo to see them all. ` : "";
4193
+ addItem(
4194
+ section2,
4195
+ `${yellow(warnGlyph)} never-sync: ${more}Run git rm --cached -- "<path>" and commit to stop tracking each one; that leaves the file on disk, so move it outside shared/ if you want to keep it. If one holds a real secret, rotate it and rewrite history too, because nomad only changes your local worktree and index and cannot scrub what a previous push already sent`
4196
+ );
4197
+ }
4120
4198
 
4121
4199
  // src/commands.doctor.checks.backups.ts
4122
4200
  init_color();
@@ -4166,14 +4244,14 @@ function reportBackupsCheck(section2, backupBase2 = backupBase()) {
4166
4244
 
4167
4245
  // src/commands.doctor.check-remote.ts
4168
4246
  init_color();
4169
- import { execFileSync as execFileSync6 } from "node:child_process";
4247
+ import { execFileSync as execFileSync7 } from "node:child_process";
4170
4248
  init_config();
4171
4249
  init_utils_json();
4172
4250
  var GIT_REMOTE_TIMEOUT_MS = 3e3;
4173
4251
  function readRemotePathMap(sec, repo) {
4174
4252
  let rawJson;
4175
4253
  try {
4176
- rawJson = execFileSync6("git", ["show", "origin/main:path-map.json"], {
4254
+ rawJson = execFileSync7("git", ["show", "origin/main:path-map.json"], {
4177
4255
  cwd: repo,
4178
4256
  stdio: ["ignore", "pipe", "pipe"],
4179
4257
  timeout: GIT_REMOTE_TIMEOUT_MS
@@ -4206,7 +4284,7 @@ function reportCheckRemote(section2) {
4206
4284
  const repo = repoHome();
4207
4285
  let names;
4208
4286
  try {
4209
- const out = execFileSync6("git", ["ls-tree", "--name-only", "origin/main"], {
4287
+ const out = execFileSync7("git", ["ls-tree", "--name-only", "origin/main"], {
4210
4288
  cwd: repo,
4211
4289
  stdio: ["ignore", "pipe", "pipe"],
4212
4290
  timeout: GIT_REMOTE_TIMEOUT_MS
@@ -4238,9 +4316,9 @@ import { join as join26 } from "node:path";
4238
4316
  init_config();
4239
4317
 
4240
4318
  // src/http-fetch.ts
4241
- import { execFileSync as execFileSync7 } from "node:child_process";
4319
+ import { execFileSync as execFileSync8 } from "node:child_process";
4242
4320
  var FETCH_TIMEOUT_MS = 3e3;
4243
- function fetchUrl(url, run = execFileSync7) {
4321
+ function fetchUrl(url, run = execFileSync8) {
4244
4322
  try {
4245
4323
  return run("curl", ["-fsSL", "-m", "3", url], {
4246
4324
  stdio: ["ignore", "pipe", "pipe"],
@@ -4301,14 +4379,14 @@ function reportCheckSchema(section2) {
4301
4379
  // src/commands.doctor.check-shared.ts
4302
4380
  init_color();
4303
4381
  import { randomBytes as randomBytes3 } from "node:crypto";
4304
- import { execFileSync as execFileSync11 } from "node:child_process";
4382
+ import { execFileSync as execFileSync12 } from "node:child_process";
4305
4383
  import { existsSync as existsSync22, mkdirSync as mkdirSync8, readdirSync as readdirSync12, rmSync as rmSync12 } from "node:fs";
4306
4384
  import { homedir as homedir6 } from "node:os";
4307
4385
  import { join as join33 } from "node:path";
4308
4386
 
4309
4387
  // src/commands.doctor.check-shared.memory.ts
4310
4388
  init_color();
4311
- import { execFileSync as execFileSync9 } from "node:child_process";
4389
+ import { execFileSync as execFileSync10 } from "node:child_process";
4312
4390
  import { randomBytes } from "node:crypto";
4313
4391
  import { mkdirSync as mkdirSync4, rmSync as rmSync9, writeFileSync as writeFileSync4 } from "node:fs";
4314
4392
  import { homedir as homedir4 } from "node:os";
@@ -4320,7 +4398,7 @@ var MEMORY_MD_PATH = /^shared\/projects\/([^/]+)\/memory\/[^/]+\.md$/;
4320
4398
  function buildMemoryScanTree(tmpRoot) {
4321
4399
  let paths;
4322
4400
  try {
4323
- const out = execFileSync9(
4401
+ const out = execFileSync10(
4324
4402
  "git",
4325
4403
  ["-C", repoHome(), "ls-tree", "-r", "-z", "--name-only", "HEAD", "--", "shared/projects"],
4326
4404
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1e4 }
@@ -4336,7 +4414,7 @@ function buildMemoryScanTree(tmpRoot) {
4336
4414
  if (m?.[1] === void 0) continue;
4337
4415
  let blob;
4338
4416
  try {
4339
- blob = execFileSync9("git", ["-C", repoHome(), "cat-file", "blob", `HEAD:${rel}`], {
4417
+ blob = execFileSync10("git", ["-C", repoHome(), "cat-file", "blob", `HEAD:${rel}`], {
4340
4418
  stdio: ["ignore", "pipe", "ignore"],
4341
4419
  maxBuffer: 67108864,
4342
4420
  timeout: 1e4
@@ -4504,7 +4582,7 @@ function scanAndReport(section2, tmpRoot, staged, logicalToEncoded) {
4504
4582
 
4505
4583
  // src/commands.doctor.check-shared.skills.ts
4506
4584
  init_color();
4507
- import { execFileSync as execFileSync10 } from "node:child_process";
4585
+ import { execFileSync as execFileSync11 } from "node:child_process";
4508
4586
  import { randomBytes as randomBytes2 } from "node:crypto";
4509
4587
  import { mkdirSync as mkdirSync5, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
4510
4588
  import { homedir as homedir5 } from "node:os";
@@ -4516,7 +4594,7 @@ var SKILL_PATH = /^shared\/skills\/([^/]+)\/(.+)$/;
4516
4594
  function buildSkillScanTree(tmpRoot) {
4517
4595
  let paths;
4518
4596
  try {
4519
- const out = execFileSync10(
4597
+ const out = execFileSync11(
4520
4598
  "git",
4521
4599
  ["-C", repoHome(), "ls-tree", "-r", "-z", "--name-only", "HEAD", "--", "shared/skills"],
4522
4600
  { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 1e4 }
@@ -4532,7 +4610,7 @@ function buildSkillScanTree(tmpRoot) {
4532
4610
  if (m?.[1] === void 0) continue;
4533
4611
  let blob;
4534
4612
  try {
4535
- blob = execFileSync10("git", ["-C", repoHome(), "cat-file", "blob", `HEAD:${rel}`], {
4613
+ blob = execFileSync11("git", ["-C", repoHome(), "cat-file", "blob", `HEAD:${rel}`], {
4536
4614
  stdio: ["ignore", "pipe", "ignore"],
4537
4615
  maxBuffer: 67108864,
4538
4616
  timeout: 1e4
@@ -4970,7 +5048,7 @@ function buildScanTree(tmpRoot) {
4970
5048
  }
4971
5049
  function probeGitleaksForScan() {
4972
5050
  try {
4973
- execFileSync11("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
5051
+ execFileSync12("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
4974
5052
  return "ok";
4975
5053
  } catch (err) {
4976
5054
  if (err.code === "ENOENT") return "missing";
@@ -5626,16 +5704,16 @@ function reportNodeEngineCheck(section2) {
5626
5704
 
5627
5705
  // src/commands.doctor.checks.longpaths.ts
5628
5706
  init_color();
5629
- import { execFileSync as execFileSync12 } from "node:child_process";
5707
+ import { execFileSync as execFileSync13 } from "node:child_process";
5630
5708
  init_config();
5631
- var PROBE_TIMEOUT_MS = 3e3;
5709
+ var PROBE_TIMEOUT_MS2 = 3e3;
5632
5710
  var LONGPATHS_REG_KEY = String.raw`HKLM\SYSTEM\CurrentControlSet\Control\FileSystem`;
5633
5711
  var LONGPATHS_REG_VALUE = "LongPathsEnabled";
5634
5712
  function probeGitLongpaths(run) {
5635
5713
  try {
5636
5714
  const out = run("git", ["-C", repoHome(), "config", "--get", "core.longpaths"], {
5637
5715
  stdio: ["ignore", "pipe", "pipe"],
5638
- timeout: PROBE_TIMEOUT_MS
5716
+ timeout: PROBE_TIMEOUT_MS2
5639
5717
  }).toString().trim();
5640
5718
  return out === "true" || out === "1";
5641
5719
  } catch {
@@ -5646,7 +5724,7 @@ function probeRegistryLongpaths(run) {
5646
5724
  try {
5647
5725
  const out = run("reg", ["query", LONGPATHS_REG_KEY, "/v", LONGPATHS_REG_VALUE], {
5648
5726
  stdio: ["ignore", "pipe", "pipe"],
5649
- timeout: PROBE_TIMEOUT_MS
5727
+ timeout: PROBE_TIMEOUT_MS2
5650
5728
  }).toString();
5651
5729
  return /0x1\b/.test(out);
5652
5730
  } catch {
@@ -5660,7 +5738,7 @@ function addLongpathsRow(section2, label, enabled2, remediation) {
5660
5738
  }
5661
5739
  addItem(section2, `${yellow(warnGlyph)} ${label}: not enabled (${remediation})`);
5662
5740
  }
5663
- function reportLongPathsCheck(section2, run = execFileSync12) {
5741
+ function reportLongPathsCheck(section2, run = execFileSync13) {
5664
5742
  if (process.platform !== "win32") return;
5665
5743
  addLongpathsRow(
5666
5744
  section2,
@@ -5684,11 +5762,11 @@ function reportSyncModality(section2) {
5684
5762
 
5685
5763
  // src/commands.doctor.checks.crlf.ts
5686
5764
  init_color();
5687
- import { execFileSync as execFileSync13 } from "node:child_process";
5765
+ import { execFileSync as execFileSync14 } from "node:child_process";
5688
5766
  import { existsSync as existsSync28, readFileSync as readFileSync13 } from "node:fs";
5689
5767
  import { join as join39 } from "node:path";
5690
5768
  init_config();
5691
- var PROBE_TIMEOUT_MS2 = 3e3;
5769
+ var PROBE_TIMEOUT_MS3 = 3e3;
5692
5770
  var GUARD_LINE = /^\*\s+-text\b/;
5693
5771
  function hasGitattributesGuard(repo) {
5694
5772
  try {
@@ -5704,7 +5782,7 @@ function probeAutocrlf(repo, run) {
5704
5782
  try {
5705
5783
  const out = run("git", ["-C", repo, "config", "core.autocrlf"], {
5706
5784
  stdio: ["ignore", "pipe", "pipe"],
5707
- timeout: PROBE_TIMEOUT_MS2
5785
+ timeout: PROBE_TIMEOUT_MS3
5708
5786
  }).toString().trim();
5709
5787
  if (out === "true" || out === "input") return "active";
5710
5788
  if (out === "false") return "disabled";
@@ -5723,7 +5801,7 @@ function addExposedRow(section2, repo, verdict) {
5723
5801
  const remediation = `add a .gitattributes with a \`* -text\` line, or run \`git config core.autocrlf false\`, in ${repo}`;
5724
5802
  addItem(section2, `${yellow(warnGlyph)} CRLF guard: ${risk}; ${remediation}`);
5725
5803
  }
5726
- function reportCrlfGuardCheck(section2, run = execFileSync13) {
5804
+ function reportCrlfGuardCheck(section2, run = execFileSync14) {
5727
5805
  const repo = repoHome();
5728
5806
  if (hasGitattributesGuard(repo)) {
5729
5807
  addItem(section2, `${green(okGlyph)} CRLF guard: .gitattributes (* -text) present`);
@@ -6148,10 +6226,10 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
6148
6226
 
6149
6227
  // src/commands.pushed-history.ts
6150
6228
  init_utils();
6151
- import { execFileSync as execFileSync14 } from "node:child_process";
6229
+ import { execFileSync as execFileSync15 } from "node:child_process";
6152
6230
  function pushedRef(repo) {
6153
6231
  try {
6154
- const ref = execFileSync14("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
6232
+ const ref = execFileSync15("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
6155
6233
  cwd: repo,
6156
6234
  stdio: ["ignore", "pipe", "ignore"]
6157
6235
  }).toString().trim();
@@ -6164,7 +6242,7 @@ function sessionInPushedHistory(id, repo) {
6164
6242
  const ref = pushedRef(repo);
6165
6243
  if (ref === null) return false;
6166
6244
  try {
6167
- const out = execFileSync14(
6245
+ const out = execFileSync15(
6168
6246
  "git",
6169
6247
  [
6170
6248
  "log",
@@ -6497,7 +6575,7 @@ var FRAGMENT_SEP = "...";
6497
6575
  var NEAR_WINDOW = 40;
6498
6576
  var VALUE_PAD = 16;
6499
6577
  var SIBLING_PAD = 2;
6500
- var CONTROL_CHARS = /[\x00-\x1f\x7f]/g;
6578
+ var CONTROL_CHARS2 = /[\x00-\x1f\x7f]/g;
6501
6579
  function classifyCharset(value) {
6502
6580
  if (/^[0-9a-fA-F]+$/.test(value)) return "hex";
6503
6581
  if (/^[0-9a-zA-Z]+$/.test(value)) return "alphanumeric";
@@ -6571,7 +6649,7 @@ function resolveSpanContext(finding, readLine, siblings = []) {
6571
6649
  return { value: aligned.value, near: emptyToNull(near), exact: aligned.exact };
6572
6650
  }
6573
6651
  function formatNear(text) {
6574
- const stripped = elideTokenRuns(text.replace(CONTROL_CHARS, ""));
6652
+ const stripped = elideTokenRuns(text.replace(CONTROL_CHARS2, ""));
6575
6653
  const truncated = stripped.length > NEAR_WINDOW;
6576
6654
  const window = stripped.slice(-NEAR_WINDOW).trim();
6577
6655
  if (window.length === 0) return null;
@@ -7048,7 +7126,7 @@ function withSpinner(label, fn, deps) {
7048
7126
 
7049
7127
  // src/commands.doctor.gitleaks-version.ts
7050
7128
  init_color();
7051
- import { execFileSync as execFileSync15 } from "node:child_process";
7129
+ import { execFileSync as execFileSync16 } from "node:child_process";
7052
7130
  import { existsSync as existsSync34 } from "node:fs";
7053
7131
  import { join as join48 } from "node:path";
7054
7132
  init_config();
@@ -7071,7 +7149,7 @@ function readGitleaksVersion(run, tomlExists) {
7071
7149
  return null;
7072
7150
  }
7073
7151
  }
7074
- function reportGitleaksVersionCheck(section2, run = execFileSync15, tomlExists = existsSync34) {
7152
+ function reportGitleaksVersionCheck(section2, run = execFileSync16, tomlExists = existsSync34) {
7075
7153
  const raw = readGitleaksVersion(run, tomlExists);
7076
7154
  if (raw === null) return;
7077
7155
  const local = majorMinorOf(raw);
@@ -7091,9 +7169,9 @@ function reportGitleaksVersionCheck(section2, run = execFileSync15, tomlExists =
7091
7169
 
7092
7170
  // src/commands.doctor.checks.deps.ts
7093
7171
  init_color();
7094
- import { execFileSync as execFileSync16 } from "node:child_process";
7172
+ import { execFileSync as execFileSync17 } from "node:child_process";
7095
7173
  var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
7096
- var PROBE_TIMEOUT_MS3 = 3e3;
7174
+ var PROBE_TIMEOUT_MS4 = 3e3;
7097
7175
  var FETCHER_BASE = "HTTP fetcher";
7098
7176
  function parseFirstVersion(line) {
7099
7177
  const m = VERSION_TOKEN.exec(line);
@@ -7103,7 +7181,7 @@ function probeOptionalDep(bin, run) {
7103
7181
  try {
7104
7182
  const firstLine = run(bin, ["--version"], {
7105
7183
  stdio: ["ignore", "pipe", "pipe"],
7106
- timeout: PROBE_TIMEOUT_MS3
7184
+ timeout: PROBE_TIMEOUT_MS4
7107
7185
  }).toString().split("\n")[0].trim();
7108
7186
  const version = parseFirstVersion(firstLine);
7109
7187
  return { status: "present", version };
@@ -7128,7 +7206,7 @@ function reportFetcherRow(section2, run) {
7128
7206
  );
7129
7207
  }
7130
7208
  }
7131
- function reportOptionalDeps(section2, run = execFileSync16) {
7209
+ function reportOptionalDeps(section2, run = execFileSync17) {
7132
7210
  const gh = probeOptionalDep("gh", run);
7133
7211
  if (gh.status === "present") {
7134
7212
  addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
@@ -7143,11 +7221,11 @@ function reportOptionalDeps(section2, run = execFileSync16) {
7143
7221
 
7144
7222
  // src/commands.doctor.actions-drift.ts
7145
7223
  init_color();
7146
- import { execFileSync as execFileSync18 } from "node:child_process";
7224
+ import { execFileSync as execFileSync19 } from "node:child_process";
7147
7225
  init_config();
7148
7226
 
7149
7227
  // src/gh-actions.ts
7150
- import { execFileSync as execFileSync17 } from "node:child_process";
7228
+ import { execFileSync as execFileSync18 } from "node:child_process";
7151
7229
  var GH_TIMEOUT_MS = 5e3;
7152
7230
  function parseGitHubRemote(remoteUrl) {
7153
7231
  const normalized = remoteUrl.trim().replace(/\/$/, "");
@@ -7155,7 +7233,7 @@ function parseGitHubRemote(remoteUrl) {
7155
7233
  if (m === null) return null;
7156
7234
  return { owner: m[1], repo: m[2] };
7157
7235
  }
7158
- function ghAuthStatus(run = execFileSync17) {
7236
+ function ghAuthStatus(run = execFileSync18) {
7159
7237
  try {
7160
7238
  run("gh", ["auth", "status"], {
7161
7239
  stdio: ["ignore", "ignore", "ignore"],
@@ -7169,7 +7247,7 @@ function ghAuthStatus(run = execFileSync17) {
7169
7247
  return "gh-probe-error";
7170
7248
  }
7171
7249
  }
7172
- function isRepoPrivate(ref, run = execFileSync17) {
7250
+ function isRepoPrivate(ref, run = execFileSync18) {
7173
7251
  const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
7174
7252
  stdio: ["ignore", "pipe", "ignore"],
7175
7253
  timeout: GH_TIMEOUT_MS
@@ -7177,7 +7255,7 @@ function isRepoPrivate(ref, run = execFileSync17) {
7177
7255
  const parsed = JSON.parse(out);
7178
7256
  return parsed.isPrivate === true;
7179
7257
  }
7180
- function isActionsEnabled(ref, run = execFileSync17) {
7258
+ function isActionsEnabled(ref, run = execFileSync18) {
7181
7259
  const out = run(
7182
7260
  "gh",
7183
7261
  ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
@@ -7185,7 +7263,7 @@ function isActionsEnabled(ref, run = execFileSync17) {
7185
7263
  ).toString().trim();
7186
7264
  return out === "true";
7187
7265
  }
7188
- function disableActions(ref, run = execFileSync17) {
7266
+ function disableActions(ref, run = execFileSync18) {
7189
7267
  run(
7190
7268
  "gh",
7191
7269
  [
@@ -7199,7 +7277,7 @@ function disableActions(ref, run = execFileSync17) {
7199
7277
  { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
7200
7278
  );
7201
7279
  }
7202
- function readOriginRemote(cwd, run = execFileSync17) {
7280
+ function readOriginRemote(cwd, run = execFileSync18) {
7203
7281
  return run("git", ["remote", "get-url", "origin"], {
7204
7282
  cwd,
7205
7283
  stdio: ["ignore", "pipe", "ignore"]
@@ -7207,7 +7285,7 @@ function readOriginRemote(cwd, run = execFileSync17) {
7207
7285
  }
7208
7286
 
7209
7287
  // src/commands.doctor.actions-drift.ts
7210
- function reportActionsDrift(section2, run = execFileSync18) {
7288
+ function reportActionsDrift(section2, run = execFileSync19) {
7211
7289
  let remote;
7212
7290
  try {
7213
7291
  remote = readOriginRemote(repoHome(), run);
@@ -7347,6 +7425,7 @@ function gatherDoctorSections(opts) {
7347
7425
  reportPathMap(pathMap);
7348
7426
  const neverSync = section("Never-sync");
7349
7427
  reportNeverSync(neverSync);
7428
+ reportTrackedDeniedShared(neverSync);
7350
7429
  const skills = section("Skills");
7351
7430
  reportSkillsDivergence(skills);
7352
7431
  const repository = section("Repository");
@@ -7426,15 +7505,15 @@ function parseDoctorArgs(args) {
7426
7505
 
7427
7506
  // src/commands.drop-session.ts
7428
7507
  init_config();
7429
- import { execFileSync as execFileSync20 } from "node:child_process";
7508
+ import { execFileSync as execFileSync21 } from "node:child_process";
7430
7509
  import { existsSync as existsSync37, readdirSync as readdirSync16, statSync as statSync10 } from "node:fs";
7431
7510
  import { join as join51, relative as relative7 } from "node:path";
7432
7511
 
7433
7512
  // src/commands.drop-session.git.ts
7434
- import { execFileSync as execFileSync19 } from "node:child_process";
7513
+ import { execFileSync as execFileSync20 } from "node:child_process";
7435
7514
  function expandStagedDir(dirRel, repo) {
7436
7515
  try {
7437
- const out = execFileSync19("git", ["ls-files", "-z", "--", dirRel], {
7516
+ const out = execFileSync20("git", ["ls-files", "-z", "--", dirRel], {
7438
7517
  cwd: repo,
7439
7518
  stdio: ["ignore", "pipe", "pipe"]
7440
7519
  });
@@ -7446,7 +7525,7 @@ function expandStagedDir(dirRel, repo) {
7446
7525
  function isTrackedInHead(rel, repo) {
7447
7526
  try {
7448
7527
  const treePath = process.platform === "win32" ? rel.replaceAll("\\", "/") : rel;
7449
- execFileSync19("git", ["cat-file", "-e", `HEAD:${treePath}`], {
7528
+ execFileSync20("git", ["cat-file", "-e", `HEAD:${treePath}`], {
7450
7529
  cwd: repo,
7451
7530
  stdio: ["ignore", "pipe", "pipe"]
7452
7531
  });
@@ -7457,7 +7536,7 @@ function isTrackedInHead(rel, repo) {
7457
7536
  }
7458
7537
  function isInIndex(rel, repo) {
7459
7538
  try {
7460
- const out = execFileSync19("git", ["ls-files", "--", rel], {
7539
+ const out = execFileSync20("git", ["ls-files", "--", rel], {
7461
7540
  cwd: repo,
7462
7541
  stdio: ["ignore", "pipe", "pipe"]
7463
7542
  });
@@ -7563,12 +7642,12 @@ function unstageOne(rel, repo) {
7563
7642
  }
7564
7643
  try {
7565
7644
  if (isTrackedInHead(rel, repo)) {
7566
- execFileSync20("git", ["restore", "--staged", "--worktree", "--", rel], {
7645
+ execFileSync21("git", ["restore", "--staged", "--worktree", "--", rel], {
7567
7646
  cwd: repo,
7568
7647
  stdio: ["ignore", "pipe", "pipe"]
7569
7648
  });
7570
7649
  } else {
7571
- execFileSync20("git", ["rm", "--cached", "-f", "--", rel], {
7650
+ execFileSync21("git", ["rm", "--cached", "-f", "--", rel], {
7572
7651
  cwd: repo,
7573
7652
  stdio: ["ignore", "pipe", "pipe"]
7574
7653
  });
@@ -8115,9 +8194,9 @@ import { join as join60 } from "node:path";
8115
8194
 
8116
8195
  // src/commands.pull.recovery.git.ts
8117
8196
  init_utils();
8118
- import { execFileSync as execFileSync21 } from "node:child_process";
8197
+ import { execFileSync as execFileSync22 } from "node:child_process";
8119
8198
  function gitCapture(args, cwd) {
8120
- return execFileSync21("git", args, {
8199
+ return execFileSync22("git", args, {
8121
8200
  cwd,
8122
8201
  stdio: ["ignore", "pipe", "pipe"],
8123
8202
  maxBuffer: 64 * 1024 * 1024
@@ -8155,23 +8234,6 @@ function parseDirtyPaths(repo, opts = {}) {
8155
8234
  // src/commands.pull.win32.ts
8156
8235
  init_config();
8157
8236
 
8158
- // src/git-probe.ts
8159
- import { execFileSync as execFileSync22 } from "node:child_process";
8160
- var PROBE_TIMEOUT_MS4 = 1e4;
8161
- var PROBE_MAX_BUFFER = 64 * 1024 * 1024;
8162
- function gitProbe(args, repo) {
8163
- try {
8164
- return execFileSync22("git", args, {
8165
- cwd: repo,
8166
- stdio: ["ignore", "pipe", "pipe"],
8167
- timeout: PROBE_TIMEOUT_MS4,
8168
- maxBuffer: PROBE_MAX_BUFFER
8169
- }).toString();
8170
- } catch {
8171
- return null;
8172
- }
8173
- }
8174
-
8175
8237
  // src/links.deletions.ts
8176
8238
  init_config();
8177
8239
  import { existsSync as existsSync40, lstatSync as lstatSync19, rmSync as rmSync17 } from "node:fs";
@@ -8235,6 +8297,7 @@ function applySharedLinkDeletions(map, ts, opts = {}) {
8235
8297
 
8236
8298
  // src/links.mirror.ts
8237
8299
  init_config();
8300
+ init_error_text();
8238
8301
  import { existsSync as existsSync41, lstatSync as lstatSync20, rmSync as rmSync18, statSync as statSync12 } from "node:fs";
8239
8302
  import { join as join57 } from "node:path";
8240
8303
  init_utils();
@@ -8402,19 +8465,19 @@ function reportTrackedDenied(repo, path, segment) {
8402
8465
  const denied = `the path segment "${segment}" is on the never-sync list`;
8403
8466
  if (inHead === null) {
8404
8467
  warn(
8405
- `could not check ${path} against HEAD: ${denied}. Nothing was changed. Inspect it with "git status -- ${path}" and take it out of shared/ before committing`
8468
+ `could not check ${path} against HEAD: ${denied}. Nothing was changed. Inspect it by running git status -- "${path}" and take it out of shared/ before committing`
8406
8469
  );
8407
8470
  return;
8408
8471
  }
8409
8472
  if (inHead.trim() === "") {
8410
8473
  warn(
8411
- `${path} is staged and has no committed version: ${denied}. Nothing was changed. Run "git rm --cached -- ${path}" to take it out of the index; that leaves the file on disk but makes it untracked, which the next nomad pull removes from the sync repo working tree (snapshotting it into the backup cache first), so move it outside shared/ instead if you want to keep it. If it is the destination half of a staged rename, "git diff --cached --name-status" names the source, whose content IS committed, so restore that half with "git checkout HEAD -- <source>" rather than leaving its deletion staged`
8474
+ `${path} is staged and has no committed version: ${denied}. Nothing was changed. Run git rm --cached -- "${path}" to take it out of the index; that leaves the file on disk but makes it untracked, which the next nomad pull removes from the sync repo working tree (snapshotting it into the backup cache first), so move it outside shared/ instead if you want to keep it. If it is the destination half of a staged rename, run git diff --cached --name-status to name the source, whose content IS committed, so restore that half with git checkout HEAD -- "<source>" rather than leaving its deletion staged`
8412
8475
  );
8413
8476
  return;
8414
8477
  }
8415
8478
  if (!existsSync41(join57(repo, path))) return;
8416
8479
  warn(
8417
- `${path} is tracked and has changes against HEAD: ${denied}. Nothing was changed. Run "git checkout HEAD -- ${path}" to put the committed content back, or move the file outside shared/ if you want to keep it. Neither of those takes the committed copy out of the repo: "git rm -- ${path}" and a commit does that going forward, and if it holds a real secret, rotate it and rewrite history, because nomad only changes your local worktree and index and cannot scrub what a previous push already sent to the remote`
8480
+ `${path} is tracked and has changes against HEAD: ${denied}. Nothing was changed. Run git checkout HEAD -- "${path}" to put the committed content back, or move the file outside shared/ if you want to keep it. Neither of those takes the committed copy out of the repo: git rm -- "${path}" and a commit does that going forward, and if it holds a real secret, rotate it and rewrite history, because nomad only changes your local worktree and index and cannot scrub what a previous push already sent to the remote`
8418
8481
  );
8419
8482
  }
8420
8483
  function revertDeniedMirrorPaths(repo, status, ts) {
@@ -8891,13 +8954,30 @@ function newlyUntracked(before, after) {
8891
8954
  if (before === null || after === null) return [];
8892
8955
  return [...after].filter((p) => !before.has(p));
8893
8956
  }
8957
+ function ignoredPathsUnderShared(repo) {
8958
+ const out = gitProbe(
8959
+ ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignored", "--", "shared/"],
8960
+ repo
8961
+ );
8962
+ if (out === null) return [];
8963
+ return out.split("\0").filter((record) => record.startsWith("!! ")).map((record) => record.slice(3));
8964
+ }
8965
+ function reportIgnoredDenied(repo) {
8966
+ for (const path of new Set(ignoredPathsUnderShared(repo))) {
8967
+ const segment = deniedSegmentFor(path);
8968
+ if (segment === null) continue;
8969
+ warn(
8970
+ `${path} is inside shared/ but git ignores it: the path segment "${segment}" is on the never-sync list. Nothing was changed, and nothing published it: nomad only ever stages with "git add -A", which skips an ignored path, so it has not reached the index or the remote. It stays invisible to the rest of this gate for the same reason, so move it outside shared/ if you want it kept, or delete it. Note the ignore rule may come from your global core.excludesFile rather than from this repo; run git check-ignore -v -- "${path}" to see which file and line the rule came from`
8971
+ );
8972
+ }
8973
+ }
8894
8974
  function revertDeniedUnderShared(repo, ts) {
8895
8975
  const out = gitProbe(
8896
8976
  ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "shared/"],
8897
8977
  repo
8898
8978
  );
8899
- if (out === null) return;
8900
- revertDeniedMirrorPaths(repo, parsePorcelainZ(out), ts);
8979
+ if (out !== null) revertDeniedMirrorPaths(repo, parsePorcelainZ(out), ts);
8980
+ reportIgnoredDenied(repo);
8901
8981
  }
8902
8982
  function reconcileSharedLinksBeforePull(repo, ts) {
8903
8983
  if (process.platform !== "win32") {
@@ -10899,7 +10979,7 @@ function parseSyncArgs(argv) {
10899
10979
  // package.json
10900
10980
  var package_default = {
10901
10981
  name: "claude-nomad",
10902
- version: "0.67.2",
10982
+ version: "0.68.0",
10903
10983
  type: "module",
10904
10984
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
10905
10985
  keywords: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-nomad",
3
- "version": "0.67.2",
3
+ "version": "0.68.0",
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": [