claude-nomad 0.66.0 → 0.67.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +87 -140
  2. package/dist/nomad.mjs +226 -149
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,58 +1,77 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.67.1](https://github.com/funkadelic/claude-nomad/compare/v0.67.0...v0.67.1) (2026-08-20)
4
+
5
+ ### What's new
6
+
7
+ **On all supported platforms, i.e. \*nix, Mac, WSL2, and native Windows.**
8
+
9
+ - **A project in a folder named `sessions`, `tasks`, `plans`, or `cache` now syncs its own history.** Those names, along with `debug`, `jobs`, `security`, and a few more, belong to nomad's own working folders, so a project of yours that happened to share one was refused as though it held something private. Nothing in it was, and the name alone decided it. Those projects now sync like any other, and nomad still checks what is actually inside a folder exactly as before.
10
+ - **A per-host settings file sitting at the very top of a folder you share now stays out of your sync repo.** Inside a folder you opted into sharing, nomad already blocked `settings.local.json` and anything else shaped like a credential file at every level except the very top, where one could slip past and reach your other machines. The top level is now covered too.
11
+
12
+ **On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and none of these apply to it.
13
+
14
+ - **A pull no longer removes that project's transcripts from your sync repo.** The same name check ran during a pull, where it deleted rather than refused, so every pull stripped that project's saved history out of the sync repo. Each one left a backup under `~/.cache/claude-nomad/backup/`, but never put anything back. Pulls now leave those projects alone.
15
+ - **When a sync cannot remove or back up a file, it now tells you why.** These warnings sometimes printed the word `undefined` where the reason belongs, so you got a filename and nothing else. They now carry the real reason. A failure arriving in an unexpected form could also stop the cleanup partway through, leaving the files after the first problem unhandled and unmentioned. The pass now runs to the end and reports everything it did.
16
+
17
+
18
+ ### Fixed
19
+
20
+ * **config:** stop a project's own folder name from blocking its history ([#536](https://github.com/funkadelic/claude-nomad/issues/536)) ([1994c55](https://github.com/funkadelic/claude-nomad/commit/1994c55a32c62220e8d7291554f95099b2c12eba))
21
+ * **mirror:** name the reason in sync warnings instead of undefined ([#537](https://github.com/funkadelic/claude-nomad/issues/537)) ([e973ff3](https://github.com/funkadelic/claude-nomad/commit/e973ff392e369694e517c5aabfb26c996836378f))
22
+
23
+
24
+ ### Changed
25
+
26
+ * **codeql:** exclude test files from security scanning ([#534](https://github.com/funkadelic/claude-nomad/issues/534)) ([c86a6d3](https://github.com/funkadelic/claude-nomad/commit/c86a6d3c80a640e0a251181c8c0461edef2375da))
27
+
28
+
29
+ ### Dependencies
30
+
31
+ * bump fast-uri from 3.1.4 to 3.1.5 ([#527](https://github.com/funkadelic/claude-nomad/issues/527)) ([6a8f6ba](https://github.com/funkadelic/claude-nomad/commit/6a8f6baa534dae9d9e6786acf96627c85bb8d400))
32
+ * bump js-yaml from 4.3.0 to 4.3.1 in /docs-site ([#529](https://github.com/funkadelic/claude-nomad/issues/529)) ([a4e541f](https://github.com/funkadelic/claude-nomad/commit/a4e541fd74100527306ebaa713a2a730ffe98af7))
33
+ * bump nanoid from 3.3.15 to 3.3.18 in /docs-site ([#531](https://github.com/funkadelic/claude-nomad/issues/531)) ([0a0f53e](https://github.com/funkadelic/claude-nomad/commit/0a0f53ee46e3b69babd82b84e39c6982791f8896))
34
+ * bump postcss from 8.5.15 to 8.5.26 in /docs-site ([#530](https://github.com/funkadelic/claude-nomad/issues/530)) ([413f75d](https://github.com/funkadelic/claude-nomad/commit/413f75db6c292785f6b7d67fbd629680d5b39e04))
35
+ * bump postcss from 8.5.16 to 8.5.26 ([#532](https://github.com/funkadelic/claude-nomad/issues/532)) ([b03bd62](https://github.com/funkadelic/claude-nomad/commit/b03bd62ab77249bdcc31c78ac2440da434b2de39))
36
+ * bump svgo from 4.0.1 to 4.0.2 in /docs-site ([#528](https://github.com/funkadelic/claude-nomad/issues/528)) ([a424693](https://github.com/funkadelic/claude-nomad/commit/a4246932d01a79082383d6a09e08e7542eb7a873))
37
+
38
+ ## [0.67.0](https://github.com/funkadelic/claude-nomad/compare/v0.66.0...v0.67.0) (2026-08-18)
39
+
40
+ ### What's new
41
+
42
+ **On all supported platforms, i.e. \*nix, Mac, WSL2, and native Windows.**
43
+
44
+ - **Your backup cache no longer collects empty folders.** Every sync saves a copy of anything it is about to overwrite into a dated folder under `~/.cache/claude-nomad/backup/`, and it creates that folder before it touches your files so a full disk or a permissions problem stops the sync before it has changed anything. A sync that turns out to have nothing to save now clears its own folder instead of leaving an empty one behind, and so does one that stops early.
45
+ - **`nomad clean --backups` removes empty backup folders whatever their age.** They hold nothing to recover, so neither the 14-day default nor `--keep <N>` keeps one alive, and they no longer use up the slots `--keep <N>` reserves: ask for the five newest and you get the five newest that actually hold something. Backups holding anything at all, a file or even a link, are left exactly as before. `nomad clean --backups --dry-run` always prints its preview, since it changes nothing and never waits on another command.
46
+ - **`nomad adopt` no longer runs at the same time as a pull, push, or sync.** It waits its turn the way those commands already wait for each other, so a cleanup or a sync running in another terminal cannot remove or overwrite a backup that adopt is still writing. If another one is already running, adopt says so and does nothing rather than starting a move it cannot finish safely. Previewing with `nomad adopt <name> --dry-run` never waits, since it only reads and prints.
47
+
48
+
49
+ ### Added
50
+
51
+ * clean up the empty folders in your backup cache ([#525](https://github.com/funkadelic/claude-nomad/issues/525)) ([3f5ae92](https://github.com/funkadelic/claude-nomad/commit/3f5ae92e46173364f5e9c66e4963bcc716c813e6))
52
+
53
+
54
+ ### Fixed
55
+
56
+ * **ci:** base the drift branch on main on a dispatch run ([#524](https://github.com/funkadelic/claude-nomad/issues/524)) ([3e94c1e](https://github.com/funkadelic/claude-nomad/commit/3e94c1e2df9296a539c71eac873058cb69225d78))
57
+
3
58
  ## [0.66.0](https://github.com/funkadelic/claude-nomad/compare/v0.65.0...v0.66.0) (2026-08-17)
4
59
 
5
60
  ### What's new
6
61
 
7
62
  **On all supported platforms, i.e. \*nix, Mac, WSL2, and native Windows.**
8
63
 
9
- - **A folder you share now carries everything inside it.** If you share a folder through
10
- `sharedDirs`, its subfolders travel with it, including ones with everyday names like `sessions`,
11
- `plans`, `tasks`, and `cache`. Those names used to be refused wherever they appeared, so a push
12
- could fail, or `nomad adopt` could turn you away, over a folder holding nothing sensitive. Your
13
- Claude login, your credential files, your per-host settings, and your local history still never
14
- leave the machine, in either direction.
15
- - **`nomad adopt` checks the whole folder before it moves anything.** It used to copy everything,
16
- so a file that never syncs between machines could land in your repo and fail your next push, on
17
- content you never chose to add. Adopt now stops before touching anything and names every path it
18
- objected to and why: a name that collides with the never-sync list, which renaming clears, or a
19
- name that looks like a credential file, where renaming only helps if the new name looks different
20
- too. `--dry-run` gives exactly the same answer as the real run, so you can check a folder before
21
- committing to it.
22
- - **An interrupted `nomad adopt` now explains itself instead of writing a crash report.** If
23
- another program has the directory open, or its permissions block the move partway through, the
24
- command says what happened, where your content ended up, and what to run next. Your content is
25
- safe in every one of these cases: either it was never touched on this machine, or it is already
26
- in the repo.
27
-
28
- **On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and none of these apply to
29
- it.
30
-
31
- - **A push no longer publishes a folder you never asked to share.** Windows keeps your shared
32
- config as real file copies rather than symlinks, and a push used to pick up any folder sitting in
33
- your Claude directory and publish it to your other machines. Sharing is now the same deliberate
34
- step it has always been on macOS and Linux: run `nomad adopt <name>` once, then push. Your
35
- existing shared folders keep syncing exactly as before.
36
- - **`nomad doctor --verbose` names a folder your repo does not carry yet.** It lists the local
37
- copies that are still private to this machine, and the command that shares one.
38
- - **One unreadable file no longer ends your pull.** A single config file that another program has
39
- open, or that your account cannot read, used to stop the whole pull with a crash report. Nomad
40
- now names the file and the reason, then carries on: your settings, your skills, and your session
41
- history all still sync, and the command finishes normally. The warning also says whether the file
42
- is still there or has been removed, and points at the backup copy when there is one.
43
- - **`nomad adopt` recovers cleanly when it cannot restore your local copy.** Adopt finishes by
44
- copying the folder back out of the sync repo so this machine keeps a usable copy. If that copy
45
- was blocked, the command used to end in a crash report, with the folder gone from your Claude
46
- directory and nothing staged for publishing. It now stops with a plain message naming the folder
47
- and the reason, stages the adopted content either way, and tells you to run `nomad pull` before
48
- your next push and why that order matters. A half written folder left behind by the interrupted
49
- copy is removed, so a later push cannot replace your adopted folder with the partial one.
50
- - **A refused `nomad pull --force-remote` now tells you what actually clears it.** The old advice
51
- did not work, and would land you back on the same refusal. The message now gives the steps that
52
- finish the job and names the one that clears the check, and it says that the stuck rebase has
53
- already been undone by that point, so retrying is now just an ordinary pull. It no longer calls
54
- every listed file at risk, since the list can include files where the shared repo is simply ahead
55
- of yours. The quickstart says the same thing and links to the matching recovery steps in the FAQ.
64
+ - **A folder you share now carries everything inside it.** If you share a folder through `sharedDirs`, its subfolders travel with it, including ones with everyday names like `sessions`, `plans`, `tasks`, and `cache`. Those names used to be refused wherever they appeared, so a push could fail, or `nomad adopt` could turn you away, over a folder holding nothing sensitive. Your Claude login, your credential files, your per-host settings, and your local history still never leave the machine, in either direction.
65
+ - **`nomad adopt` checks the whole folder before it moves anything.** It used to copy everything, so a file that never syncs between machines could land in your repo and fail your next push, on content you never chose to add. Adopt now stops before touching anything and names every path it objected to and why: a name that collides with the never-sync list, which renaming clears, or a name that looks like a credential file, where renaming only helps if the new name looks different too. `--dry-run` gives exactly the same answer as the real run, so you can check a folder before committing to it.
66
+ - **An interrupted `nomad adopt` now explains itself instead of writing a crash report.** If another program has the directory open, or its permissions block the move partway through, the command says what happened, where your content ended up, and what to run next. Your content is safe in every one of these cases: either it was never touched on this machine, or it is already in the repo.
67
+
68
+ **On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and none of these apply to it.
69
+
70
+ - **A push no longer publishes a folder you never asked to share.** Windows keeps your shared config as real file copies rather than symlinks, and a push used to pick up any folder sitting in your Claude directory and publish it to your other machines. Sharing is now the same deliberate step it has always been on macOS and Linux: run `nomad adopt <name>` once, then push. Your existing shared folders keep syncing exactly as before.
71
+ - **`nomad doctor --verbose` names a folder your repo does not carry yet.** It lists the local copies that are still private to this machine, and the command that shares one.
72
+ - **One unreadable file no longer ends your pull.** A single config file that another program has open, or that your account cannot read, used to stop the whole pull with a crash report. Nomad now names the file and the reason, then carries on: your settings, your skills, and your session history all still sync, and the command finishes normally. The warning also says whether the file is still there or has been removed, and points at the backup copy when there is one.
73
+ - **`nomad adopt` recovers cleanly when it cannot restore your local copy.** Adopt finishes by copying the folder back out of the sync repo so this machine keeps a usable copy. If that copy was blocked, the command used to end in a crash report, with the folder gone from your Claude directory and nothing staged for publishing. It now stops with a plain message naming the folder and the reason, stages the adopted content either way, and tells you to run `nomad pull` before your next push and why that order matters. A half written folder left behind by the interrupted copy is removed, so a later push cannot replace your adopted folder with the partial one.
74
+ - **A refused `nomad pull --force-remote` now tells you what actually clears it.** The old advice did not work, and would land you back on the same refusal. The message now gives the steps that finish the job and names the one that clears the check, and it says that the stuck rebase has already been undone by that point, so retrying is now just an ordinary pull. It no longer calls every listed file at risk, since the list can include files where the shared repo is simply ahead of yours. The quickstart says the same thing and links to the matching recovery steps in the FAQ.
56
75
 
57
76
 
58
77
  ### Added
@@ -81,39 +100,16 @@ it.
81
100
 
82
101
  ### What's new
83
102
 
84
- **On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and none of these apply to
85
- it.
86
-
87
- - **`nomad pull --force-remote` no longer discards edits you have not published.** The flag exists
88
- to rescue a sync repo that got stuck part-way through an update. On native Windows it also skipped
89
- the step that saves your local shared-config edits into the repo, every time it ran, even on a
90
- healthy machine with nothing to rescue. That was the flag's only visible effect on a healthy repo,
91
- and nothing told you it had happened. It now rescues a stuck repo and nothing else, and when a
92
- rescue genuinely does replace your copies, the pull says how many and where your previous ones
93
- were saved.
94
- - **A pull now says what it did to your shared config.** On Windows your shared config is kept as
95
- real file copies rather than symlinks, so every pull starts by copying your local edits into the
96
- sync repo. That step used to be silent. It now prints a `Symlinks` section naming each file it
97
- picked up, and each file it removed from the repo because you deleted it here. If a file cannot be
98
- read at all, because another program is holding it open or its permissions changed, the pull names
99
- it and says why rather than leaving a silent gap. `nomad diff` and `nomad pull --dry-run` read
100
- from the same place, so a preview now matches what a real pull does.
101
- - **Windows keeps the same things out of your sync repo that your other machines do.** The Windows
102
- copy step used to hold back only files whose names looked like passwords or keys. It now skips
103
- everything on the never-sync list, so the folders Claude Code uses for its own state, named things
104
- like `sessions`, `tasks`, `plans`, and `cache`, stay on the machine even when they sit inside a
105
- folder you share. If one of them is already sitting in your sync repo, the pull tells you, and
106
- takes a backup copy before removing it.
103
+ **On native Windows (PowerShell or cmd).** Note: WSL2 behaves like Linux and none of these apply to it.
104
+
105
+ - **`nomad pull --force-remote` no longer discards edits you have not published.** The flag exists to rescue a sync repo that got stuck part-way through an update. On native Windows it also skipped the step that saves your local shared-config edits into the repo, every time it ran, even on a healthy machine with nothing to rescue. That was the flag's only visible effect on a healthy repo, and nothing told you it had happened. It now rescues a stuck repo and nothing else, and when a rescue genuinely does replace your copies, the pull says how many and where your previous ones were saved.
106
+ - **A pull now says what it did to your shared config.** On Windows your shared config is kept as real file copies rather than symlinks, so every pull starts by copying your local edits into the sync repo. That step used to be silent. It now prints a `Symlinks` section naming each file it picked up, and each file it removed from the repo because you deleted it here. If a file cannot be read at all, because another program is holding it open or its permissions changed, the pull names it and says why rather than leaving a silent gap. `nomad diff` and `nomad pull --dry-run` read from the same place, so a preview now matches what a real pull does.
107
+ - **Windows keeps the same things out of your sync repo that your other machines do.** The Windows copy step used to hold back only files whose names looked like passwords or keys. It now skips everything on the never-sync list, so the folders Claude Code uses for its own state, named things like `sessions`, `tasks`, `plans`, and `cache`, stay on the machine even when they sit inside a folder you share. If one of them is already sitting in your sync repo, the pull tells you, and takes a backup copy before removing it.
107
108
 
108
109
  **On all supported platforms, i.e. \*nix, Mac, WSL2, and native Windows.**
109
110
 
110
- - **`nomad doctor` names the files that have drifted.** When it reports that your skills or your
111
- shared config no longer match the repo, it lists them. Before, you got a count and had to run it
112
- again with `--verbose` to find out which files it meant.
113
- - **`nomad pull --force-remote` tells you when there was nothing to rescue.** On a repo that is not
114
- stuck it now says so and pulls normally, instead of appearing to do nothing. If it cannot tell
115
- whether the repo is stuck, it says that too rather than reporting a clean repo. The help text,
116
- README, and docs site now agree on what the flag does.
111
+ - **`nomad doctor` names the files that have drifted.** When it reports that your skills or your shared config no longer match the repo, it lists them. Before, you got a count and had to run it again with `--verbose` to find out which files it meant.
112
+ - **`nomad pull --force-remote` tells you when there was nothing to rescue.** On a repo that is not stuck it now says so and pulls normally, instead of appearing to do nothing. If it cannot tell whether the repo is stuck, it says that too rather than reporting a clean repo. The help text, README, and docs site now agree on what the flag does.
117
113
 
118
114
 
119
115
  ### Added
@@ -152,21 +148,9 @@ it.
152
148
 
153
149
  ### What's new
154
150
 
155
- - **A stray dot or space at the end of a file name no longer hides a secret.** nomad never copies
156
- password and key files (things like `.env`, `id_rsa`, or `server.pem`) out of the project folders
157
- you sync. It spots them by name, and a name with an extra dot or space on the end, such as
158
- `.env.`, did not match, so a file like that could end up in your sync repo. It matches now, and so
159
- do the other files nomad always keeps out, such as `settings.local.json`. Nothing changes in how
160
- you use nomad. It simply catches more ways of writing the same names it already blocked.
161
- - **A `.claude` folder is recognized however it is capitalized.** If a project had a folder named
162
- `.Claude` instead of `.claude`, nomad checked what was inside it less carefully than it should
163
- have. Both are treated the same now.
164
- - **A folder name nomad will never sync is turned down as soon as you add it.** You can ask nomad to
165
- sync extra folders by listing their names in `path-map.json`. If a name you list is one nomad
166
- refuses to sync, because it looks like a password file, or because it is a name that causes
167
- trouble on one of the machines you sync to, you are told straight away and told why. Before,
168
- nothing was said until a later `nomad push` stopped without explaining itself. `nomad doctor` also
169
- lists any names in this situation and what to do about them.
151
+ - **A stray dot or space at the end of a file name no longer hides a secret.** nomad never copies password and key files (things like `.env`, `id_rsa`, or `server.pem`) out of the project folders you sync. It spots them by name, and a name with an extra dot or space on the end, such as `.env.`, did not match, so a file like that could end up in your sync repo. It matches now, and so do the other files nomad always keeps out, such as `settings.local.json`. Nothing changes in how you use nomad. It simply catches more ways of writing the same names it already blocked.
152
+ - **A `.claude` folder is recognized however it is capitalized.** If a project had a folder named `.Claude` instead of `.claude`, nomad checked what was inside it less carefully than it should have. Both are treated the same now.
153
+ - **A folder name nomad will never sync is turned down as soon as you add it.** You can ask nomad to sync extra folders by listing their names in `path-map.json`. If a name you list is one nomad refuses to sync, because it looks like a password file, or because it is a name that causes trouble on one of the machines you sync to, you are told straight away and told why. Before, nothing was said until a later `nomad push` stopped without explaining itself. `nomad doctor` also lists any names in this situation and what to do about them.
170
154
 
171
155
  All of this applies on macOS, Linux, WSL2, and native Windows.
172
156
 
@@ -185,31 +169,11 @@ All of this applies on macOS, Linux, WSL2, and native Windows.
185
169
 
186
170
  ### What's new
187
171
 
188
- - **Native Windows: deleting a shared config file now sticks.** On native Windows (PowerShell or
189
- cmd), deleting a file inside a synced folder, say `~/.claude/commands/old.md`, used to leave it
190
- sitting in the sync repo, and the next `nomad pull` put it straight back. The deletion is now
191
- carried into the sync repo instead, so it survives the pull and reaches your other machines.
192
- Nothing is removed unless this machine's own record shows it had the file, and every removed file
193
- is copied to the backup directory first. The first pull after upgrading has no record yet, so
194
- deletions start propagating from the pull after that. macOS, Linux, and WSL2 were never affected:
195
- they symlink shared config, so deleting the file was always a deletion in the repo.
196
- - **Native Windows: a name clash during a pull now explains itself.** If a file you created inside a
197
- synced folder has the same name as one an incoming update adds, the pull stops before changing
198
- anything. You used to be left with git's own error, which pointed at a copy inside the sync repo
199
- and told you to move it, and following that advice looped, because the next pull copied your file
200
- back over it. You now get the file under `~/.claude/` to act on and the two ways to finish, keep
201
- the incoming version or keep both and merge them, and nomad clears its own copy out of the way for
202
- you. Your file is untouched either way.
203
- - **Native Windows shared-config changes show up in a preview first.** `nomad pull --dry-run` and
204
- `nomad diff` now list both the edits being carried into the repo and the files being removed from
205
- it, worked out before the pull so the preview and the real run agree.
206
- - **`nomad doctor` notices a native Windows shared file that has drifted.** When a synced copy under
207
- `~/.claude/` no longer matches the one in the repo, doctor warns about it instead of reporting the
208
- machine as healthy. On the symlink platforms there is only ever one file, so there is nothing to
209
- drift.
210
- - **`nomad doctor` no longer recognizes `leftArrowOpensAgents`.** Claude Code dropped it from its
211
- settings schema, so doctor will flag it as an unknown key if you still have it set. This one
212
- applies everywhere, not just on Windows.
172
+ - **Native Windows: deleting a shared config file now sticks.** On native Windows (PowerShell or cmd), deleting a file inside a synced folder, say `~/.claude/commands/old.md`, used to leave it sitting in the sync repo, and the next `nomad pull` put it straight back. The deletion is now carried into the sync repo instead, so it survives the pull and reaches your other machines. Nothing is removed unless this machine's own record shows it had the file, and every removed file is copied to the backup directory first. The first pull after upgrading has no record yet, so deletions start propagating from the pull after that. macOS, Linux, and WSL2 were never affected: they symlink shared config, so deleting the file was always a deletion in the repo.
173
+ - **Native Windows: a name clash during a pull now explains itself.** If a file you created inside a synced folder has the same name as one an incoming update adds, the pull stops before changing anything. You used to be left with git's own error, which pointed at a copy inside the sync repo and told you to move it, and following that advice looped, because the next pull copied your file back over it. You now get the file under `~/.claude/` to act on and the two ways to finish, keep the incoming version or keep both and merge them, and nomad clears its own copy out of the way for you. Your file is untouched either way.
174
+ - **Native Windows shared-config changes show up in a preview first.** `nomad pull --dry-run` and `nomad diff` now list both the edits being carried into the repo and the files being removed from it, worked out before the pull so the preview and the real run agree.
175
+ - **`nomad doctor` notices a native Windows shared file that has drifted.** When a synced copy under `~/.claude/` no longer matches the one in the repo, doctor warns about it instead of reporting the machine as healthy. On the symlink platforms there is only ever one file, so there is nothing to drift.
176
+ - **`nomad doctor` no longer recognizes `leftArrowOpensAgents`.** Claude Code dropped it from its settings schema, so doctor will flag it as an unknown key if you still have it set. This one applies everywhere, not just on Windows.
213
177
 
214
178
 
215
179
  ### Added
@@ -235,17 +199,9 @@ All of this applies on macOS, Linux, WSL2, and native Windows.
235
199
 
236
200
  ### What's new
237
201
 
238
- - **`nomad doctor` stops warning about settings you legitimately set.** The list of settings it
239
- recognizes has been updated and synced with the current Claude Code settings schema, so settings
240
- the app actually writes (quiet hours, the break reminder, the token-usage reminder, and others)
241
- are no longer reported as unknown keys on an otherwise healthy machine. Entries that were never
242
- settings at all have been dropped at the same time.
243
- - **A new Claude Code setting is recognized within a day.** The job that tracks the official
244
- settings list ran once a week, so a setting added on a Tuesday could be reported as unknown until
245
- the following Monday. It now runs daily.
246
- - **The list keeps its own bookkeeping straight from here.** When a setting Claude Code writes
247
- ahead of its published documentation is later documented, it now moves across on its own instead
248
- of being recorded in two places at once.
202
+ - **`nomad doctor` stops warning about settings you legitimately set.** The list of settings it recognizes has been updated and synced with the current Claude Code settings schema, so settings the app actually writes (quiet hours, the break reminder, the token-usage reminder, and others) are no longer reported as unknown keys on an otherwise healthy machine. Entries that were never settings at all have been dropped at the same time.
203
+ - **A new Claude Code setting is recognized within a day.** The job that tracks the official settings list ran once a week, so a setting added on a Tuesday could be reported as unknown until the following Monday. It now runs daily.
204
+ - **The list keeps its own bookkeeping straight from here.** When a setting Claude Code writes ahead of its published documentation is later documented, it now moves across on its own instead of being recorded in two places at once.
249
205
 
250
206
 
251
207
  ### Fixed
@@ -276,18 +232,9 @@ All of this applies on macOS, Linux, WSL2, and native Windows.
276
232
 
277
233
  ### What's new
278
234
 
279
- - **Windows: your own edits to shared config are no longer reverted.** On Windows, editing a synced
280
- file like `~/.claude/CLAUDE.md` and then running `nomad pull` overwrote it from the repo, and
281
- `nomad sync` went on to publish the reverted version, silently undoing the change you ran sync to
282
- publish. Your edit is now carried into the sync repo first, so it survives the pull and gets
283
- published instead.
284
- - **The secret prompt on push is readable and asks once per secret.** When a push finds a possible
285
- secret it used to ask the same question several times over for one secret, and the snippet it
286
- showed was mostly noise. It now asks once per secret and describes what was actually found, so you
287
- can tell a real credential from a false positive at a glance.
288
- - **Three ways that prompt could show you a secret it meant to hide are closed**, along with a case
289
- where allowing one false positive could quietly let a real secret through beside it on the same
290
- line.
235
+ - **Windows: your own edits to shared config are no longer reverted.** On Windows, editing a synced file like `~/.claude/CLAUDE.md` and then running `nomad pull` overwrote it from the repo, and `nomad sync` went on to publish the reverted version, silently undoing the change you ran sync to publish. Your edit is now carried into the sync repo first, so it survives the pull and gets published instead.
236
+ - **The secret prompt on push is readable and asks once per secret.** When a push finds a possible secret it used to ask the same question several times over for one secret, and the snippet it showed was mostly noise. It now asks once per secret and describes what was actually found, so you can tell a real credential from a false positive at a glance.
237
+ - **Three ways that prompt could show you a secret it meant to hide are closed**, along with a case where allowing one false positive could quietly let a real secret through beside it on the same line.
291
238
 
292
239
 
293
240
  ### Added
package/dist/nomad.mjs CHANGED
@@ -70,6 +70,9 @@ function regionKey(segment) {
70
70
  function isExtrasScoped(segments) {
71
71
  return segments[0] === "shared" && segments.length > 1 && regionKey(segments[1]) === "extras";
72
72
  }
73
+ function isLogicalNameScoped(segments) {
74
+ return segments[0] === "shared" && segments.length > 1 && UNFILTERED_SHARED_REGIONS.has(regionKey(segments[1]));
75
+ }
73
76
  function isSharedNameScoped(segments) {
74
77
  return segments[0] === "shared" && segments.length > 1 && !UNFILTERED_SHARED_REGIONS.has(regionKey(segments[1]));
75
78
  }
@@ -83,7 +86,7 @@ function blockSetFor(segments) {
83
86
  function deniedSegmentFor(path) {
84
87
  const segments = path.split("/");
85
88
  const blockSet = blockSetFor(segments);
86
- const scan = isExtrasScoped(segments) ? segments.slice(4) : segments;
89
+ const scan = isLogicalNameScoped(segments) && segments.length > 3 ? segments.slice(3) : segments;
87
90
  for (const segment of scan) {
88
91
  if (isDeniedName(blockSet, segment)) return segment;
89
92
  }
@@ -807,6 +810,7 @@ import {
807
810
  mkdirSync,
808
811
  openSync,
809
812
  renameSync,
813
+ rmdirSync,
810
814
  statSync,
811
815
  symlinkSync,
812
816
  writeFileSync
@@ -867,6 +871,12 @@ function freshBackupTs(backupRoot) {
867
871
  while (existsSync3(join5(backupRoot, `${base}-${n}`))) n++;
868
872
  return `${base}-${n}`;
869
873
  }
874
+ function discardEmptyBackupDir(backupRoot) {
875
+ try {
876
+ rmdirSync(backupRoot);
877
+ } catch {
878
+ }
879
+ }
870
880
  function ensureSymlink(linkPath, target) {
871
881
  if (existsSync3(linkPath)) {
872
882
  if (lstatSync4(linkPath).isSymbolicLink()) return;
@@ -1549,7 +1559,7 @@ var init_push_leak_verdict = __esm({
1549
1559
 
1550
1560
  // src/commands.adopt.ts
1551
1561
  import { existsSync as existsSync5, lstatSync as lstatSync7 } from "node:fs";
1552
- import { join as join8 } from "node:path";
1562
+ import { join as join9 } from "node:path";
1553
1563
 
1554
1564
  // src/commands.adopt.recover.ts
1555
1565
  import { lstatSync as lstatSync6, rmSync as rmSync4 } from "node:fs";
@@ -1557,10 +1567,22 @@ import { dirname as dirname2, join as join7, resolve as resolve2 } from "node:pa
1557
1567
 
1558
1568
  // src/commands.adopt.scan.ts
1559
1569
  init_config();
1560
- init_exit_codes();
1561
- init_utils();
1562
1570
  import { lstatSync, readdirSync } from "node:fs";
1563
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
1584
+ init_exit_codes();
1585
+ init_utils();
1564
1586
  function toForwardSlash(p) {
1565
1587
  return p.replaceAll("\\", "/");
1566
1588
  }
@@ -1589,10 +1611,8 @@ function scanOrFatal(name, root, state) {
1589
1611
  try {
1590
1612
  return scanDeniedEntries(root);
1591
1613
  } catch (err) {
1592
- const message = err?.message;
1593
- const text = typeof message === "string" ? message : String(err);
1594
1614
  throw new NomadFatal(
1595
- `cannot adopt ${name}: could not scan ${root} for never-sync content (${text}). ${state()} Check that it is readable and not being written to, then run \`nomad adopt ${name}\` again.`,
1615
+ `cannot adopt ${name}: could not scan ${root} for never-sync content (${errorText(err)}). ${state()} Check that it is readable and not being written to, then run \`nomad adopt ${name}\` again.`,
1596
1616
  { code: EXIT.GENERIC_FAILURE }
1597
1617
  );
1598
1618
  }
@@ -2332,10 +2352,6 @@ function regenerateSettings(ts, opts = {}) {
2332
2352
 
2333
2353
  // src/commands.adopt.recover.ts
2334
2354
  init_utils();
2335
- function errorText(err) {
2336
- const message = err?.message;
2337
- return typeof message === "string" ? message : String(err);
2338
- }
2339
2355
  function lexists(p) {
2340
2356
  try {
2341
2357
  lstatSync6(p);
@@ -2460,10 +2476,125 @@ init_config_sharedDirs_guard();
2460
2476
  init_exit_codes();
2461
2477
  init_utils();
2462
2478
  init_utils_fs();
2479
+
2480
+ // src/utils.lockfile.ts
2481
+ init_config();
2482
+ init_utils();
2483
+ import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2484
+ import { dirname as dirname3, join as join8 } from "node:path";
2485
+ function lockFilePath() {
2486
+ return join8(home(), ".cache", "claude-nomad", "nomad.lock");
2487
+ }
2488
+ function acquireLock(verb) {
2489
+ const lp = lockFilePath();
2490
+ mkdirSync2(dirname3(lp), { recursive: true });
2491
+ try {
2492
+ const fd = openSync2(lp, "wx");
2493
+ try {
2494
+ writeFileSync2(fd, String(process.pid));
2495
+ } catch (writeErr) {
2496
+ try {
2497
+ closeSync2(fd);
2498
+ } catch {
2499
+ }
2500
+ try {
2501
+ unlinkSync(lp);
2502
+ } catch {
2503
+ }
2504
+ throw writeErr;
2505
+ }
2506
+ return { fd, path: lp };
2507
+ } catch (err) {
2508
+ const code = err.code;
2509
+ if (code !== "EEXIST") throw err;
2510
+ return checkStaleAndRetry(verb, lp);
2511
+ }
2512
+ }
2513
+ function releaseLock(handle) {
2514
+ if (handle === null) return;
2515
+ const lp = handle.path;
2516
+ try {
2517
+ closeSync2(handle.fd);
2518
+ } catch {
2519
+ }
2520
+ try {
2521
+ unlinkSync(lp);
2522
+ } catch (err) {
2523
+ if (err.code !== "ENOENT") throw err;
2524
+ }
2525
+ }
2526
+ function unlinkIfSamePid(expectedPidStr, lp) {
2527
+ let current;
2528
+ try {
2529
+ current = readFileSync3(lp, "utf8").trim();
2530
+ } catch {
2531
+ return false;
2532
+ }
2533
+ if (current !== expectedPidStr) return false;
2534
+ try {
2535
+ unlinkSync(lp);
2536
+ return true;
2537
+ } catch {
2538
+ return false;
2539
+ }
2540
+ }
2541
+ function checkStaleAndRetry(verb, lp) {
2542
+ let pidStr;
2543
+ try {
2544
+ pidStr = readFileSync3(lp, "utf8").trim();
2545
+ } catch {
2546
+ pidStr = "";
2547
+ }
2548
+ const pid = Number.parseInt(pidStr, 10);
2549
+ if (!Number.isFinite(pid) || pid <= 0) {
2550
+ if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
2551
+ warn(`another nomad ${verb} running, skipping`);
2552
+ return null;
2553
+ }
2554
+ try {
2555
+ process.kill(pid, 0);
2556
+ warn(`another nomad ${verb} running, skipping`);
2557
+ return null;
2558
+ } catch (err) {
2559
+ const code = err.code;
2560
+ if (code === "ESRCH") {
2561
+ if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
2562
+ warn(`another nomad ${verb} running, skipping`);
2563
+ return null;
2564
+ }
2565
+ warn(`another nomad ${verb} running, skipping`);
2566
+ return null;
2567
+ }
2568
+ }
2569
+ function retryOnce(verb, lp) {
2570
+ try {
2571
+ const fd = openSync2(lp, "wx");
2572
+ try {
2573
+ writeFileSync2(fd, String(process.pid));
2574
+ } catch {
2575
+ try {
2576
+ closeSync2(fd);
2577
+ } catch {
2578
+ }
2579
+ try {
2580
+ unlinkSync(lp);
2581
+ } catch {
2582
+ }
2583
+ warn(`another nomad ${verb} running, skipping`);
2584
+ return null;
2585
+ }
2586
+ return { fd, path: lp };
2587
+ } catch {
2588
+ warn(`another nomad ${verb} running, skipping`);
2589
+ return null;
2590
+ }
2591
+ }
2592
+
2593
+ // src/commands.adopt.ts
2463
2594
  init_utils_json();
2464
2595
  var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
2465
2596
  function readMapIfPresent(repoHome2) {
2466
- const mapPath = join8(repoHome2, "path-map.json");
2597
+ const mapPath = join9(repoHome2, "path-map.json");
2467
2598
  return existsSync5(mapPath) ? readPathMap(mapPath) : { projects: {} };
2468
2599
  }
2469
2600
  function isConfiguredTarget(name, map) {
@@ -2498,7 +2629,7 @@ function adoptStopsEarly(name, linkPath, sharedTarget) {
2498
2629
  function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
2499
2630
  const ts = freshBackupTs(backup);
2500
2631
  const snapshotted = backupBeforeWrite(linkPath, ts);
2501
- const rel = join8("shared", name);
2632
+ const rel = join9("shared", name);
2502
2633
  const stage = () => gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
2503
2634
  copyIntoSharedOrFatal(name, linkPath, sharedTarget, repo);
2504
2635
  refuseLateDeniedEntries(name, linkPath, sharedTarget, repo, { snapshotted, ts });
@@ -2538,8 +2669,21 @@ function cmdAdopt(name, opts = {}) {
2538
2669
  );
2539
2670
  process.exit(1);
2540
2671
  }
2541
- const linkPath = join8(claude, name);
2542
- const sharedTarget = join8(repo, "shared", name);
2672
+ const linkPath = join9(claude, name);
2673
+ const sharedTarget = join9(repo, "shared", name);
2674
+ if (dryRun) {
2675
+ adoptPreflightAndMove(name, linkPath, sharedTarget, repo, backup, true);
2676
+ return;
2677
+ }
2678
+ const handle = acquireLock("adopt");
2679
+ if (handle === null) process.exit(EXIT.SUCCESS);
2680
+ try {
2681
+ adoptPreflightAndMove(name, linkPath, sharedTarget, repo, backup, false);
2682
+ } finally {
2683
+ releaseLock(handle);
2684
+ }
2685
+ }
2686
+ function adoptPreflightAndMove(name, linkPath, sharedTarget, repo, backup, dryRun) {
2543
2687
  if (adoptStopsEarly(name, linkPath, sharedTarget)) return;
2544
2688
  refuseDeniedEntries(name, linkPath);
2545
2689
  if (dryRun) {
@@ -2566,8 +2710,8 @@ init_config();
2566
2710
  import { existsSync as existsSync6 } from "node:fs";
2567
2711
 
2568
2712
  // src/commands.redact.core.ts
2569
- import { appendFileSync, readFileSync as readFileSync3 } from "node:fs";
2570
- import { join as join9 } from "node:path";
2713
+ import { appendFileSync, readFileSync as readFileSync4 } from "node:fs";
2714
+ import { join as join10 } from "node:path";
2571
2715
  function collectMatchIntervals(content, findings) {
2572
2716
  const intervals = [];
2573
2717
  for (const f of findings) {
@@ -2625,10 +2769,10 @@ function isAlreadyPresent(line, lines) {
2625
2769
  function appendGitleaksIgnore(fingerprint, repo) {
2626
2770
  const sanitized = fingerprint.replace(/[\r\n]/g, "").trim();
2627
2771
  if (sanitized.length === 0) return;
2628
- const ignPath = join9(repo, ".gitleaksignore");
2772
+ const ignPath = join10(repo, ".gitleaksignore");
2629
2773
  let raw;
2630
2774
  try {
2631
- raw = readFileSync3(ignPath, "utf8");
2775
+ raw = readFileSync4(ignPath, "utf8");
2632
2776
  } catch {
2633
2777
  raw = "";
2634
2778
  }
@@ -2666,121 +2810,6 @@ import { join as join11 } from "node:path";
2666
2810
  import { createInterface } from "node:readline/promises";
2667
2811
  init_utils_fs();
2668
2812
  init_utils_json();
2669
-
2670
- // src/utils.lockfile.ts
2671
- init_config();
2672
- init_utils();
2673
- import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2674
- import { dirname as dirname3, join as join10 } from "node:path";
2675
- function lockFilePath() {
2676
- return join10(home(), ".cache", "claude-nomad", "nomad.lock");
2677
- }
2678
- function acquireLock(verb) {
2679
- const lp = lockFilePath();
2680
- mkdirSync2(dirname3(lp), { recursive: true });
2681
- try {
2682
- const fd = openSync2(lp, "wx");
2683
- try {
2684
- writeFileSync2(fd, String(process.pid));
2685
- } catch (writeErr) {
2686
- try {
2687
- closeSync2(fd);
2688
- } catch {
2689
- }
2690
- try {
2691
- unlinkSync(lp);
2692
- } catch {
2693
- }
2694
- throw writeErr;
2695
- }
2696
- return { fd, path: lp };
2697
- } catch (err) {
2698
- const code = err.code;
2699
- if (code !== "EEXIST") throw err;
2700
- return checkStaleAndRetry(verb, lp);
2701
- }
2702
- }
2703
- function releaseLock(handle) {
2704
- if (handle === null) return;
2705
- const lp = handle.path;
2706
- try {
2707
- closeSync2(handle.fd);
2708
- } catch {
2709
- }
2710
- try {
2711
- unlinkSync(lp);
2712
- } catch (err) {
2713
- if (err.code !== "ENOENT") throw err;
2714
- }
2715
- }
2716
- function unlinkIfSamePid(expectedPidStr, lp) {
2717
- let current;
2718
- try {
2719
- current = readFileSync4(lp, "utf8").trim();
2720
- } catch {
2721
- return false;
2722
- }
2723
- if (current !== expectedPidStr) return false;
2724
- try {
2725
- unlinkSync(lp);
2726
- return true;
2727
- } catch {
2728
- return false;
2729
- }
2730
- }
2731
- function checkStaleAndRetry(verb, lp) {
2732
- let pidStr;
2733
- try {
2734
- pidStr = readFileSync4(lp, "utf8").trim();
2735
- } catch {
2736
- pidStr = "";
2737
- }
2738
- const pid = Number.parseInt(pidStr, 10);
2739
- if (!Number.isFinite(pid) || pid <= 0) {
2740
- if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
2741
- warn(`another nomad ${verb} running, skipping`);
2742
- return null;
2743
- }
2744
- try {
2745
- process.kill(pid, 0);
2746
- warn(`another nomad ${verb} running, skipping`);
2747
- return null;
2748
- } catch (err) {
2749
- const code = err.code;
2750
- if (code === "ESRCH") {
2751
- if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
2752
- warn(`another nomad ${verb} running, skipping`);
2753
- return null;
2754
- }
2755
- warn(`another nomad ${verb} running, skipping`);
2756
- return null;
2757
- }
2758
- }
2759
- function retryOnce(verb, lp) {
2760
- try {
2761
- const fd = openSync2(lp, "wx");
2762
- try {
2763
- writeFileSync2(fd, String(process.pid));
2764
- } catch {
2765
- try {
2766
- closeSync2(fd);
2767
- } catch {
2768
- }
2769
- try {
2770
- unlinkSync(lp);
2771
- } catch {
2772
- }
2773
- warn(`another nomad ${verb} running, skipping`);
2774
- return null;
2775
- }
2776
- return { fd, path: lp };
2777
- } catch {
2778
- warn(`another nomad ${verb} running, skipping`);
2779
- return null;
2780
- }
2781
- }
2782
-
2783
- // src/commands.capture-settings.ts
2784
2813
  init_utils();
2785
2814
  async function confirmCapture(destLabel, keys) {
2786
2815
  if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
@@ -2866,6 +2895,9 @@ var TS_SHAPE = /^\d{8}-\d{6}(-\d+)?$/;
2866
2895
  var DURATION_RE = /^(\d+)([dhm])$/;
2867
2896
  var UNIT_MS = { d: 864e5, h: 36e5, m: 6e4 };
2868
2897
  var CLEAN_DEFAULT_OLDER_THAN_MS = 14 * 24 * 60 * 60 * 1e3;
2898
+ function nameOf(d) {
2899
+ return d.name;
2900
+ }
2869
2901
  function isTsDir(name) {
2870
2902
  return TS_SHAPE.test(name);
2871
2903
  }
@@ -2878,6 +2910,19 @@ function listBackupDirs(backupBase2) {
2878
2910
  if (!existsSync8(backupBase2)) return [];
2879
2911
  return readdirSync4(backupBase2).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join12(backupBase2, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
2880
2912
  }
2913
+ function holdsNoContent(dir) {
2914
+ let entries;
2915
+ try {
2916
+ entries = readdirSync4(dir, { withFileTypes: true });
2917
+ } catch {
2918
+ return false;
2919
+ }
2920
+ for (const entry of entries) {
2921
+ if (!entry.isDirectory()) return false;
2922
+ if (!holdsNoContent(join12(dir, entry.name))) return false;
2923
+ }
2924
+ return true;
2925
+ }
2881
2926
  function prunableByAge(dirs, olderThanMs, nowMs) {
2882
2927
  return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
2883
2928
  }
@@ -2910,8 +2955,30 @@ function cmdClean(opts, backupBase2 = backupBase()) {
2910
2955
  }
2911
2956
  olderThanMs = parsed;
2912
2957
  }
2958
+ if (dryRun) {
2959
+ pruneBackups({ dryRun, olderThanMs, keep }, backupBase2);
2960
+ return;
2961
+ }
2962
+ const handle = acquireLock("clean");
2963
+ if (handle === null) process.exit(EXIT.SUCCESS);
2964
+ try {
2965
+ pruneBackups({ dryRun, olderThanMs, keep }, backupBase2);
2966
+ } finally {
2967
+ releaseLock(handle);
2968
+ }
2969
+ }
2970
+ function pruneBackups(opts, backupBase2) {
2971
+ const { dryRun, olderThanMs, keep } = opts;
2913
2972
  const dirs = listBackupDirs(backupBase2);
2914
- const targets = resolveTargets(dirs, olderThanMs, keep);
2973
+ const empty = new Set(dirs.filter((d) => holdsNoContent(join12(backupBase2, d.name))).map(nameOf));
2974
+ const aged = new Set(
2975
+ resolveTargets(
2976
+ dirs.filter((d) => !empty.has(d.name)),
2977
+ olderThanMs,
2978
+ keep
2979
+ )
2980
+ );
2981
+ const targets = dirs.filter((d) => empty.has(d.name) || aged.has(d.name)).map(nameOf);
2915
2982
  if (dryRun) {
2916
2983
  for (const name of targets) item(name);
2917
2984
  log(`dry-run: ${targets.length} backup(s) would be removed`);
@@ -3964,7 +4031,7 @@ function reportBackupsCheck(section2, backupBase2 = backupBase()) {
3964
4031
  if (count > DOCTOR_BACKUP_COUNT_WARN || sizeMb > DOCTOR_BACKUP_SIZE_WARN_MB) {
3965
4032
  addItem(
3966
4033
  section2,
3967
- `${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups --keep <N>'; bare --backups only prunes dirs older than 14d)`
4034
+ `${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups --keep <N>'; bare --backups prunes empty dirs plus those older than 14d)`
3968
4035
  );
3969
4036
  }
3970
4037
  }
@@ -8054,8 +8121,8 @@ function emitMirror(onPreview, name, localPath, repoPath) {
8054
8121
  function emitMirrorWet(onPreview, name, localPath, repoPath) {
8055
8122
  if (onPreview) onPreview({ kind: "mirror", name, localPath, repoPath });
8056
8123
  }
8057
- function notShared(policy, target) {
8058
- return !policy.adoptNew && !existsSync42(target);
8124
+ function notShared(target) {
8125
+ return !existsSync42(target);
8059
8126
  }
8060
8127
  function mirrorOneSharedName(name, claude, repo, policy, opts) {
8061
8128
  const localPath = join56(claude, name);
@@ -8064,21 +8131,21 @@ function mirrorOneSharedName(name, claude, repo, policy, opts) {
8064
8131
  try {
8065
8132
  stat = lstatSync20(localPath, { throwIfNoEntry: false });
8066
8133
  } catch (err) {
8067
- if (notShared(policy, target)) return;
8134
+ if (notShared(target)) return;
8068
8135
  if (opts.dryRun === true) {
8069
8136
  warn(
8070
- `${name} could not be read (${err.message}), so nothing was captured for it and nothing was written. A pull that captures shared edits would skip it too. Check its permissions, or whether another program has it open`
8137
+ `${name} could not be read (${errorText(err)}), so nothing was captured for it and nothing was written. A pull that captures shared edits would skip it too. Check its permissions, or whether another program has it open`
8071
8138
  );
8072
8139
  return;
8073
8140
  }
8074
8141
  warn(
8075
- `${name} could not be read (${err.message}), so it was left out of shared/ this run. Check its permissions, or whether another program has it open`
8142
+ `${name} could not be read (${errorText(err)}), so it was left out of shared/ this run. Check its permissions, or whether another program has it open`
8076
8143
  );
8077
8144
  return;
8078
8145
  }
8079
8146
  if (stat === void 0) return;
8080
8147
  if (stat.isSymbolicLink()) return;
8081
- if (notShared(policy, target)) return;
8148
+ if (notShared(target)) return;
8082
8149
  if (opts.dryRun === true) {
8083
8150
  emitMirror(opts.onPreview, name, localPath, target);
8084
8151
  return;
@@ -8102,10 +8169,10 @@ function mirrorSharedNames(map, policy, opts = {}) {
8102
8169
  }
8103
8170
  }
8104
8171
  function syncSharedLinksPush(map, opts = {}) {
8105
- mirrorSharedNames(map, { adoptNew: false, overlay: false }, opts);
8172
+ mirrorSharedNames(map, { overlay: false }, opts);
8106
8173
  }
8107
8174
  function stageLocalSharedEdits(map, ts, opts = {}) {
8108
- mirrorSharedNames(map, { adoptNew: false, overlay: true, backupTs: ts }, opts);
8175
+ mirrorSharedNames(map, { overlay: true, backupTs: ts }, opts);
8109
8176
  }
8110
8177
  function presentAt(abs) {
8111
8178
  try {
@@ -8128,7 +8195,7 @@ function removeUntrackedDenied(repo, path, segment, ts) {
8128
8195
  backupRepoWrite(abs, ts, repo);
8129
8196
  } catch (err) {
8130
8197
  warn(
8131
- `could not snapshot ${abs} before removing it (${err.message}), so it was left in place: ${denied}, so remove it by hand`
8198
+ `could not snapshot ${abs} before removing it (${errorText(err)}), so it was left in place: ${denied}, so remove it by hand`
8132
8199
  );
8133
8200
  return;
8134
8201
  }
@@ -8141,7 +8208,7 @@ function removeUntrackedDenied(repo, path, segment, ts) {
8141
8208
  const where = snapshotted ? `. A copy was snapshotted under backup/${ts}/repo/ first` : "";
8142
8209
  warn(`removed ${path} from the sync repo working tree: ${denied}${where}`);
8143
8210
  } catch (err) {
8144
- warn(`could not remove ${abs}: ${err.message}`);
8211
+ warn(`could not remove ${abs}: ${errorText(err)}`);
8145
8212
  }
8146
8213
  }
8147
8214
  function reportTrackedDenied(repo, path, segment) {
@@ -9001,12 +9068,20 @@ function handleWedge(repo, forceRemote) {
9001
9068
  return die(wedgeMarkerRunbookText(state), { code: EXIT.CONFLICT });
9002
9069
  }
9003
9070
  function runPullCore(opts = {}) {
9071
+ const backup = backupBase();
9072
+ const ts = freshBackupTs(backup);
9073
+ try {
9074
+ return runPullWithBackupTs(ts, opts);
9075
+ } finally {
9076
+ discardEmptyBackupDir(join61(backup, ts));
9077
+ }
9078
+ }
9079
+ function runPullWithBackupTs(ts, opts) {
9004
9080
  const dryRun = opts.dryRun === true;
9005
9081
  const forceRemote = opts.forceRemote === true;
9006
9082
  const compose = opts.compose === true;
9007
9083
  const repo = repoHome();
9008
9084
  const backup = backupBase();
9009
- const ts = freshBackupTs(backup);
9010
9085
  const recovered = handleWedge(repo, forceRemote);
9011
9086
  if (!dryRun) {
9012
9087
  const backupRoot = join61(backup, ts);
@@ -10638,7 +10713,7 @@ function parseSyncArgs(argv) {
10638
10713
  // package.json
10639
10714
  var package_default = {
10640
10715
  name: "claude-nomad",
10641
- version: "0.66.0",
10716
+ version: "0.67.1",
10642
10717
  type: "module",
10643
10718
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
10644
10719
  keywords: [
@@ -10867,7 +10942,9 @@ var DEFAULT_HELP = [
10867
10942
  row(" --backups", "Required: confirm backup pruning is the intended target."),
10868
10943
  row(" --dry-run", "List the snapshots that would be removed without deleting."),
10869
10944
  row(" --older-than <dur>", "Delete snapshots older than a duration (e.g. 14d, 24h, 30m)."),
10870
- cont("Default when no retention flag is given: 14d."),
10945
+ cont("Default when no retention flag is given: 14d. A snapshot holding"),
10946
+ cont("nothing is removed in every mode, whatever its age, and never"),
10947
+ cont("uses up one of the --keep slots."),
10871
10948
  row(" --keep <N>", "Keep the N most-recent snapshots, delete the rest. Mutually"),
10872
10949
  cont("exclusive with --older-than."),
10873
10950
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-nomad",
3
- "version": "0.66.0",
3
+ "version": "0.67.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": [