claude-nomad 0.25.2 → 0.25.4

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,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.25.4](https://github.com/funkadelic/claude-nomad/compare/v0.25.3...v0.25.4) (2026-05-27)
4
+
5
+
6
+ ### Documentation
7
+
8
+ * **readme:** clarify setup docs, reduce jargon, add $ command prompts ([#147](https://github.com/funkadelic/claude-nomad/issues/147)) ([d590833](https://github.com/funkadelic/claude-nomad/commit/d590833e9a2cc1503651b2a4468ca6e8269fd57c))
9
+
10
+ ## [0.25.3](https://github.com/funkadelic/claude-nomad/compare/v0.25.2...v0.25.3) (2026-05-26)
11
+
12
+
13
+ ### Changed
14
+
15
+ * **config:** add skipAutoPermissionPrompt to KNOWN_SETTINGS_KEYS ([#142](https://github.com/funkadelic/claude-nomad/issues/142)) ([09b4d7c](https://github.com/funkadelic/claude-nomad/commit/09b4d7c27d74cc922f9c516296ca6123e1ee84f5))
16
+
17
+
18
+ ### Documentation
19
+
20
+ * document runtime deps and foreground the security posture ([#145](https://github.com/funkadelic/claude-nomad/issues/145)) ([fb029ee](https://github.com/funkadelic/claude-nomad/commit/fb029eec1a6d915224a39cc2755ff76e42618365))
21
+ * refresh hero.svg tagline and doctor mockup ([#144](https://github.com/funkadelic/claude-nomad/issues/144)) ([58a0176](https://github.com/funkadelic/claude-nomad/commit/58a01760e8d977ce4cf46dcd3137c02f3bc6541c))
22
+
3
23
  ## [0.25.2](https://github.com/funkadelic/claude-nomad/compare/v0.25.1...v0.25.2) (2026-05-26)
4
24
 
5
25
 
package/README.md CHANGED
@@ -6,12 +6,12 @@
6
6
 
7
7
  ![claude-nomad - Sync your Claude Code setup. Same environment. Any machine.](docs/hero.svg)
8
8
 
9
- **Your entire Claude Code setup, on every machine. History included, secrets excluded.**
9
+ **Your entire Claude Code setup, on every machine. History included, every push secret-scanned.**
10
10
 
11
11
  Open Claude Code on a second machine and it is a blank slate: none of your custom agents, slash commands, tuned settings, or past conversations. claude-nomad keeps all of it in sync through a private Git repo you control. `nomad push` on one machine, `nomad pull` on the next, and everything is there, conversations included.
12
12
 
13
13
  - **Resume your sessions on any machine.** Start a conversation on your desktop and pick it up on your laptop. claude-nomad remaps the file paths Claude Code embeds in every transcript, so your history follows you instead of getting stranded on the box where it started.
14
- - **Private by default.** Your `~/.claude/` also holds OAuth tokens, MCP credentials, and the full text of every conversation. Every push is secret-scanned before it leaves your machine, credentials and ephemeral state never sync, and `nomad init` disables CI on your private mirror by default, so transcripts can't leak through Actions logs.
14
+ - **Secret-scanned, private by default.** Your `~/.claude/` also holds OAuth tokens, MCP credentials, and the full text of every conversation, so claude-nomad is deliberate about what leaves your machine: credentials and ephemeral state never sync, only an explicit allow-list of paths is pushed, and everything that does go up is scanned by [gitleaks](https://github.com/gitleaks/gitleaks) before it leaves your machine; the push aborts on any hit. `nomad init` also disables Actions on your private mirror by default, so transcripts can't leak through CI logs.
15
15
  - **One setup, every machine.** Your agents, skills, slash commands, and settings live in one place and follow you everywhere. Per-machine tweaks like model choice, MCP URLs, and env vars merge on top instead of clobbering your shared defaults.
16
16
 
17
17
  Not dotfiles, not rsync. claude-nomad understands Claude Code's state, so your session history survives different file paths and your secrets never ride along.
@@ -50,12 +50,12 @@ For anyone running Claude Code on more than one machine: a laptop and a desktop,
50
50
  If you already have a private claude-nomad mirror (see [Setup](#setup) for the one-time bootstrap), adding a new host is three steps:
51
51
 
52
52
  ```bash
53
- npm i -g claude-nomad
53
+ $ npm i -g claude-nomad
54
54
  ```
55
55
 
56
56
  ```bash
57
57
  # Clone your private mirror so nomad has a repo to sync into.
58
- git clone git@github.com:<your-username>/claude-nomad.git ~/claude-nomad
58
+ $ git clone git@github.com:<your-username>/claude-nomad.git ~/claude-nomad
59
59
 
60
60
  # Add to ~/.zshrc or ~/.bashrc:
61
61
  export NOMAD_HOST=<your-host-label>
@@ -68,9 +68,9 @@ export NOMAD_HOST=<your-host-label>
68
68
  Then the everyday loop:
69
69
 
70
70
  ```bash
71
- nomad doctor # confirm setup
72
- nomad pull # apply config to ~/.claude/
73
- nomad push # publish local changes (sessions, settings)
71
+ $ nomad doctor # confirm setup
72
+ $ nomad pull # apply config to ~/.claude/
73
+ $ nomad push # publish local changes (sessions, settings)
74
74
  ```
75
75
 
76
76
  First-host bootstrap and the safe-migration sequence for a populated `~/.claude/` are in [Setup](#setup) and [Migrating an existing ~/.claude/](#migrating-an-existing-claude).
@@ -150,14 +150,14 @@ The hard problem: Claude Code stores sessions in `~/.claude/projects/<encoded-pa
150
150
  ```json
151
151
  {
152
152
  "projects": {
153
- "ha-acwd": {
154
- "<your-mac>": "/Users/you/code/ha-acwd",
155
- "<your-wsl-host>": "/home/you/code/ha-acwd",
153
+ "my-example-repo": {
154
+ "<your-mac>": "/Users/you/code/my-example-repo",
155
+ "<your-wsl-host>": "/home/you/code/my-example-repo",
156
156
  "<your-nuc>": "TBD"
157
157
  }
158
158
  },
159
159
  "extras": {
160
- "ha-acwd": [".planning", "CLAUDE.md"]
160
+ "my-example-repo": [".planning", "CLAUDE.md"]
161
161
  }
162
162
  }
163
163
  ```
@@ -167,9 +167,13 @@ The hard problem: Claude Code stores sessions in `~/.claude/projects/<encoded-pa
167
167
 
168
168
  Use the literal string `"TBD"` for hosts you haven't onboarded yet; `remapPull` skips TBD entries cleanly instead of creating an orphan `~/.claude/projects/TBD/`. Replace each `"TBD"` with the real path when you bring up that host.
169
169
 
170
- On `push`, sessions in `~/.claude/projects/-Users-you-code-ha-acwd/` get copied to `shared/projects/ha-acwd/`. On `pull` on another machine, they get copied to that host's encoded path. `claude --resume` then finds them (see [What does NOT sync (deliberate trade-offs)](#what-does-not-sync-deliberate-trade-offs) for the cross-OS cwd-binding gotcha).
170
+ On `push`, sessions in `~/.claude/projects/-Users-you-code-my-example-repo/` get copied to `shared/projects/my-example-repo/`. On `pull` on another machine, they get copied to that host's encoded path. `claude --resume` then finds them (see [What does NOT sync (deliberate trade-offs)](#what-does-not-sync-deliberate-trade-offs) for the cross-OS cwd-binding gotcha).
171
171
 
172
- The `extras` block is additive and back-compatible: legacy `path-map.json` files without it continue to work unchanged. Each value is an array of directory or root-file names (e.g. `.planning`, `CLAUDE.md`) validated against `SUPPORTED_EXTRAS` in `src/config.ts`; values outside the whitelist are skipped with a log line so an unrecognized name cannot widen the sync surface. On `push`, opted-in content at `<localRoot>/<name>` (a directory subtree or a single file) is copied to `shared/extras/<logical>/<name>` and inherits the staged-tree gitleaks scan. On `pull`, the reverse copy runs after `git pull --rebase`; just before it overwrites your working tree, a divergence check compares the incoming content against your local copy and emits a per-file WARN naming the diverging files. The existing local content is backed up to `~/.cache/claude-nomad/backup/<ts>/extras/<encoded-localRoot>/<rel>/` before the pull copy lands (`<encoded-localRoot>` is the `localRoot` with `/` rewritten as `-`, so two opted-in projects with the same relative extras path do not collide in one backup run).
172
+ The `extras` block is additive and back-compatible: legacy `path-map.json` files without it keep working unchanged. Each value is an array of directory or root-file names (e.g. `.planning`, `CLAUDE.md`) checked against `SUPPORTED_EXTRAS` in `src/config.ts`; anything outside that whitelist is skipped with a log line, so an unrecognized name cannot widen the sync surface.
173
+
174
+ On `push`, opted-in content at `<localRoot>/<name>` (a directory subtree or a single file) is copied to `shared/extras/<logical>/<name>` and goes through the same staged-tree gitleaks scan as everything else. On `pull`, the reverse copy runs after `git pull --rebase`, and just before it overwrites your working tree a divergence check compares the incoming content against your local copy and prints a per-file WARN naming anything that differs.
175
+
176
+ Your existing local content is backed up under `~/.cache/claude-nomad/backup/<ts>/extras/` before the pull copy lands, so an unexpected overwrite is always recoverable.
173
177
 
174
178
  ## Per-host overrides
175
179
 
@@ -206,7 +210,7 @@ Read these before adopting so you opt in with eyes open.
206
210
  - **Manual push/pull.** No file watcher. Shell hooks recommended.
207
211
  - **OAuth doesn't sync.** You'll log in once per host. Intentional.
208
212
  - **Only sessions in `path-map.json` are remapped.** Drive-by sessions on un-mapped paths are left alone.
209
- - **Extras are opt-in and whitelisted.** Projects without an `extras` entry in `path-map.json` are unaffected. Names (a directory or a single root file) outside `SUPPORTED_EXTRAS` are skipped with a `skip ... not in SUPPORTED_EXTRAS` log line so an unrecognized name cannot widen the sync surface. Unsafe path-map values (path-traversal in `logical` keys, non-absolute or unnormalized `localRoot` values) FATAL before any filesystem mutation via `assertSafeLogical` / `assertSafeLocalRoot` in `src/extras-sync.ts`.
213
+ - **Extras are opt-in and whitelisted.** Projects without an `extras` entry in `path-map.json` are unaffected. Names (a directory or a single root file) outside `SUPPORTED_EXTRAS` are skipped with a `skip ... not in SUPPORTED_EXTRAS` log line so an unrecognized name cannot widen the sync surface. Unsafe path-map values (path-traversal in `logical` keys, non-absolute or unnormalized `localRoot` values) abort the run before any file is touched, so a malformed entry fails loudly instead of corrupting state.
210
214
  - **Cross-OS `claude --resume` cwd binding.** Sessions embed the cwd where they were created, so the picker's `cd ... && claude --resume <id>` line fails on a different host. Use `nomad doctor --resume-cmd <id>` for a host-local equivalent (see [Cross-OS resume](#cross-os-resume)). The sidecar approach preserves transcript byte-equality.
211
215
  - **Empty directories don't survive sync.** Git doesn't track empty dirs; `nomad doctor` reports them as `missing` (benign). Drop a `.gitkeep` to force materialization.
212
216
 
@@ -215,18 +219,24 @@ Read these before adopting so you opt in with eyes open.
215
219
  - Node.js 22.22.1 or newer (24 LTS recommended; the npm `engines` field declares the 22.22.1 floor and surfaces a warning on older runtimes - npm only blocks the install when `engine-strict=true` is configured)
216
220
  - `tsx` (ships as a runtime dependency of the published package; no separate global install required)
217
221
  - Git
222
+ - [`gitleaks`](https://github.com/gitleaks/gitleaks) (required for `nomad push`, which exits with an error if it is not on PATH; `nomad doctor` also checks it against the pinned 8.30.x and warns when it is absent or mismatched)
218
223
  - A **private** GitHub repo (or any Git remote you control)
219
224
 
225
+ **Optional:**
226
+
227
+ - `gh` (GitHub CLI), used only by `nomad init` to auto-disable Actions on the private repo; if it is missing or unauthenticated, init prints a manual fallback tip and continues
228
+ - `curl`, used only by the version/update check (the `nomad doctor` latest-release line and the post-`nomad update` check); it degrades silently when curl is absent or offline, so the rest of the CLI works without it
229
+
220
230
  ## Setup
221
231
 
222
232
  **Why not just fork?** GitHub doesn't let you flip a public fork to private, and your config (especially session transcripts) must stay private. So the bootstrap is a one-time mirror-push into a fresh private repo, not a fork.
223
233
 
224
234
  ### Privacy by default
225
235
 
226
- Your private mirror has two layers of defense against leaking transcripts via CI, both applied automatically:
236
+ When you mirror-push the tool into your repo, you copy its automation along with its code: the `.github/workflows/` directory holds the public project's own CI (running its test suite, linting, secret and code scanning, release tagging, and npm publishing). That CI is meant for the public project, not your config; if it ran on your private mirror, a job could echo transcript contents into build logs. So your mirror gets two independent layers of defense against that, both applied automatically:
227
237
 
228
- 1. Every workflow under `.github/workflows/` is gated on `${{ !github.event.repository.private }}`, so they skip on private repos and only run on public ones.
229
- 2. `nomad init` calls `gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false` on first run, turning Actions off at the repo level. Requires `gh` CLI authed; if missing or unauthed, init logs a manual fallback tip and continues.
238
+ 1. **The workflows are written to skip private repos.** Each one carries the run condition `${{ !github.event.repository.private }}` (in plain terms: "run only when this repo is NOT private"), so even with Actions enabled the jobs do not run on your mirror.
239
+ 2. **`nomad init` turns Actions off for the whole repo** on first run, via the GitHub API call `gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false`. This needs the `gh` CLI installed and authed; if it is missing or unauthed, init logs a manual fallback tip and continues.
230
240
 
231
241
  Pass `--keep-actions` to either form of init to skip step 2 (for example, when your org already enforces an Actions policy upstream).
232
242
 
@@ -235,34 +245,32 @@ Pass `--keep-actions` to either form of init to skip step 2 (for example, when y
235
245
 
236
246
  ### Bootstrap
237
247
 
238
- Steps 1-2 are once-ever across all hosts; step 3 repeats per host:
248
+ Steps 1-2 are once-ever across all hosts; steps 3-4 repeat on every host:
239
249
 
240
250
  ```bash
241
251
  # 1. Create the private repo (or use the GitHub UI). Once, ever.
242
- gh repo create <your-username>/claude-nomad --private
252
+ $ gh repo create <your-username>/claude-nomad --private
243
253
 
244
- # 2. Mirror the public tool into it. This severs the fork relationship,
245
- # so your repo is independent of upstream. Once, ever.
246
- git clone --bare git@github.com:funkadelic/claude-nomad.git /tmp/cn.git
247
- cd /tmp/cn.git
248
- git push --mirror git@github.com:<your-username>/claude-nomad.git
249
- cd .. && rm -rf /tmp/cn.git
254
+ # 2. Copy the public tool into your private repo. A bare clone followed by a
255
+ # mirror push makes a complete, independent copy (every branch and tag) with
256
+ # no fork link back to upstream, which is what lets you keep it private. Once, ever.
257
+ $ git clone --bare git@github.com:funkadelic/claude-nomad.git /tmp/cn.git # download a full copy
258
+ $ cd /tmp/cn.git
259
+ $ git push --mirror git@github.com:<your-username>/claude-nomad.git # upload it to your private repo
260
+ $ cd .. && rm -rf /tmp/cn.git
250
261
 
251
262
  # 3. Install the CLI globally and clone your private copy. Repeat on every host.
252
- npm i -g claude-nomad
253
- git clone git@github.com:<your-username>/claude-nomad.git ~/claude-nomad
254
- ```
255
-
256
- `npm i -g claude-nomad` puts a `nomad` binary on your PATH. The bin shim is the existing `src/nomad.ts` entrypoint resolved through tsx (a runtime dependency); no compile step. The npm `engines` field declares the 22.22.1 floor and surfaces a warning on older runtimes; npm only blocks the install when `engine-strict=true` is configured.
257
-
258
- On every additional host you only repeat step 3 (the global install is per-host; your private repo already exists on the remote from step 2).
263
+ $ npm i -g claude-nomad
264
+ $ git clone git@github.com:<your-username>/claude-nomad.git ~/claude-nomad
259
265
 
260
- Add to `~/.zshrc` or `~/.bashrc`:
261
-
262
- ```bash
266
+ # 4. Add a stable host label to your shell rc (~/.zshrc or ~/.bashrc). Repeat on every host.
263
267
  export NOMAD_HOST=<your-host-label> # any short, stable label; nomad reads this instead of os.hostname()
264
268
  ```
265
269
 
270
+ `npm i -g claude-nomad` puts a `nomad` binary on your PATH. The bin shim is the existing `src/nomad.ts` entrypoint resolved through tsx (a runtime dependency); no compile step. (The Node version floor and the `engine-strict` caveat are in [Requirements](#requirements).)
271
+
272
+ On every additional host you repeat only steps 3-4; steps 1-2 are already done, since your private repo lives on the remote from step 2.
273
+
266
274
  `NOMAD_HOST` overrides `os.hostname()`, which returns noisy values like `WINDOWS-I5NT6OH` on WSL or `<name>.local` on macOS. Pick a clean label per machine (e.g., `wsl-laptop`, `macbook`, `homelab-nuc`). `nomad doctor` reports the resolved host so you can confirm.
267
275
 
268
276
  ### Initialize the repo layout
@@ -271,15 +279,15 @@ First host only; subsequent hosts just clone and `nomad pull`. Both forms below
271
279
 
272
280
  ```bash
273
281
  # Fresh start: scaffold an empty shared/, hosts/, path-map.json skeleton.
274
- nomad init
282
+ $ nomad init
275
283
 
276
284
  # Already have ~/.claude/ populated on this host? Capture it as the
277
285
  # starting point. Stages shared/ and writes hosts/<NOMAD_HOST>.json from
278
286
  # your current ~/.claude/settings.json. Does NOT touch the originals.
279
- nomad init --snapshot
287
+ $ nomad init --snapshot
280
288
 
281
289
  # Either form accepts --keep-actions to skip the auto-disable.
282
- nomad init --keep-actions
290
+ $ nomad init --keep-actions
283
291
  ```
284
292
 
285
293
  `nomad init` refuses to clobber existing scaffold artifacts, so re-running on a populated repo is a safe no-op (it errors out naming the offender). `nomad pull` against an unscaffolded repo fails fast with `FATAL: repo not initialized; run 'nomad init' to scaffold` instead of silently leaving a half-state.
@@ -287,11 +295,11 @@ nomad init --keep-actions
287
295
  Edit `path-map.json` to add your logical projects (see [Path remapping](#path-remapping)), then:
288
296
 
289
297
  ```bash
290
- nomad doctor # read-only state check; reports host, repo state, every check as ✓ (pass) / ✗ (fail) / ⚠︎ (warn)
291
- nomad doctor --check-shared # read-only gitleaks preflight over the session transcripts a push would stage
292
- nomad diff # preview what nomad pull would change on this host; no lock, no network, no mutation
293
- nomad push # send current state to the private remote
294
- nomad pull # apply on another host (or this one after a remote update)
298
+ $ nomad doctor # read-only state check; reports host, repo state, every check as ✓ (pass) / ✗ (fail) / ⚠︎ (warn)
299
+ $ nomad doctor --check-shared # read-only gitleaks preflight over the session transcripts a push would stage
300
+ $ nomad diff # preview what nomad pull would change on this host; no lock, no network, no mutation
301
+ $ nomad push # send current state to the private remote
302
+ $ nomad pull # apply on another host (or this one after a remote update)
295
303
  ```
296
304
 
297
305
  `nomad pull --dry-run` is the network-aware twin of `nomad diff`: it acquires the lock and runs `git pull` so you see what the next real pull would do given the latest remote, then exits without mutating.
@@ -304,11 +312,11 @@ If a host already has real files at `~/.claude/{CLAUDE.md, agents/, skills/, ...
304
312
 
305
313
  ```bash
306
314
  # From the host that has the canonical config (the originals are not modified):
307
- nomad init --snapshot # stages shared/ and writes hosts/<NOMAD_HOST>.json from ~/.claude/
308
- nomad push # publish the captured state to the private remote
315
+ $ nomad init --snapshot # stages shared/ and writes hosts/<NOMAD_HOST>.json from ~/.claude/
316
+ $ nomad push # publish the captured state to the private remote
309
317
 
310
318
  # Then, on this host or any other host that has the private remote checked out:
311
- nomad pull # materializes the symlinks
319
+ $ nomad pull # materializes the symlinks
312
320
  ```
313
321
 
314
322
  `nomad pull` is what actually migrates the host. `applySharedLinks` runs a two-pass scan: any pre-existing non-symlink at a `SHARED_LINKS` path whose counterpart exists under `shared/` is renamed into `~/.cache/claude-nomad/backup/<ts>/` first, then the symlink is created. Your originals are preserved under that timestamped backup directory, not deleted. Paths whose `shared/<name>` is absent from the remote are left untouched, so a partial publish does not delete data on the destination host.
@@ -319,38 +327,42 @@ Prefer an explicit tarball rollback and a confirmation prompt before any deletio
319
327
 
320
328
  ## Upgrading the tool
321
329
 
322
- Two upgrade paths, depending on how you installed:
330
+ Two different things can fall behind, and they update independently:
331
+
332
+ - **The `nomad` CLI binary** (what runs when you type `nomad`). If you installed it with `npm i -g claude-nomad`, upgrade it with `npm update -g claude-nomad`. This refreshes only the binary on your PATH; it does not touch anything inside your private `~/claude-nomad/` repo.
333
+ - **The synced tool files inside your private repo:** `src/`, `.gitleaks.toml` (the secret-scan allowlist), and the `.github/workflows/` privacy gating. These were copied from the public repo at bootstrap and then froze, so `npm update -g` does not refresh them. `nomad update`, run from `~/claude-nomad/`, is what pulls newer versions of these files in. Topology-aware: detects vanilla vs fork remotes, pulls or merges upstream, and re-runs `npm install` when `package-lock.json` shifted.
323
334
 
324
- - **Global install (`npm i -g claude-nomad`):** `npm update -g claude-nomad`. This refreshes only the `nomad` CLI binary on PATH; your private `~/claude-nomad/` repo is untouched.
325
- - **Source-checkout developer workflow:** `nomad update` (run from `~/claude-nomad/`). Topology-aware: detects vanilla vs fork remotes, pulls or merges upstream, and re-runs `npm install` when `package-lock.json` shifted.
335
+ Most people who followed the Quickstart need both: `npm update -g` for the binary, and an occasional `nomad update` for the repo files (notably to receive `.gitleaks.toml` allowlist changes and any update to the privacy gating itself). The mirror-push bootstrap leaves your repo with `origin` on your private mirror and no `upstream` remote; that becomes the "fork" topology `nomad update` expects once you add the upstream remote (the one-time `git remote add upstream ...` step is below).
326
336
 
327
337
  Your private repo is not a fork, so GitHub's "Sync fork" UI doesn't apply. The shortcut on a source-checkout host is:
328
338
 
329
339
  ```bash
330
- cd ~/claude-nomad
331
- nomad update
340
+ $ cd ~/claude-nomad
341
+ $ nomad update
332
342
  ```
333
343
 
334
- `nomad update` (see `cmdUpdate` in `src/commands.update.ts`) detects which layout your `~/claude-nomad/` uses and does the right thing:
344
+ `nomad update` detects which layout your `~/claude-nomad/` uses and does the right thing:
335
345
 
336
346
  - **vanilla** (`origin` points at the public repo): `git pull --ff-only origin main`.
337
347
  - **fork** (`upstream` points at the public repo, `origin` points at your private mirror): `git fetch upstream`, then (before merging) commit any whitelisted `shared/extras/` content that is still untracked locally so an overlap with upstream becomes a normal file merge instead of an untracked-overwrite abort, `git merge upstream/main`, then prompt before pushing the merge to `origin/main`. Pass `--push-origin` to skip the prompt. When the merge is a no-op (HEAD unchanged, nothing new to push) the prompt is skipped entirely and `nomad update` logs `already in sync with origin/main`.
338
348
 
339
- Pre-flight checks run before any mutation: `REPO_HOME` exists, topology resolves to `vanilla` or `fork`, current branch is `main`, working tree is clean per `git status --porcelain -z` (override with `--force`), and `--push-origin` is rejected on vanilla topology. After the merge or pull, `nomad update` re-runs `npm install` only when `package-lock.json` actually shifted, commits the regenerated `package-lock.json` (fork topology) if the reinstall changed it, then invokes `nomad doctor`. The trailing version-check is non-fatal: `✓` when local matches the latest release, `⚠︎` when behind, an informational `ℹ︎ ... ahead of latest release` line when ahead (e.g. a `-dev` build between releases), and silent on network failures.
349
+ Pre-flight checks run before any mutation: `REPO_HOME` exists, the topology resolves to `vanilla` or `fork`, the current branch is `main`, the working tree is clean (override with `--force`), and `--push-origin` is rejected on vanilla topology.
350
+
351
+ After the merge or pull, `nomad update` re-runs `npm install` only when `package-lock.json` actually shifted, commits the regenerated `package-lock.json` (fork topology) if the reinstall changed it, then invokes `nomad doctor`. The trailing version-check is non-fatal: `✓` when local matches the latest release, `⚠︎` when behind, an informational `ℹ︎ ... ahead of latest release` line when ahead (e.g. a `-dev` build between releases), and silent on network failures.
340
352
 
341
353
  Common cases:
342
354
 
343
355
  ```bash
344
- nomad update # the usual path
345
- nomad update --dry-run # detect topology + pre-flight, print would-be git commands only
346
- nomad update --push-origin # fork topology: push merge to origin/main without prompting
347
- nomad update --force # proceed past a dirty working tree
356
+ $ nomad update # the usual path
357
+ $ nomad update --dry-run # detect topology + pre-flight, print would-be git commands only
358
+ $ nomad update --push-origin # fork topology: push merge to origin/main without prompting
359
+ $ nomad update --force # proceed past a dirty working tree
348
360
  ```
349
361
 
350
362
  One-time setup if you're running a fork layout and don't have the `upstream` remote yet:
351
363
 
352
364
  ```bash
353
- git remote add upstream git@github.com:funkadelic/claude-nomad.git
365
+ $ git remote add upstream git@github.com:funkadelic/claude-nomad.git
354
366
  ```
355
367
 
356
368
  To pin to a specific release (`vX.Y.Z`, tagged by release-please) instead of tracking `main`, fetch tags from the public repo and check out the tag (detached HEAD). On vanilla topology that's `origin`; on fork topology that's `upstream` (the private mirror at `origin` does not accumulate upstream release tags). Example: `git fetch upstream --tags && git switch --detach vX.Y.Z` (substitute `origin` for vanilla; use `git checkout vX.Y.Z` on older Git).
@@ -364,7 +376,7 @@ If you installed an earlier version via `./install.sh` and a shell alias (the pr
364
376
  | `nomad init` | Scaffold empty `shared/`, `hosts/`, `path-map.json` on a fresh clone. Refuses to clobber existing scaffold. Auto-disables Actions on a detected private GitHub mirror (see [Privacy by default](#privacy-by-default)). |
365
377
  | `nomad init --snapshot` | Overlay current host's `~/.claude/` into `shared/` and write `~/.claude/settings.json` verbatim into `hosts/<NOMAD_HOST>.json`. Originals not modified. Same auto-disable behavior as `nomad init`. |
366
378
  | `nomad init --keep-actions` | Skip the auto-disable. Combinable with `--snapshot`. Use when an upstream org policy already governs Actions, or you intentionally want CI on the private mirror. |
367
- | `nomad pull` | `git pull --rebase --autostash`, apply symlinks, regenerate `settings.json`, remap session paths, and pull opted-in per-project extras. FATAL if scaffold missing. |
379
+ | `nomad pull` | `git pull --rebase --autostash`, apply symlinks, regenerate `settings.json`, remap session paths, and pull opted-in per-project extras. Errors out if scaffold missing. |
368
380
  | `nomad pull --dry-run` | Network-aware preview: acquire lock + `git pull --rebase`, print planned changes (symlink moves, `settings.json` diff, transcript overwrites), exit without writing. |
369
381
  | `nomad diff` | Offline, lockless twin of `pull --dry-run`. No network, no lock. Works against the current local repo state. |
370
382
  | `nomad push` | Export local sessions and opted-in per-project extras to logical names, commit (`chore: sync from <NOMAD_HOST>`), push. |
@@ -397,7 +409,7 @@ Every `nomad pull`, `nomad push`, and `nomad diff` run ends with a single `summa
397
409
  Surgically unstages every `shared/projects/*/<id>.jsonl` plus the sibling `shared/projects/*/<id>/` subagent directory (whose nested transcripts are keyed by the same session id) from the staged tree of `~/claude-nomad/`. The local `~/.claude/projects/<encoded>/<id>.jsonl` and the local `<id>/` tree are never touched.
398
410
 
399
411
  ```bash
400
- nomad drop-session <id>
412
+ $ nomad drop-session <id>
401
413
  ```
402
414
 
403
415
  Single positional id (the session filename minus `.jsonl`). Anything else (missing id, leading dash, extra arg) exits 1 with a `usage:` line.
@@ -413,7 +425,7 @@ What it does NOT do: touch the local `~/.claude/projects/<encoded>/<id>.jsonl` f
413
425
 
414
426
  ### Recovery flow: gitleaks FATAL on a session JSONL
415
427
 
416
- `nomad push` runs `gitleaks protect --staged` before commit. To catch the same findings before you push (and without mutating anything), run the read-only preflight `nomad doctor --check-shared`, which stages and scans the exact transcripts a push would publish. When findings live in a session transcript, the push FATAL names every affected session id and the recovery command:
428
+ `nomad push` runs `gitleaks protect --staged` before commit. To catch the same findings before you push (and without mutating anything), run the read-only preflight `nomad doctor --check-shared`, which stages and scans the exact transcripts a push would publish. When findings live in a session transcript, the push aborts and names every affected session id and the recovery command:
417
429
 
418
430
  ```text
419
431
  ✗ gitleaks detected secrets in 1 session transcript(s).
@@ -468,16 +480,16 @@ Claude Code embeds the original `cwd` in each session transcript. When you resum
468
480
  Run this instead:
469
481
 
470
482
  ```bash
471
- eval "$(nomad doctor --resume-cmd <session-id>)"
483
+ $ eval "$(nomad doctor --resume-cmd <session-id>)"
472
484
  ```
473
485
 
474
486
  Or pipe through bash:
475
487
 
476
488
  ```bash
477
- nomad doctor --resume-cmd <session-id> | bash
489
+ $ nomad doctor --resume-cmd <session-id> | bash
478
490
  ```
479
491
 
480
- `nomad doctor --resume-cmd <id>` reads the `.jsonl`'s recorded `cwd`, reverse-looks up the logical project in `path-map.json`, finds your current host's abspath for that logical, and prints `cd <local-abspath> && claude --resume <id>` to stdout. The command is read-only: it never modifies any transcript byte (Phase 1's sha256 byte-equality invariant is preserved).
492
+ `nomad doctor --resume-cmd <id>` reads the `.jsonl`'s recorded `cwd`, reverse-looks up the logical project in `path-map.json`, finds your current host's abspath for that logical, and prints `cd <local-abspath> && claude --resume <id>` to stdout. The command is read-only: it never modifies any transcript byte.
481
493
 
482
494
  If the session isn't mapped on this host, you'll see:
483
495
 
@@ -490,6 +502,6 @@ Other fatal surfaces: missing `~/.claude/projects/`, session id absent from ever
490
502
  ## Run tests
491
503
 
492
504
  ```bash
493
- npm install
494
- npx vitest run
505
+ $ npm install
506
+ $ npx vitest run
495
507
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-nomad",
3
- "version": "0.25.2",
3
+ "version": "0.25.4",
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": [
@@ -123,13 +123,13 @@ function saveCache(latest: string): void {
123
123
  /**
124
124
  * Fetch the latest release tag from the upstream GitHub releases API. Uses
125
125
  * `execFileSync('curl', ...)` rather than `node:https` because curl honors
126
- * system proxies, respects the `-m` timeout reliably, and is already a
127
- * required dependency on every supported host (push uses gitleaks; pull uses
128
- * git). 3-second timeout, fail-fast on non-2xx (`-f`), silent (`-s`), follow
129
- * redirects (`-L`). Returns `null` on ANY failure path including a missing
130
- * `tag_name` field or a tag that fails strict-semver validation after the
131
- * leading `v` strip. Release tags ship as `v<semver>` per
132
- * `release-please-config.json`'s `include-v-in-tag: true`.
126
+ * system proxies and respects the `-m` timeout reliably. curl is optional, not
127
+ * a hard dependency: this is its only consumer, so a host without curl simply
128
+ * skips the version line. 3-second timeout, fail-fast on non-2xx (`-f`), silent
129
+ * (`-s`), follow redirects (`-L`). Returns `null` on ANY failure path (curl
130
+ * missing from PATH, a missing `tag_name` field, or a tag that fails
131
+ * strict-semver validation after the leading `v` strip). Release tags ship as
132
+ * `v<semver>` per `release-please-config.json`'s `include-v-in-tag: true`.
133
133
  */
134
134
  function fetchLatestTag(): string | null {
135
135
  try {
package/src/config.ts CHANGED
@@ -139,6 +139,7 @@ export const KNOWN_SETTINGS_KEYS = new Set<string>([
139
139
  'pluginRepositoryEnabled',
140
140
  'pluginsLocalConfig',
141
141
  'proxy',
142
+ 'skipAutoPermissionPrompt',
142
143
  'statsig',
143
144
  'statusLine',
144
145
  'subagents',