claude-nomad 0.36.0 → 0.37.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/README.md CHANGED
@@ -16,58 +16,10 @@ commands, tuned settings, or past conversations. **claude-nomad** keeps all of i
16
16
  private Git repo you control. `nomad push` on one machine, `nomad pull` on the next, and everything
17
17
  is there, conversations included.
18
18
 
19
- - **Resume your Claude Code [sessions](https://code.claude.com/docs/en/agent-sdk/sessions) on any
20
- machine.** Start a conversation on your desktop and pick it up on your laptop. **claude-nomad**
21
- remaps the file paths Claude Code embeds in every transcript, so your history follows you instead
22
- of getting stranded on the box where it started.
23
- - **Secret-scanned, private by default.** Your `~/.claude/` also holds OAuth tokens, MCP
24
- credentials, and the full text of every conversation, so **claude-nomad** is deliberate about what
25
- leaves your machine: credentials and ephemeral state never sync, only an explicit allow-list of
26
- paths is pushed, and everything that does go up is scanned by
27
- [gitleaks](https://github.com/gitleaks/gitleaks) before it leaves your machine; the push aborts on
28
- any hit. `nomad init` also disables Actions on your private mirror by default, so transcripts
29
- can't leak through CI logs.
30
- - **One setup, every machine.** Your agents, skills, slash commands, and settings live in one place
31
- and follow you everywhere. Per-machine tweaks like model choice, MCP URLs, and env vars merge on
32
- top instead of clobbering your shared defaults.
33
-
34
19
  Not dotfiles, not rsync. **claude-nomad** understands Claude Code's state, so your session history
35
20
  survives different file paths and your secrets never ride along.
36
21
 
37
- For anyone running Claude Code on more than one machine: a laptop and a desktop, a Mac and a WSL
38
- box, a personal rig and a work machine. [Get started in three steps.](#quickstart)
39
-
40
- ## Table of contents
41
-
42
- - [Quickstart](#quickstart)
43
- - **Concepts**
44
- - [How it works](#how-it-works)
45
- - [Repo layout](#repo-layout-what-claude-nomad-looks-like-on-a-configured-host)
46
- - [What gets synced vs. not](#what-gets-synced-vs-not)
47
- - [Path remapping](#path-remapping)
48
- - [Shared support dirs (sharedDirs)](#shared-support-dirs-shareddirs)
49
- - [Per-host overrides](#per-host-overrides)
50
- - [What does NOT sync (deliberate trade-offs)](#what-does-not-sync-deliberate-trade-offs)
51
- - **Getting started**
52
- - [Requirements](#requirements)
53
- - [Setup](#setup)
54
- - [Privacy by default](#privacy-by-default)
55
- - [First host](#first-host)
56
- - [Each additional host](#each-additional-host)
57
- - [Migrating an existing ~/.claude/](#migrating-an-existing-claude)
58
- - [Upgrading the CLI](#upgrading-the-cli)
59
- - **Reference**
60
- - [Commands](#commands)
61
- - [Recovery flows](#recovery-flows)
62
- - [Pruning old backups](#pruning-old-backups)
63
- - [`nomad drop-session <id>`](#nomad-drop-session-id)
64
- - [`nomad redact <session-id>`](#nomad-redact-session-id)
65
- - [Recovery flow: gitleaks FATAL on a session JSONL](#recovery-flow-gitleaks-fatal-on-a-session-jsonl)
66
- - [Recovery flow: push-time interactive menu](#recovery-flow-push-time-interactive-menu)
67
- - [`.gitleaks.toml` allowlist policy](#gitleakstoml-allowlist-policy)
68
- - [Customizing the allowlist with an overlay](#customizing-the-allowlist-with-an-overlay)
69
- - [Cross-OS resume](#cross-os-resume)
70
- - [Run tests](#run-tests)
22
+ **Full documentation: <https://funkadelic.github.io/claude-nomad/>**
71
23
 
72
24
  ## Quickstart
73
25
 
@@ -77,10 +29,9 @@ box, a personal rig and a work machine. [Get started in three steps.](#quickstar
77
29
  # 1. Install the CLI.
78
30
  $ npm i -g claude-nomad
79
31
 
80
- # 2. Create your private sync repo and scaffold it. nomad init uses gh to
81
- # create the repo, wire origin, and disable Actions, then scaffolds locally.
32
+ # 2. Create your private sync repo and scaffold it.
82
33
  $ nomad init # prompts for a repo name (default: claude-nomad-config)
83
- $ nomad init --repo my-config # non-interactive: use this name, no prompt
34
+ $ nomad init --repo my-config # non-interactive
84
35
 
85
36
  # 3. Add a stable host label to ~/.zshrc or ~/.bashrc, then reload.
86
37
  export NOMAD_HOST=<your-host-label>
@@ -98,7 +49,7 @@ export NOMAD_HOST=<your-host-label> # add to ~/.zshrc or ~/.bashrc
98
49
  $ nomad pull
99
50
  ```
100
51
 
101
- Then the everyday loop on any host:
52
+ Everyday loop on any host:
102
53
 
103
54
  ```bash
104
55
  $ nomad doctor # confirm setup
@@ -106,891 +57,24 @@ $ nomad pull # apply config to ~/.claude/
106
57
  $ nomad push # publish local changes (sessions, settings)
107
58
  ```
108
59
 
109
- Full walkthrough and the safe-migration sequence for a populated `~/.claude/` are in [Setup](#setup)
110
- and [Migrating an existing ~/.claude/](#migrating-an-existing-claude).
111
-
112
- ## How it works
113
-
114
- **claude-nomad** is a **tool**, not a config store. You install the CLI globally
115
- (`npm i -g claude-nomad`) and keep a separate **private** Git repo that holds only your config:
116
- `CLAUDE.md`, agents, skills, settings, session transcripts. No tool source code lives in that repo.
117
-
118
- ```text
119
- your private <your-username>/claude-nomad-config
120
- ├── shared/ (your config, synced to every host)
121
- │ ├── CLAUDE.md
122
- │ ├── agents/
123
- │ ├── skills/
124
- │ ├── commands/
125
- │ ├── rules/
126
- │ ├── hooks/
127
- │ ├── settings.base.json
128
- │ └── projects/
129
- ├── hosts/<hostname>.json
130
- └── path-map.json
131
- ```
132
-
133
- `nomad init` creates this repo for you (via `gh`) and scaffolds the directory structure in one step.
134
- Every host after the first installs the CLI, clones your private data repo to `~/claude-nomad/`, and
135
- runs `nomad pull` to sync.
136
-
137
- By default the CLI operates on `~/claude-nomad/` (see `REPO_HOME` in `src/config.ts`). Developers
138
- working from an alternate checkout can `export NOMAD_REPO=/path/to/repo` to point the CLI at their
139
- working tree without symlink gymnastics; `nomad doctor` surfaces an active override via a trailing
140
- `(NOMAD_REPO)` annotation on the repo-state line. Empty `NOMAD_REPO` falls through to the default,
141
- so a clobbered dotfile variable does not break the CLI.
142
-
143
- ## Repo layout (what `~/claude-nomad/` looks like on a configured host)
144
-
145
- ```text
146
- ~/claude-nomad/
147
- ├── shared/ # synced to every machine
148
- │ ├── CLAUDE.md
149
- │ ├── settings.base.json # baseline settings
150
- │ ├── agents/
151
- │ ├── skills/
152
- │ ├── commands/
153
- │ ├── rules/
154
- │ ├── hooks/ # hook scripts, symlinked into ~/.claude/hooks/
155
- │ ├── my-statusline.cjs # any script you want symlinked into ~/.claude/
156
- │ ├── .gitignore # defense-in-depth: blocks .claude.json, settings.local.json, *.token, *.key, *.pem, id_rsa, id_ed25519, .env, .env.*
157
- │ ├── projects/ # session transcripts under logical names
158
- │ └── extras/ # opt-in per-project content (materializes when path-map.json declares extras)
159
- ├── hosts/
160
- │ ├── <your-mac>.json # patches merged over settings.base.json
161
- │ ├── <your-wsl-host>.json
162
- │ └── <your-nuc>.json
163
- └── path-map.json # logical project -> per-host absolute path
164
- ```
165
-
166
- ## What gets synced vs. not
167
-
168
- | Category | Items | Behavior |
169
- | ----------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
170
- | **Synced** | `CLAUDE.md`, `agents/`, `skills/`, `commands/`, `rules/`, `hooks/`, `my-statusline.cjs` | Symlinked into `~/.claude/` from `shared/`. |
171
- | **Generated** | `settings.json` | Deep-merge of `settings.base.json` with `hosts/<hostname>.json`; rewritten every pull. |
172
- | **Remapped** | `projects/` session transcripts | Copied with path translation per `path-map.json`. |
173
- | **Per-project extras** | Whitelisted dirs like `.planning/`, or a root file like `CLAUDE.md` | Opt-in via the `extras` field in `path-map.json`; mirrored to/from `shared/extras/<logical>/`. |
174
- | **Shared support dirs** | Opt-in global `~/.claude/` dirs like a tool's `get-shit-done/` | Opt-in via the `sharedDirs` field in `path-map.json`; symlinked into `~/.claude/` from `shared/`. See [Shared support dirs](#shared-support-dirs-shareddirs). |
175
- | **Never synced** | OAuth and MCP state, shell history, per-host overrides, caches, scratch dirs | Per-host ephemeral state; left untouched in both directions. |
176
- | **Auto-rehydrated** | `~/.claude/plugins/cache/<plugin>/...` | Re-downloaded by Claude Code from the `enabledPlugins` list; no per-host install. |
177
-
178
- Pointers and specifics:
179
-
180
- - **Synced** link names live in `SHARED_LINKS` (and the optional `sharedDirs` field in
181
- `path-map.json` -- see [Shared support dirs](#shared-support-dirs-shareddirs)), **whitelisted
182
- extras** names in `SUPPORTED_EXTRAS`, and the full **never-synced** set in `NEVER_SYNC` (all in
183
- `src/config.ts`).
184
- - **Never synced**, in full: `~/.claude.json` (OAuth, MCP state), `.credentials.json` (OAuth
185
- credential store), `history.jsonl`, `settings.local.json` (per-host overrides),
186
- `stats-cache.json`, `todos/`, `shell-snapshots/`, `debug/`, `file-history/`, `plans/`,
187
- `session-env/`, `statsig/`, `telemetry/`, `ide/`, plus host-local caches and runtime state
188
- (`cache/`, `backups/`, `paste-cache/`, `daemon/`, `jobs/`, `tasks/`, `security/`, `sessions/`).
189
- This set is also the deny-list the `sharedDirs` opt-in is checked against, so one of these names
190
- cannot be symlinked into the shared repo by mistake.
191
- - **Per-project extras** run a pre-pull divergence WARN that flags local edits before they get
192
- overwritten.
193
-
194
- <!-- prettier-ignore -->
195
- > [!NOTE]
196
- > Plugins that depend on host-specific state (external binaries, API keys in env, MCP server
197
- > URLs) still need that side set up on each host. Put them in `hosts/<host>.json` or the plugin's
198
- > own per-host config.
199
-
200
- <!-- prettier-ignore -->
201
- > [!IMPORTANT]
202
- > Syncing a tool's `skills/` or `commands/` files copies the command shims, not the engine behind
203
- > them. If a tool keeps a binary or runtime outside `~/.claude/` (installed with `npm i -g`, a setup
204
- > script, and so on), nomad does not carry that part, so the synced commands appear on a new host but
205
- > fail until the tool itself is installed there. Install such tools once per host. For example, if you
206
- > sync the GSD (`get-shit-done`) skills, run `npm i -g get-shit-done-cc` on each host, pinned to the
207
- > version that matches your committed skills. Claude Code marketplace plugins (such as superpowers)
208
- > are the exception: they are listed in `enabledPlugins`, synced via `settings.base.json`, and
209
- > re-downloaded by Claude Code automatically, so they need no manual install.
210
-
211
- For the rationale behind these choices, see
212
- [What does NOT sync (deliberate trade-offs)](#what-does-not-sync-deliberate-trade-offs).
213
-
214
- ## Path remapping
215
-
216
- The hard problem: Claude Code stores sessions in `~/.claude/projects/<encoded-path>/` where the
217
- encoded path is the absolute path with `/` replaced by `-`. So the same logical project ends up in
218
- different directories on each host.
219
-
220
- `path-map.json` defines logical names and where the repo lives on each host. The optional `extras`
221
- block opts a project into syncing whitelisted directories (or a single root file) at its root:
222
-
223
- ```json
224
- {
225
- "projects": {
226
- "my-example-repo": {
227
- "<your-mac>": "/Users/you/code/my-example-repo",
228
- "<your-wsl-host>": "/home/you/code/my-example-repo",
229
- "<your-nuc>": "TBD"
230
- }
231
- },
232
- "extras": {
233
- "my-example-repo": [".planning", "CLAUDE.md"]
234
- }
235
- }
236
- ```
237
-
238
- <!-- prettier-ignore -->
239
- > [!IMPORTANT]
240
- > The host-label keys must match whatever you set `NOMAD_HOST=` to on each host (see
241
- > [Setup](#setup)). Mismatched labels silently skip remap, so sessions land in the wrong host's
242
- > encoded dir.
243
-
244
- Use the literal string `"TBD"` for hosts you haven't onboarded yet; `remapPull` skips TBD entries
245
- cleanly instead of creating an orphan `~/.claude/projects/TBD/`. Replace each `"TBD"` with the real
246
- path when you bring up that host.
247
-
248
- On `push`, sessions in `~/.claude/projects/-Users-you-code-my-example-repo/` get copied to
249
- `shared/projects/my-example-repo/`. On `nomad pull` on another machine, they get copied to that
250
- host's encoded path. `claude --resume` then finds them (see
251
- [What does NOT sync (deliberate trade-offs)](#what-does-not-sync-deliberate-trade-offs) for the
252
- cross-OS cwd-binding gotcha).
253
-
254
- The `extras` block is additive and back-compatible: legacy `path-map.json` files without it keep
255
- working unchanged. Each value is an array of directory or root-file names (e.g. `.planning`,
256
- `CLAUDE.md`) checked against `SUPPORTED_EXTRAS` in `src/config.ts`; anything outside that whitelist
257
- is skipped with a log line, so an unrecognized name cannot widen the sync surface.
258
-
259
- On `nomad push`, opted-in content at `<localRoot>/<name>` (a directory subtree or a single file) is
260
- copied to `shared/extras/<logical>/<name>` and goes through the same staged-tree gitleaks scan as
261
- everything else. On `nomad pull`, the reverse copy runs after `git pull --rebase`, and just before
262
- it overwrites your working tree a divergence check compares the incoming content against your local
263
- copy and prints a per-file WARN naming anything that differs.
264
-
265
- Your existing local content is backed up under `~/.cache/claude-nomad/backup/<ts>/extras/` before
266
- the pull copy lands, so an unexpected overwrite is always recoverable.
267
-
268
- ## Shared support dirs (sharedDirs)
269
-
270
- Some tools install a `hooks` block into `settings.json` whose commands point at scripts under
271
- `~/.claude/hooks/` (and sometimes a support directory such as `~/.claude/get-shit-done/`). Because
272
- `settings.json` is regenerated on every pull, that hook configuration travels to every host, but the
273
- scripts it points at did not, so hooks broke on a freshly configured host. `~/.claude/hooks/` is now
274
- a built-in synced link (it rides the same symlink model as `skills/` and `agents/`), so hook scripts
275
- travel automatically.
276
-
277
- For any other global `~/.claude/` support directory a tool needs, the optional top-level
278
- `sharedDirs` field in `path-map.json` opts it into the same symlink sync:
279
-
280
- ```json
281
- {
282
- "projects": {
283
- "my-example-repo": {
284
- "<your-mac>": "/Users/you/code/my-example-repo"
285
- }
286
- },
287
- "sharedDirs": ["get-shit-done"]
288
- }
289
- ```
290
-
291
- What this means for you: each listed name is symlinked from `shared/<name>` into `~/.claude/<name>`
292
- (the same model as the built-in synced links, not a copy), so editing it on any host updates the one
293
- shared copy. The field is additive and back-compatible: a `path-map.json` without it behaves exactly
294
- as before.
295
-
296
- Entries are validated before anything is linked. A name is accepted only if it is a single path
297
- segment (no `/`, no `..`), is not one of the never-synced names, and does not collide with a
298
- reserved `shared/` name (`settings.base.json`, the built-in synced links, `hooks`, `hosts`,
299
- `path-map.json`). An invalid entry is dropped with a warning rather than aborting the run. The
300
- contents still go through the same gitleaks scan as everything else on push, so do not point
301
- `sharedDirs` at a directory that holds credentials.
302
-
303
- First-time setup on an already-configured repo: a symlink can only form once the directory exists
304
- under `shared/`. On a fresh repo `nomad init --snapshot` handles this for you. To add `hooks/` (or a
305
- new `sharedDirs` entry) to a repo that is already set up, move it into `shared/` once on the host
306
- that has it, then let the normal flow take over:
307
-
308
- ```bash
309
- $ mv ~/.claude/hooks ~/claude-nomad/shared/hooks # one-time, on the source host
310
- $ nomad pull # re-creates ~/.claude/hooks as a symlink
311
- $ nomad push # shares it with your other hosts
312
- ```
313
-
314
- `nomad pull` never writes back to the remote, so it will not seed `shared/` for you; the one-time
315
- move is deliberate.
316
-
317
- ## Per-host overrides
318
-
319
- `settings.base.json` holds portable defaults (model, permissions, plugins).
320
- `hosts/<NOMAD_HOST>.json` holds machine-specific patches. They're deep-merged on every pull (scalars
321
- override, objects merge recursively, arrays replace). Keys that used to be force-marked per-host
322
- because they embedded absolute paths (`statusLine.command`, `hooks`) can live in
323
- `settings.base.json` if you write the commands with `$HOME` (e.g.
324
- `"command": "node \"$HOME/.claude/my-statusline.cjs\""`); Claude Code runs them through a shell so
325
- shell expansion applies. Reserve per-host files for truly machine-specific values (env, MCP URLs,
326
- host-only model overrides).
327
-
328
- `shared/settings.base.json`:
329
-
330
- ```json
331
- {
332
- "model": "claude-sonnet-4-6",
333
- "permissions": { "allow": ["Bash(npm run *)", "Bash(git status)"] }
334
- }
335
- ```
336
-
337
- `hosts/<your-other-host>.json`:
338
-
339
- ```json
340
- {
341
- "model": "claude-opus-4-8",
342
- "env": { "OLLAMA_HOST": "http://localhost:11434" }
343
- }
344
- ```
345
-
346
- Results on `your-other-host`: opus 4.8, the local Ollama env var, plus the shared permissions array.
347
-
348
- <!-- prettier-ignore -->
349
- > [!CAUTION]
350
- > Never hand-edit `~/.claude/settings.json` on a synced host. It's regenerated on every
351
- > `nomad pull` from base + host, so your edits will be clobbered. Edit the base or host file in the
352
- > repo instead.
353
-
354
- `nomad doctor` warns when `settings.json` carries a top-level key it does not recognize (a cue that
355
- Claude Code added a setting). The recognized set is kept current against Claude Code's published
356
- settings schema by a weekly automated PR in the public repo, so a periodic `nomad update` (to get
357
- the latest CLI) is what keeps that warning quiet on your hosts. To check your own `settings.json`
358
- against the live schema on demand, run `nomad doctor --check-schema`.
359
-
360
- ## What does NOT sync (deliberate trade-offs)
361
-
362
- Read these before adopting so you opt in with eyes open.
363
-
364
- - **Last-write-wins on conflicts.** Git surfaces them on merge; no field-level JSON merging.
365
- - **Manual push/pull.** No file watcher. Shell hooks recommended.
366
- - **OAuth doesn't sync.** You'll log in once per host. Intentional.
367
- - **Only sessions in `path-map.json` are remapped.** Drive-by sessions on un-mapped paths are left
368
- alone.
369
- - **Extras are opt-in and whitelisted.** Projects without an `extras` entry in `path-map.json` are
370
- unaffected. Names (a directory or a single root file) outside `SUPPORTED_EXTRAS` are skipped with
371
- a `skip ... not in SUPPORTED_EXTRAS` log line so an unrecognized name cannot widen the sync
372
- surface. Unsafe path-map values (path-traversal in `logical` keys, non-absolute or unnormalized
373
- `localRoot` values) abort the run before any file is touched, so a malformed entry fails loudly
374
- instead of corrupting state.
375
- - **Cross-OS `claude --resume` cwd binding.** Sessions embed the cwd where they were created, so
376
- Claude Code's picker's `cd ... && claude --resume <id>` line fails on a different host. Use
377
- `nomad doctor --resume-cmd <id>` for a host-local equivalent (see
378
- [Cross-OS resume](#cross-os-resume)). The sidecar approach preserves transcript byte-equality.
379
- - **Empty directories don't survive sync.** Git doesn't track empty dirs; `nomad doctor` reports
380
- them as `missing` (benign). Drop a `.gitkeep` to force materialization.
381
-
382
60
  ## Requirements
383
61
 
384
- - Node.js 22.22.1 or newer (24 LTS recommended; the npm `engines` field declares the 22.22.1 floor
385
- and surfaces a warning on older runtimes - npm only blocks the install when `engine-strict=true`
386
- is configured)
62
+ - Node.js 22.22.1 or newer (24 LTS recommended)
387
63
  - Git
388
- - [`gitleaks`](https://github.com/gitleaks/gitleaks) (required for `nomad push`, which exits with an
389
- error if it is not on PATH; `nomad doctor` also checks it against the pinned 8.30.x and warns when
390
- it is absent or mismatched)
391
- - `gh` ([GitHub CLI](https://cli.github.com/)), required by `nomad init` to create and wire the
392
- private sync repo. When `gh` is missing or unauthenticated, `nomad init` exits with a FATAL and
393
- shows install / `gh auth login` guidance. On hosts where the private repo is already set up (all
394
- subsequent hosts), `gh` is only needed by `nomad doctor`'s Actions-drift check and auto-disable;
395
- pull and push work without it.
396
-
397
- **Optional:**
398
-
399
- - [curl](https://curl.se/) or [wget](https://www.gnu.org/software/wget/), the HTTP fetcher behind
400
- the version-staleness check (`nomad doctor` latest-release line) and
401
- `nomad doctor --check-schema`. curl is tried first and wget is the fallback, so either one works.
402
- The checks soft-skip (no error, no exit-code change) when neither is present, so the rest of the
403
- CLI works without it; `nomad doctor` shows a single "HTTP fetcher (curl or wget)" row that is OK
404
- when either is installed and warns only when both are absent.
405
-
406
- ## Setup
407
-
408
- ### Privacy by default
409
-
410
- Your private sync repo must stay private. Session transcripts contain the full text of your
411
- conversations. `nomad init` disables Actions on the new repo as soon as it is created, via the
412
- GitHub API call `gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false`. What this
413
- means for you: CI workflows (which could echo transcript content into build logs) are turned off on
414
- your private data repo automatically; you do not need to remember to do it.
415
-
416
- Pass `--keep-actions` to skip the disable step (for example, when your org already enforces an
417
- Actions policy).
418
-
419
- <!-- prettier-ignore -->
420
- > [!WARNING]
421
- > If you ever make the repo public, your session transcripts (which include conversation content)
422
- > become world-readable. **Keep it private.**
423
-
424
- ### First host
425
-
426
- `nomad init` creates the private repo via `gh`, wires it as `origin`, disables Actions, scaffolds
427
- the directory layout, and pushes. The `gh` CLI must be installed and authenticated before you run
428
- it.
429
-
430
- ```bash
431
- # Install the CLI.
432
- $ npm i -g claude-nomad
433
-
434
- # Create the private sync repo and scaffold it. You will be prompted for a
435
- # repo name (default: claude-nomad-config). Pass --repo to skip the prompt.
436
- $ nomad init
437
- # or non-interactively:
438
- $ nomad init --repo my-config
439
-
440
- # If ~/.claude/ is already populated on this host, capture it as the starting
441
- # point instead of an empty scaffold. Stages shared/ and writes
442
- # hosts/<NOMAD_HOST>.json from your current ~/.claude/settings.json.
443
- # Does NOT touch the originals.
444
- $ nomad init --snapshot
445
- ```
446
-
447
- `nomad init` refuses to clobber existing scaffold artifacts, so re-running on a populated repo is a
448
- safe no-op (it errors out naming the offender). `nomad pull` against an unscaffolded repo fails fast
449
- with `FATAL: repo not initialized; run 'nomad init' to scaffold` instead of silently leaving a
450
- half-state.
451
-
452
- Add a stable host label to your shell rc, then reload it:
453
-
454
- ```bash
455
- export NOMAD_HOST=<your-host-label> # add to ~/.zshrc or ~/.bashrc
456
- ```
457
-
458
- `NOMAD_HOST` overrides `os.hostname()`, which returns noisy values like `WINDOWS-I5NT6OH` on WSL or
459
- `<name>.local` on macOS. Pick a clean label per machine (e.g., `wsl-laptop`, `macbook`,
460
- `homelab-nuc`). `nomad doctor` reports the resolved host so you can confirm.
461
-
462
- Edit `path-map.json` to add your logical projects (see [Path remapping](#path-remapping)), then:
463
-
464
- ```bash
465
- $ nomad doctor # read-only state check; reports host, repo state, every check as ✓ (pass) / ✗ (fail) / ⚠︎ (warn)
466
- $ nomad doctor --check-shared # read-only gitleaks preflight over the session transcripts a push would stage
467
- $ nomad diff # preview what nomad pull would change on this host; no lock, no network, no mutation
468
- $ nomad push # send current state to the private remote
469
- $ nomad pull # apply on another host (or this one after a remote update)
470
- ```
471
-
472
- `nomad pull --dry-run` is the network-aware twin of `nomad diff`: it acquires the lock and runs
473
- `git pull` so you see what the next real pull would do given the latest remote, then exits without
474
- mutating.
475
-
476
- If the destination host already has populated `~/.claude/{CLAUDE.md, agents/, ...}`, the first
477
- `nomad pull` will refuse to overwrite real files. See
478
- [Migrating an existing ~/.claude/](#migrating-an-existing-claude) for the safe migration flow.
479
-
480
- ### Each additional host
481
-
482
- ```bash
483
- # Install the CLI.
484
- $ npm i -g claude-nomad
485
-
486
- # Clone your private data repo.
487
- $ gh repo clone <your-username>/claude-nomad-config ~/claude-nomad
488
- # or with plain git:
489
- $ git clone git@github.com:<your-username>/claude-nomad-config.git ~/claude-nomad
490
-
491
- # Add to ~/.zshrc or ~/.bashrc, then reload.
492
- export NOMAD_HOST=<your-host-label>
493
-
494
- $ nomad pull # apply config to ~/.claude/
495
- ```
496
-
497
- `npm i -g claude-nomad` puts a `nomad` binary on your PATH. What this means for you: there is no
498
- compile step, no extra transpiler to install, and nothing is fetched from the network the first time
499
- you run `nomad`, so the first run works offline. (The Node version floor and the `engine-strict`
500
- caveat are in [Requirements](#requirements).)
501
-
502
- ## Migrating an existing ~/.claude/
503
-
504
- If a host already has real files at `~/.claude/{CLAUDE.md, agents/, skills/, ...}` and you want to
505
- bring them into the sync, the required sequence is `nomad init --snapshot` → `nomad push` →
506
- `nomad pull`:
507
-
508
- ```bash
509
- # From the host that has the canonical config (the originals are not modified):
510
- $ nomad init --snapshot # stages shared/ and writes hosts/<NOMAD_HOST>.json from ~/.claude/
511
- $ nomad push # publish the captured state to the private remote
512
-
513
- # Then, on this host or any other host that has the private remote checked out:
514
- $ nomad pull # materializes the symlinks
515
- ```
516
-
517
- `nomad pull` is what actually migrates the host. `applySharedLinks` runs a two-pass scan: any
518
- pre-existing non-symlink at a `SHARED_LINKS` path whose counterpart exists under `shared/` is
519
- renamed into `~/.cache/claude-nomad/backup/<ts>/` first, then the symlink is created. Your originals
520
- are preserved under that timestamped backup directory, not deleted. Paths whose `shared/<name>` is
521
- absent from the remote are left untouched, so a partial publish does not delete data on the
522
- destination host.
523
-
524
- If the remote has not been populated yet (you skipped `nomad init --snapshot` and `nomad push`),
525
- `nomad pull` is a no-op for SHARED_LINKS: there is nothing on the remote to symlink against, so your
526
- local `~/.claude/` files stay in place. The auto-move only triggers once the canonical state is
527
- published.
528
-
529
- Prefer an explicit tarball rollback and a confirmation prompt before any deletion? Write the
530
- equivalent under `scripts/`: tar the `SHARED_LINKS` entries under `~/.claude/` first, copy into
531
- `shared/`, prompt, then `nomad pull`. The auto-move path above is the recommended default.
532
-
533
- ## Upgrading the CLI
534
-
535
- `nomad update` updates the `nomad` binary from npm:
536
-
537
- ```bash
538
- $ nomad update
539
- ```
540
-
541
- What this means for you: it runs `npm update -g claude-nomad` and refreshes the binary on your PATH.
542
- It does NOT pull your sync data; run `nomad pull` separately when you want to apply remote changes
543
- to this host.
544
-
545
- `nomad doctor` reports when your local install is behind the latest npm release:
546
- `⚠︎ claude-nomad: <local> -> <latest> (run nomad update)`.
547
-
548
- ## Commands
549
-
550
- | Command | Description |
551
- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
552
- | `nomad init` | Create a private GitHub repo via `gh`, wire it as `origin`, disable Actions, scaffold `shared/`, `hosts/`, `path-map.json`, and push. Prompts for a repo name (default: `claude-nomad-config`). `gh` must be installed and authenticated; exits with FATAL otherwise. Refuses to clobber existing scaffold. See [Privacy by default](#privacy-by-default). |
553
- | `nomad init --repo <name>` | Non-interactive: use `<name>` as the private repo name without prompting. Useful in scripts. |
554
- | `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`. |
555
- | `nomad init --keep-actions` | Skip the Actions-disable step. Combinable with `--snapshot` and `--repo`. Use when an org policy already governs Actions, or you intentionally want CI on the private repo. |
556
- | `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. |
557
- | `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. |
558
- | `nomad diff` | Offline, lockless twin of `pull --dry-run`. No network, no lock. Works against the current local repo state. |
559
- | `nomad push` | Export local sessions and opted-in per-project extras to logical names, commit (`chore: sync from <NOMAD_HOST>`), push. |
560
- | `nomad push --dry-run` | Run pre-push safety checks (gitleaks probe, rebase, remap preview, gitlink scan, allow-list) and a read-only gitleaks leak preview over a throwaway temp copy of the sessions and extras this host would stage; skip stage, commit, and push. Exits 1 if a leak is found in the preview. Nothing is written to the sync repo. |
561
- | `nomad push --redact-all` | Redact all findings non-interactively (backup written first) without a TTY. Does not auto-Allow findings. After redaction re-stages and re-scans; aborts with the session-aware FATAL if any finding survives. Use this in scripts or when you are confident every finding is a real secret that should be scrubbed. See [Recovery flow: push-time interactive menu](#recovery-flow-push-time-interactive-menu). |
562
- | `nomad drop-session <id>` | Surgically unstage every `shared/projects/*/<id>.jsonl` and the sibling `shared/projects/*/<id>/` subagent directory from the staged tree of `~/claude-nomad/`. Idempotent; the local `~/.claude/projects/<encoded>/<id>.jsonl` and `<id>/` tree are preserved. See [Recovery flows](#recovery-flows). |
563
- | `nomad adopt <name>` | Back up, then move a pre-existing `~/.claude/<name>` directory into `shared/<name>`, recreate the symlink so this host keeps working, and stage the result for push. `<name>` must already be listed in `SHARED_LINKS` or in the `sharedDirs` field of `path-map.json`; adopt is a mover, not a config editor, so it never writes `path-map.json` itself. |
564
- | `nomad adopt <name> --dry-run` | Preview the planned backup, move, and `git add` without touching the filesystem or the git index. |
565
- | `nomad redact <session-id>` | Rewrite the secret span in the local source transcript for a session, backed up to `~/.cache/claude-nomad/backup/`. Refuses to touch a session that was modified recently (potential active session). Safe to re-run. See [`nomad redact <session-id>`](#nomad-redact-session-id). |
566
- | `nomad redact --rule <id>` | Limit redaction to findings of one gitleaks rule id only. |
567
- | `nomad redact --dry-run` | Show what `nomad redact` would change without writing anything. |
568
- | `nomad clean --backups` | Delete old backup snapshots under `~/.cache/claude-nomad/backup/`. By default removes snapshots older than 14 days; pass `--older-than <dur>` (e.g. `7d`, `24h`) to change the age, or `--keep <N>` to keep the N newest and delete the rest (the two flags cannot be combined). Always preview with `--dry-run` first. See [Pruning old backups](#pruning-old-backups). |
569
- | `nomad update` | Update the `nomad` CLI binary from npm (`npm update -g claude-nomad`). Does NOT pull your sync data; run `nomad pull` separately for that. See [Upgrading the CLI](#upgrading-the-cli). |
570
- | `nomad doctor` | Read-only health check. Each line carries a status glyph (`✓` pass, `✗` fail, `⚠︎` warn); any `✗` sets `process.exitCode = 1` (`⚠︎` does not). Includes an offline-tolerant release-version staleness check, a Hook targets check that fails (`✗`, exit 1) when `settings.json` references a hook command whose script under `~/.claude/` is missing on this host, plus a set of `⚠︎`-only checks: gitleaks version drift; on a private GitHub mirror, re-enabled Actions; optional-dependency presence (`gh` and the curl-or-wget HTTP fetcher); a backups-cache size/count nudge toward `nomad clean --backups`; an ESM/CommonJS hook-scope mismatch; and a Node-engine floor check. |
571
- | `nomad doctor --resume-cmd <id>` | Print a host-local `cd ... && claude --resume <id>` line for a session (see [Cross-OS resume](#cross-os-resume)). |
572
- | `nomad doctor --check-shared` | Read-only gitleaks preflight: stages the session transcripts a `push` would publish into a temp tree and scans them, failing (`✗`, exit 1) per affected session with rotate-and-scrub guidance. Skips with a `⚠︎` when gitleaks is not on PATH. See [Recovery flow: gitleaks FATAL on a session JSONL](#recovery-flow-gitleaks-fatal-on-a-session-jsonl). |
573
- | `nomad doctor --check-schema` | Read-only: fetches the live Claude Code settings schema and lists any `~/.claude/settings.json` key absent from it (candidates for the hand-maintained `APP_ONLY_KEYS` list). Non-fatal and offline-tolerant: skips with a `⚠︎` when neither curl nor wget is available or the schema is unreachable. |
574
- | `nomad --version` | Print the installed CLI version as bare semver to stdout; exits 0. Used by the npm-publish smoke test and useful for ad-hoc upgrade checks. |
575
-
576
- The version-check emits ``⚠︎ claude-nomad: <local> -> <latest> (run `nomad update`)`` when the local
577
- install is behind the latest upstream release, and `✓ claude-nomad: <local> (latest)` when current.
578
- It silently skips on network failures.
579
-
580
- The Hook targets check reads the live `~/.claude/settings.json` `hooks` block and fails (`✗`, exit
581
-
582
- 1. when a hook command points at a script under `~/.claude/` that is missing on this host (the
583
- freshly-configured-host symptom that motivated syncing `hooks/`). It deliberately skips any
584
- command it cannot resolve to a `~/.claude/` path (bare binaries like `jq`, unresolved env vars),
585
- so it never false-fails on a command that does not reference a local script.
586
-
587
- Two further `⚠︎`-only drift checks run in `nomad doctor`. The gitleaks version-drift line
588
- `⚠︎ gitleaks: <local> -> <pinned> (...)` fires when the local gitleaks major.minor differs from the
589
- CI-pinned `GITLEAKS_PINNED_VERSION` (gitleaks rule and allowlist behavior tracks the minor line, so
590
- a patch-only difference stays `✓`), and is silent when gitleaks is not on PATH. The mirror-Actions
591
- line (carrying a `gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false`
592
- remediation hint) fires when origin is a private GitHub mirror that is gh-authed with Actions
593
- re-enabled, complementing the auto-disable that runs on `nomad init` (see
594
- [Privacy by default](#privacy-by-default)); it is silent on every prerequisite miss (non-GitHub
595
- origin, `gh` unauthed, public repo, or Actions already off).
596
-
597
- ### Reading push and pull output
598
-
599
- `nomad push` and `nomad pull` print a grouped tree, the same left-gutter layout you already see from
600
- `nomad doctor`. There is a header line naming the command and host, then a few named sections
601
- (`Sessions`, `Extras`, and so on), each with its items hanging off `├`/`└` connectors. A status
602
- glyph leads every line: `✓` green for something that synced, `ℹ︎` dim for an informational count, `⚠︎`
603
- yellow for a warning, and `✗` red for a failure. What this means for you: instead of one long flat
604
- list with a line per project, related work is grouped and the noise is collapsed.
605
-
606
- A clean `nomad push` looks like this (one `✓` row per project whose sessions were copied up, the
607
- projects this host does not track folded into a single count, then the secret-scan result and a
608
- one-line summary):
609
-
610
- ```text
611
- push on host=workstation
612
- Sessions
613
- ├ ✓ claude-nomad
614
- ├ ✓ my-side-project
615
- └ ℹ︎ 4 not in path-map (run nomad doctor to list)
616
- Extras
617
- └ ✓ claude-nomad/.planning
618
- Leak scan
619
- └ ✓ no leaks
620
- Summary
621
- └ ✓ summary: clean
622
- ```
623
-
624
- The `ℹ︎ 4 not in path-map` row is the collapse: rather than printing one line per project that this
625
- host does not sync, push and pull now show a single count and point you at `nomad doctor`, which
626
- lists those projects by name if you want the detail. The `Leak scan` section is the secret check
627
- that runs before anything is published: `✓ no leaks` when the staged transcripts are clean. If a
628
- secret IS found, that row turns into `✗ gitleaks detected secrets in N session transcript(s)` and
629
- the full recovery block (which sessions, how to scrub them) still prints below the tree, exactly as
630
- before (see
631
- [Recovery flow: gitleaks FATAL on a session JSONL](#recovery-flow-gitleaks-fatal-on-a-session-jsonl)).
632
- The same `Leak scan` row shows up under `nomad push --dry-run`, which runs that secret scan as a
633
- read-only preview (nothing is written to the sync repo) and exits non-zero if the preview finds
634
- anything.
635
-
636
- A `nomad pull` is the mirror image, leading with the settings file it regenerated and then the
637
- sessions and extras it copied down for this host:
638
-
639
- ```text
640
- pull on host=workstation (backup=2026-05-27T14-02-09Z)
641
- Settings
642
- └ ✓ settings.json (base + workstation.json)
643
- Sessions
644
- ├ ✓ claude-nomad
645
- └ ℹ︎ 2 not in path-map (run nomad doctor to list)
646
- Extras
647
- └ ✓ claude-nomad/.planning
648
- Summary
649
- └ ✓ summary: clean
650
- ```
651
-
652
- The `Summary` row is the final verdict for the run. It reads `✓ summary: clean` when everything
653
- synced, or a `⚠︎` warning naming the counts when something was skipped:
654
-
655
- ```text
656
- ⚠︎ summary: 3 unmapped on pull (run nomad doctor to list)
657
- ⚠︎ summary: 2 unmapped on push, 1 collisions (run nomad doctor to list)
658
- ```
659
-
660
- `✓` lines go to stdout; `⚠︎` and `✗` lines go to stderr. An early, pre-tree fatal abort (for example
661
- gitleaks missing when push checks for it, or a rebase conflict before anything is staged) suppresses
662
- the tree entirely, so you do not see "summary: clean" stacked under an error. A later leak-scan
663
- finding is different: by then the tree has already been built, so it still renders in full with a
664
- `✗` Leak scan row and the recovery block below it (see
665
- [Recovery flow: gitleaks FATAL on a session JSONL](#recovery-flow-gitleaks-fatal-on-a-session-jsonl)).
666
- Projects with no entry in `path-map.json` for this host count as unmapped and fold into the
667
- collapsed `ℹ︎ ... not in path-map` count; the hint points at `nomad doctor`, which lists them by
668
- logical name.
669
-
670
- `nomad pull --dry-run` keeps its own readable preview format (a unified diff of the `settings.json`
671
- changes plus the transcripts a real pull would overwrite) rather than the grouped tree, so that
672
- preview stays easy to scan; only a real `nomad pull` prints the tree above. `nomad diff` is
673
- unchanged.
674
-
675
- ## Recovery flows
676
-
677
- ### Pruning old backups
678
-
679
- Every `nomad pull` and `nomad push` keeps you safe by copying any file it is about to overwrite into
680
- a timestamped snapshot under `~/.cache/claude-nomad/backup/<ts>/`. That is what makes an unexpected
681
- overwrite recoverable, but the snapshots are never deleted automatically, so over many syncs the
682
- folder slowly grows. It lives in your local cache and is never synced to the shared repo, so
683
- cleaning it up is purely local disk housekeeping.
684
-
685
- `nomad clean --backups` prunes those snapshots. **Always run it with `--dry-run` first** so you can
686
- see exactly which snapshots it would delete before anything is removed:
687
-
688
- ```bash
689
- $ nomad clean --backups --dry-run # list what would be deleted, remove nothing
690
- $ nomad clean --backups # delete snapshots older than 14 days (the default)
691
- ```
692
-
693
- You choose what counts as "old" in one of two ways (you cannot use both at once):
694
-
695
- - `--older-than <duration>` deletes snapshots older than the given age. The duration is a number
696
- plus a unit: `d` for days, `h` for hours, `m` for minutes (for example `7d`, `24h`, `30m`). With
697
- no retention flag at all, the default is `--older-than 14d`.
698
- - `--keep <N>` keeps the `N` most recent snapshots and deletes the rest, regardless of age.
699
-
700
- `nomad clean` only ever touches the timestamped snapshot directories directly inside the backup
701
- folder; it never follows symlinks out of it and never removes the backup folder itself. As a gentle
702
- reminder, `nomad doctor` shows a `⚠︎` warning when the backup folder grows past roughly 20 snapshots
703
- or 200 MB, nudging you to run `nomad clean --backups`. That warning is informational only and never
704
- changes the doctor exit code.
705
-
706
- ### `nomad drop-session <id>`
707
-
708
- Surgically unstages every `shared/projects/*/<id>.jsonl` plus the sibling `shared/projects/*/<id>/`
709
- subagent directory (whose nested transcripts are keyed by the same session id) from the staged tree
710
- of `~/claude-nomad/`. The local `~/.claude/projects/<encoded>/<id>.jsonl` and the local `<id>/` tree
711
- are never touched.
712
-
713
- ```bash
714
- $ nomad drop-session <id>
715
- ```
716
-
717
- Single positional id (the session filename minus `.jsonl`). Anything else (missing id, leading dash,
718
- extra arg) exits 1 with a `usage:` line.
719
-
720
- For each match in the staged tree, `cmdDropSession` (in `src/commands.drop-session.ts`) classifies
721
- the entry as tracked-in-HEAD vs newly-staged and unstages it via
722
- `git restore --staged --worktree --` or `git rm --cached -f --` respectively. The `<id>/` subagent
723
- directory is expanded into its staged entries via `git ls-files -z` so every nested transcript flows
724
- through the same per-entry classification; a session that has only a subagent directory (no flat
725
- `<id>.jsonl`) is still droppable. Idempotent: a second run on the same id sees no matching staged
726
- entries and exits 0.
727
-
728
- Exit codes:
729
-
730
- - `0` on any drop, including an idempotent re-run.
731
- - `1` with `✗ no staged session matches <id>` on stderr when neither a
732
- `shared/projects/*/<id>.jsonl` nor a `shared/projects/*/<id>/` directory with staged entries
733
- matches.
734
-
735
- What it does NOT do: touch the local `~/.claude/projects/<encoded>/<id>.jsonl` file or the local
736
- `<id>/` subagent tree. The local copies are preserved for `claude --resume`, grep recovery, or
737
- whatever the user wants. If the underlying secret is real, scrubbing or removing the local files is
738
- REQUIRED for durability, not optional housekeeping: `remapPush` (in `src/remap.ts`) re-mirrors the
739
- local content into the staged tree on the next push, so a drop without a local scrub re-stages the
740
- same secret.
741
-
742
- A successful drop prints this reminder inline, pointing at the live transcript that still needs
743
- scrubbing (the exact path when `path-map.json` maps the project to the current host, a generic
744
- `~/.claude/projects/<encoded>/<id>.jsonl` template otherwise). This is why a
745
- `nomad doctor --check-shared` run still reports the session after a drop: that scan reads the live
746
- `~/.claude/projects/` source, not the staged tree, so it keeps flagging the secret until the local
747
- transcript is scrubbed.
748
-
749
- ### `nomad redact <session-id>`
750
-
751
- Rewrites the secret span in the local source transcript at
752
- `~/.claude/projects/<encoded>/<session-id>.jsonl` in place, replacing each flagged span with
753
- `[REDACTED:<rule>]`. Before rewriting, the original transcript is backed up to
754
- `~/.cache/claude-nomad/backup/<timestamp>/`.
755
-
756
- ```bash
757
- $ nomad redact <session-id>
758
- $ nomad redact <session-id> --rule github-pat # one rule only
759
- $ nomad redact <session-id> --dry-run # preview without writing
760
- ```
761
-
762
- What it does: rewrites the LOCAL source transcript (not just the staged copy). This is the durable
763
- fix for a gitleaks finding: `nomad drop-session` only removes the staged copy, but `remapPush`
764
- re-copies from local on the next push, so the secret resurfaces. Redacting the local source means
765
- future pushes carry clean content.
766
-
767
- What it does NOT do: rotate credentials. Always rotate the secret at its provider first.
768
-
769
- Safety checks:
770
-
771
- - A session whose transcript was modified within the last 5 minutes is treated as potentially active
772
- (Claude Code may still be writing to it). `nomad redact` refuses to touch it and suggests
773
- `nomad drop-session` or waiting for the session to end.
774
- - Before every rewrite, a backup is written to `~/.cache/claude-nomad/backup/<timestamp>/`, so the
775
- original content is recoverable.
776
- - `--dry-run` prints the planned redactions and writes nothing.
777
-
778
- This command is safe to re-run: if the span was already redacted (the replacement token is already
779
- present), the content is unchanged.
780
-
781
- ### Recovery flow: gitleaks FATAL on a session JSONL
782
-
783
- `nomad push` runs `gitleaks protect --staged` before commit. To catch the same findings before you
784
- push (and without mutating anything), two read-only options are available:
785
- `nomad doctor --check-shared` scans the session transcripts a push would publish;
786
- `nomad push --dry-run` runs the same scan AND also covers opted-in extras (`.planning`,
787
- `CLAUDE.md`), which `--check-shared` does not. Both stage content into a throwaway temp copy and
788
- never write to the sync repo. A leak-scan finding is the contrast to an early, pre-tree fatal:
789
- because the scan runs after the tree is built, the push aborts but the grouped tree still renders in
790
- full, with a `✗ gitleaks detected secrets in N session transcript(s)` row in its `Leak scan`
791
- section, and then the full recovery block prints below it, naming every affected session id and the
792
- recovery command:
793
-
794
- ```text
795
- ✗ gitleaks detected secrets in 1 session transcript(s).
796
-
797
- Session <sid-aaaa>:
798
- generic-api-key (14), aws-access-token (1)
799
- Recover with: nomad drop-session <sid-aaaa>
800
-
801
- After recovery, re-run nomad push.
802
- ```
803
-
804
- Two branches from here:
805
-
806
- 1. **Real secret.** Rotate the credential at its provider first (revoke in dashboard, issue
807
- replacement) before touching anything else. Running `nomad drop-session <sid-aaaa>` clears the
808
- contaminated copy from the current staged tree, but that alone is NOT durable: `remapPush` (in
809
- `src/remap.ts`) does a full rm-and-copy mirror of your LOCAL transcripts into `shared/projects/`
810
- on every push, so the next `nomad push` re-copies the un-scrubbed local file forward and
811
- re-stages the same secret. The durable fix is to rotate AND scrub the local transcript. The
812
- easiest way: `nomad redact <sid-aaaa>` (see [`nomad redact`](#nomad-redact-session-id)), which
813
- rewrites the secret span in place with a backup. Alternatively, remove the local transcript at
814
- `~/.claude/projects/<encoded>/<sid-aaaa>.jsonl` (plus the sibling `<sid-aaaa>/` subagent
815
- directory, if present). Do not leave the local file un-scrubbed and expect the staged-tree drop
816
- to hold.
817
-
818
- 2. **False positive.** Add an allowlist regex to `.gitleaks.toml` at the repo root that matches the
819
- noise pattern but not real-secret formats, commit it, then re-run `nomad push`. The new allowlist
820
- propagates to other hosts when they run `nomad update` (CLI upgrade) or when you push the updated
821
- file to your data repo.
822
-
823
- `nomad drop-session` only acts on the staged tree of `~/claude-nomad/`. Active Claude Code sessions
824
- writing to the local file are not disturbed.
825
-
826
- ### Recovery flow: push-time interactive menu
827
-
828
- When `nomad push` detects a secret and the process is running on an interactive TTY, it presents a
829
- per-finding menu instead of aborting immediately. Each finding is shown with its rule id, file, and
830
- line number (the secret value is never printed: the scan uses `--redact`).
831
-
832
- ```text
833
- Finding: github-pat in shared/projects/my-proj/abc123.jsonl line 42 (session: abc123)
834
- [R]edact [A]llow [D]rop session [S]kip (default)
835
- >
836
- ```
837
-
838
- What the actions do:
839
-
840
- - **Redact** rewrites the secret span in the LOCAL source transcript in place (same flow as
841
- `nomad redact`), backs up first, then re-copies the file to the staged tree. Refuses if the
842
- session was modified in the last 5 minutes (potential active session): choose Drop or Skip instead
843
- and wait for the session to end.
844
- - **Allow** appends the finding's fingerprint to `.gitleaksignore` at the repo root. Use this for
845
- confirmed false positives. The fingerprint format (`file:rule:line`) is tied to the current line,
846
- so if the content moves gitleaks re-prompts rather than silently suppressing a new hit.
847
- - **Drop session** excludes this session from the current push by unstaging it from the repo's git
848
- index (same as `nomad drop-session <id>`). The local `~/.claude/projects/.../` transcript is kept
849
- intact and any running Claude session is not stopped. Not durable: the next push re-copies from
850
- local unless you also redact or remove the local transcript.
851
- - **Skip** (default on bare Enter) leaves the finding unresolved for now.
852
-
853
- After you respond to every finding, the menu applies your choices. If any finding was Skipped, the
854
- push aborts with the session-aware FATAL (same exit as a non-interactive push with findings). If all
855
- findings were resolved, the staged tree is updated and re-scanned. A clean re-scan proceeds to
856
- commit and push. If new findings appear after the first round of actions, the menu loops on the new
857
- set.
858
-
859
- On a non-TTY (CI, piped input, or scripted `nomad push`), the menu never appears and the push aborts
860
- with the existing session-aware FATAL unchanged.
861
-
862
- **Batch redact without a TTY:** `nomad push --redact-all` redacts every finding non-interactively
863
- (backup written first) without prompting and without requiring a TTY. It does not auto-Allow. After
864
- redaction the staged tree is re-scanned; any surviving finding aborts with the FATAL. Use this in
865
- scripts or when every finding is a real secret that should be scrubbed. For a single session,
866
- `nomad redact <session-id>` (see [`nomad redact`](#nomad-redact-session-id)) gives you per-session
867
- control with `--rule` and `--dry-run` options.
868
-
869
- ### `.gitleaks.toml` allowlist policy
870
-
871
- `gitleaks protect` runs against the staged tree on every `nomad push` and can flag
872
- structurally-distinguishable tool-output noise as `generic-api-key`. The repo-root `.gitleaks.toml`
873
- pre-allows four such patterns so routine pushes are not blocked:
874
-
875
- - Sonar issue keys (`AY` prefix + 20+ url-safe chars).
876
- - gitleaks fingerprint format (`<context>:<rule>:<line>` emitted by gitleaks's own reports).
877
- - npm audit advisory hashes (anchored on the JSON shape `"id":"<40..64 hex>"`).
878
- - Coverage-report line-keys (`key=<hex> <path>:<line>`).
879
-
880
- The file extends the default gitleaks ruleset, so real high-entropy secrets like `ghp_*`,
881
- `sk_live_*`, `xoxb-*`, and `AKIA*` still fire. The allowlist patterns are structurally
882
- distinguishable from real-secret formats: a malformed credential cannot match an allowlist regex by
883
- accident.
884
-
885
- ```toml
886
- [extend]
887
- useDefault = true
888
-
889
- [[allowlists]]
890
- description = "claude-nomad: structurally-distinguishable tool-output noise"
891
- regexes = [
892
- '''AY[A-Za-z0-9_-]{20,}''',
893
- '''[\w-]+:[\w-]+:\d+''',
894
- # ...see .gitleaks.toml at the repo root for the full list
895
- ]
896
- ```
897
-
898
- File location: `.gitleaks.toml` ships bundled with the CLI binary. At runtime both `probeGitleaks`
899
- (in `src/push-checks.ts`) and `runGitleaksScan` (in `src/push-gitleaks.ts`) try
900
- `<REPO_HOME>/.gitleaks.toml` first and fall back to the package-bundled copy when the repo-level
901
- file is absent. So when you have no repo-level copy the allowlist tracks the installed binary, and
902
- running `nomad update` (to get the latest CLI) is enough to receive allowlist updates. If you do
903
- place a `<REPO_HOME>/.gitleaks.toml`, it takes precedence and `nomad update` will not change it; you
904
- maintain that file yourself.
905
-
906
- #### Customizing the allowlist with an overlay
907
-
908
- What this means for you: if you only want to allow a couple of extra patterns of your own (say, an
909
- internal tool that emits a structured token that keeps tripping the scan), you do not have to copy
910
- the whole bundled allowlist into your sync repo and keep it in step by hand. Instead, drop a small
911
- `<REPO_HOME>/.gitleaks.overlay.toml` containing only your extra `[[allowlists]]` tables (and
912
- optionally `[[rules]]`). nomad layers your entries on top of the bundled allowlist at scan time, so
913
- the shipped Sonar / gitleaks / npm-audit / coverage noise allows stay in effect, the gitleaks
914
- default ruleset stays in effect, and your additions are appended to all of them.
915
-
916
- Why this is better than a full `.gitleaks.toml`: a full repo-level `.gitleaks.toml` replaces the
917
- bundled allowlist outright, so the shipped noise allows are lost and `nomad update` can no longer
918
- refresh them (you own that file). The overlay is additive instead: it never drops the bundled base,
919
- and because the base still ships with the CLI, `nomad update` keeps the base current while your
920
- overlay rides on top.
921
-
922
- How it works, briefly: on `nomad push`, when the overlay is present, nomad generates a throwaway
923
- config that extends the bundled `.gitleaks.toml` (which itself extends the gitleaks default),
924
- appends your overlay body, scans with that combined config, then deletes the throwaway file. The
925
- merge is gitleaks' own `[extend]` append, so your allowlist entries add to the shipped and default
926
- ones rather than replacing them.
927
-
928
- Two rules to keep in mind:
929
-
930
- - Your overlay must NOT contain its own `[extend]` block. nomad writes the `[extend]` line for you;
931
- if the overlay includes one, the push aborts with a clear error rather than scanning with a config
932
- you did not intend.
933
- - If you keep BOTH a full `<REPO_HOME>/.gitleaks.toml` AND an overlay, the full `.gitleaks.toml`
934
- wins and the overlay is ignored (a full repo toml means you have taken complete manual control).
935
- Pick one approach: the overlay for additive tweaks, or a full `.gitleaks.toml` for total control.
936
-
937
- Example `<REPO_HOME>/.gitleaks.overlay.toml` (note: no `[extend]` block):
938
-
939
- ```toml
940
- [[allowlists]]
941
- description = "my-org: internal build-token noise"
942
- regexes = [
943
- '''BUILDTOK-[A-Za-z0-9]{24}''',
944
- ]
945
- ```
946
-
947
- The overlay file is push-allowed (it is an exact-name entry in `PUSH_ALLOWED_STATIC` in
948
- `src/config.ts`, alongside `.gitleaksignore`), so you can commit `.gitleaks.overlay.toml` to your
949
- sync repo and it travels to your other hosts on the next `nomad pull`.
950
-
951
- Editing: amend `.gitleaks.toml` in this repo, open a PR, and merge to `main`. Use TOML literal
952
- strings (triple single quotes, `'''regex'''`) for new regex entries so backslashes do not need
953
- escaping. Verify the new pattern does not match real-secret formats (`ghp_<36>`, `sk_live_*`,
954
- `xoxb-*`, `AKIA[A-Z0-9]{16}`, etc.) before merging. The allowlist ships with the binary, so
955
- `nomad update` on each host picks up the new file.
956
-
957
- ## Cross-OS resume
958
-
959
- Claude Code embeds the original `cwd` in each session transcript. When you resume on a different
960
- host where that path doesn't exist, the picker prints a `cd <orig-cwd> && claude --resume <id>` line
961
- that fails (the source-host path isn't there).
962
-
963
- Run this instead:
964
-
965
- ```bash
966
- $ eval "$(nomad doctor --resume-cmd <session-id>)"
967
- ```
968
-
969
- Or pipe through bash:
970
-
971
- ```bash
972
- $ nomad doctor --resume-cmd <session-id> | bash
973
- ```
974
-
975
- `nomad doctor --resume-cmd <id>` reads the `.jsonl`'s recorded `cwd`, reverse-looks up the logical
976
- project in `path-map.json`, finds your current host's abspath for that logical, and prints
977
- `cd <local-abspath> && claude --resume <id>` to stdout. The command is read-only: it never modifies
978
- any transcript byte.
979
-
980
- If the session isn't mapped on this host, you'll see:
981
-
982
- ```text
983
- ✗ session <id> not mapped on this host; add the logical to path-map.json
984
- ```
985
-
986
- Other fatal surfaces: missing `~/.claude/projects/`, session id absent from every encoded dir, no
987
- `cwd` field anywhere in the transcript, missing `path-map.json`, recorded cwd not present in any
988
- logical's host map. All errors go to stderr prefixed with the red `✗` fail glyph; the success line
989
- goes to stdout as a bare shell command (no glyph) so `eval` works.
990
-
991
- ## Run tests
992
-
993
- ```bash
994
- $ npm install
995
- $ npx vitest run
996
- ```
64
+ - [`gitleaks`](https://github.com/gitleaks/gitleaks) (required for `nomad push`)
65
+ - `gh` ([GitHub CLI](https://cli.github.com/)), required by `nomad init`
66
+
67
+ **Optional:** [curl](https://curl.se/) or [wget](https://www.gnu.org/software/wget/) for the
68
+ version-staleness check and `nomad doctor --check-schema`. The CLI works without them.
69
+
70
+ ## Learn more
71
+
72
+ - [How it works](https://funkadelic.github.io/claude-nomad/how-it-works/) -- path remapping,
73
+ settings merge, what syncs and what doesn't
74
+ - [Setup and migration](https://funkadelic.github.io/claude-nomad/quickstart/) -- full setup
75
+ walkthrough, migrating an existing `~/.claude/`
76
+ - [Commands reference](https://funkadelic.github.io/claude-nomad/commands/) -- all CLI flags
77
+ - [Recovery flows](https://funkadelic.github.io/claude-nomad/recovery/) -- backups, drop-session,
78
+ redact, gitleaks allowlist
79
+ - [Contributing](https://funkadelic.github.io/claude-nomad/contributing/)
80
+ - [Security policy](https://funkadelic.github.io/claude-nomad/security/)