claude-nomad 0.35.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,889 +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/), used by the version-staleness check (`nomad doctor` latest-release line)
400
- and by `nomad doctor --check-schema`; it degrades silently when curl is absent or offline, so the
401
- rest of the CLI works without it. `nomad doctor` reports its presence in the Version Checks
402
- section.
403
-
404
- ## Setup
405
-
406
- ### Privacy by default
407
-
408
- Your private sync repo must stay private. Session transcripts contain the full text of your
409
- conversations. `nomad init` disables Actions on the new repo as soon as it is created, via the
410
- GitHub API call `gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false`. What this
411
- means for you: CI workflows (which could echo transcript content into build logs) are turned off on
412
- your private data repo automatically; you do not need to remember to do it.
413
-
414
- Pass `--keep-actions` to skip the disable step (for example, when your org already enforces an
415
- Actions policy).
416
-
417
- <!-- prettier-ignore -->
418
- > [!WARNING]
419
- > If you ever make the repo public, your session transcripts (which include conversation content)
420
- > become world-readable. **Keep it private.**
421
-
422
- ### First host
423
-
424
- `nomad init` creates the private repo via `gh`, wires it as `origin`, disables Actions, scaffolds
425
- the directory layout, and pushes. The `gh` CLI must be installed and authenticated before you run
426
- it.
427
-
428
- ```bash
429
- # Install the CLI.
430
- $ npm i -g claude-nomad
431
-
432
- # Create the private sync repo and scaffold it. You will be prompted for a
433
- # repo name (default: claude-nomad-config). Pass --repo to skip the prompt.
434
- $ nomad init
435
- # or non-interactively:
436
- $ nomad init --repo my-config
437
-
438
- # If ~/.claude/ is already populated on this host, capture it as the starting
439
- # point instead of an empty scaffold. Stages shared/ and writes
440
- # hosts/<NOMAD_HOST>.json from your current ~/.claude/settings.json.
441
- # Does NOT touch the originals.
442
- $ nomad init --snapshot
443
- ```
444
-
445
- `nomad init` refuses to clobber existing scaffold artifacts, so re-running on a populated repo is a
446
- safe no-op (it errors out naming the offender). `nomad pull` against an unscaffolded repo fails fast
447
- with `FATAL: repo not initialized; run 'nomad init' to scaffold` instead of silently leaving a
448
- half-state.
449
-
450
- Add a stable host label to your shell rc, then reload it:
451
-
452
- ```bash
453
- export NOMAD_HOST=<your-host-label> # add to ~/.zshrc or ~/.bashrc
454
- ```
455
-
456
- `NOMAD_HOST` overrides `os.hostname()`, which returns noisy values like `WINDOWS-I5NT6OH` on WSL or
457
- `<name>.local` on macOS. Pick a clean label per machine (e.g., `wsl-laptop`, `macbook`,
458
- `homelab-nuc`). `nomad doctor` reports the resolved host so you can confirm.
459
-
460
- Edit `path-map.json` to add your logical projects (see [Path remapping](#path-remapping)), then:
461
-
462
- ```bash
463
- $ nomad doctor # read-only state check; reports host, repo state, every check as ✓ (pass) / ✗ (fail) / ⚠︎ (warn)
464
- $ nomad doctor --check-shared # read-only gitleaks preflight over the session transcripts a push would stage
465
- $ nomad diff # preview what nomad pull would change on this host; no lock, no network, no mutation
466
- $ nomad push # send current state to the private remote
467
- $ nomad pull # apply on another host (or this one after a remote update)
468
- ```
469
-
470
- `nomad pull --dry-run` is the network-aware twin of `nomad diff`: it acquires the lock and runs
471
- `git pull` so you see what the next real pull would do given the latest remote, then exits without
472
- mutating.
473
-
474
- If the destination host already has populated `~/.claude/{CLAUDE.md, agents/, ...}`, the first
475
- `nomad pull` will refuse to overwrite real files. See
476
- [Migrating an existing ~/.claude/](#migrating-an-existing-claude) for the safe migration flow.
477
-
478
- ### Each additional host
479
-
480
- ```bash
481
- # Install the CLI.
482
- $ npm i -g claude-nomad
483
-
484
- # Clone your private data repo.
485
- $ gh repo clone <your-username>/claude-nomad-config ~/claude-nomad
486
- # or with plain git:
487
- $ git clone git@github.com:<your-username>/claude-nomad-config.git ~/claude-nomad
488
-
489
- # Add to ~/.zshrc or ~/.bashrc, then reload.
490
- export NOMAD_HOST=<your-host-label>
491
-
492
- $ nomad pull # apply config to ~/.claude/
493
- ```
494
-
495
- `npm i -g claude-nomad` puts a `nomad` binary on your PATH. What this means for you: there is no
496
- compile step, no extra transpiler to install, and nothing is fetched from the network the first time
497
- you run `nomad`, so the first run works offline. (The Node version floor and the `engine-strict`
498
- caveat are in [Requirements](#requirements).)
499
-
500
- ## Migrating an existing ~/.claude/
501
-
502
- If a host already has real files at `~/.claude/{CLAUDE.md, agents/, skills/, ...}` and you want to
503
- bring them into the sync, the required sequence is `nomad init --snapshot` → `nomad push` →
504
- `nomad pull`:
505
-
506
- ```bash
507
- # From the host that has the canonical config (the originals are not modified):
508
- $ nomad init --snapshot # stages shared/ and writes hosts/<NOMAD_HOST>.json from ~/.claude/
509
- $ nomad push # publish the captured state to the private remote
510
-
511
- # Then, on this host or any other host that has the private remote checked out:
512
- $ nomad pull # materializes the symlinks
513
- ```
514
-
515
- `nomad pull` is what actually migrates the host. `applySharedLinks` runs a two-pass scan: any
516
- pre-existing non-symlink at a `SHARED_LINKS` path whose counterpart exists under `shared/` is
517
- renamed into `~/.cache/claude-nomad/backup/<ts>/` first, then the symlink is created. Your originals
518
- are preserved under that timestamped backup directory, not deleted. Paths whose `shared/<name>` is
519
- absent from the remote are left untouched, so a partial publish does not delete data on the
520
- destination host.
521
-
522
- If the remote has not been populated yet (you skipped `nomad init --snapshot` and `nomad push`),
523
- `nomad pull` is a no-op for SHARED_LINKS: there is nothing on the remote to symlink against, so your
524
- local `~/.claude/` files stay in place. The auto-move only triggers once the canonical state is
525
- published.
526
-
527
- Prefer an explicit tarball rollback and a confirmation prompt before any deletion? Write the
528
- equivalent under `scripts/`: tar the `SHARED_LINKS` entries under `~/.claude/` first, copy into
529
- `shared/`, prompt, then `nomad pull`. The auto-move path above is the recommended default.
530
-
531
- ## Upgrading the CLI
532
-
533
- `nomad update` updates the `nomad` binary from npm:
534
-
535
- ```bash
536
- $ nomad update
537
- ```
538
-
539
- What this means for you: it runs `npm update -g claude-nomad` and refreshes the binary on your PATH.
540
- It does NOT pull your sync data; run `nomad pull` separately when you want to apply remote changes
541
- to this host.
542
-
543
- `nomad doctor` reports when your local install is behind the latest npm release:
544
- `⚠︎ claude-nomad: <local> -> <latest> (run nomad update)`.
545
-
546
- ## Commands
547
-
548
- | Command | Description |
549
- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
550
- | `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). |
551
- | `nomad init --repo <name>` | Non-interactive: use `<name>` as the private repo name without prompting. Useful in scripts. |
552
- | `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`. |
553
- | `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. |
554
- | `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. |
555
- | `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. |
556
- | `nomad diff` | Offline, lockless twin of `pull --dry-run`. No network, no lock. Works against the current local repo state. |
557
- | `nomad push` | Export local sessions and opted-in per-project extras to logical names, commit (`chore: sync from <NOMAD_HOST>`), push. |
558
- | `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. |
559
- | `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). |
560
- | `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). |
561
- | `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. |
562
- | `nomad adopt <name> --dry-run` | Preview the planned backup, move, and `git add` without touching the filesystem or the git index. |
563
- | `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). |
564
- | `nomad redact --rule <id>` | Limit redaction to findings of one gitleaks rule id only. |
565
- | `nomad redact --dry-run` | Show what `nomad redact` would change without writing anything. |
566
- | `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). |
567
- | `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). |
568
- | `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 two `⚠︎`-only drift checks: gitleaks version drift and, on a private GitHub mirror, re-enabled Actions. |
569
- | `nomad doctor --resume-cmd <id>` | Print a host-local `cd ... && claude --resume <id>` line for a session (see [Cross-OS resume](#cross-os-resume)). |
570
- | `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). |
571
- | `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 curl is missing or the schema is unreachable. |
572
- | `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. |
573
-
574
- The version-check emits ``⚠︎ claude-nomad: <local> -> <latest> (run `nomad update`)`` when the local
575
- install is behind the latest upstream release, and `✓ claude-nomad: <local> (latest)` when current.
576
- It silently skips on network failures.
577
-
578
- The Hook targets check reads the live `~/.claude/settings.json` `hooks` block and fails (`✗`, exit
579
-
580
- 1. when a hook command points at a script under `~/.claude/` that is missing on this host (the
581
- freshly-configured-host symptom that motivated syncing `hooks/`). It deliberately skips any
582
- command it cannot resolve to a `~/.claude/` path (bare binaries like `jq`, unresolved env vars),
583
- so it never false-fails on a command that does not reference a local script.
584
-
585
- Two further `⚠︎`-only drift checks run in `nomad doctor`. The gitleaks version-drift line
586
- `⚠︎ gitleaks: <local> -> <pinned> (...)` fires when the local gitleaks major.minor differs from the
587
- CI-pinned `GITLEAKS_PINNED_VERSION` (gitleaks rule and allowlist behavior tracks the minor line, so
588
- a patch-only difference stays `✓`), and is silent when gitleaks is not on PATH. The mirror-Actions
589
- line (carrying a `gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false`
590
- remediation hint) fires when origin is a private GitHub mirror that is gh-authed with Actions
591
- re-enabled, complementing the auto-disable that runs on `nomad init` (see
592
- [Privacy by default](#privacy-by-default)); it is silent on every prerequisite miss (non-GitHub
593
- origin, `gh` unauthed, public repo, or Actions already off).
594
-
595
- ### Reading push and pull output
596
-
597
- `nomad push` and `nomad pull` print a grouped tree, the same left-gutter layout you already see from
598
- `nomad doctor`. There is a header line naming the command and host, then a few named sections
599
- (`Sessions`, `Extras`, and so on), each with its items hanging off `├`/`└` connectors. A status
600
- glyph leads every line: `✓` green for something that synced, `ℹ︎` dim for an informational count, `⚠︎`
601
- yellow for a warning, and `✗` red for a failure. What this means for you: instead of one long flat
602
- list with a line per project, related work is grouped and the noise is collapsed.
603
-
604
- A clean `nomad push` looks like this (one `✓` row per project whose sessions were copied up, the
605
- projects this host does not track folded into a single count, then the secret-scan result and a
606
- one-line summary):
607
-
608
- ```text
609
- push on host=workstation
610
- Sessions
611
- ├ ✓ claude-nomad
612
- ├ ✓ my-side-project
613
- └ ℹ︎ 4 not in path-map (run nomad doctor to list)
614
- Extras
615
- └ ✓ claude-nomad/.planning
616
- Leak scan
617
- └ ✓ no leaks
618
- Summary
619
- └ ✓ summary: clean
620
- ```
621
-
622
- The `ℹ︎ 4 not in path-map` row is the collapse: rather than printing one line per project that this
623
- host does not sync, push and pull now show a single count and point you at `nomad doctor`, which
624
- lists those projects by name if you want the detail. The `Leak scan` section is the secret check
625
- that runs before anything is published: `✓ no leaks` when the staged transcripts are clean. If a
626
- secret IS found, that row turns into `✗ gitleaks detected secrets in N session transcript(s)` and
627
- the full recovery block (which sessions, how to scrub them) still prints below the tree, exactly as
628
- before (see
629
- [Recovery flow: gitleaks FATAL on a session JSONL](#recovery-flow-gitleaks-fatal-on-a-session-jsonl)).
630
- The same `Leak scan` row shows up under `nomad push --dry-run`, which runs that secret scan as a
631
- read-only preview (nothing is written to the sync repo) and exits non-zero if the preview finds
632
- anything.
633
-
634
- A `nomad pull` is the mirror image, leading with the settings file it regenerated and then the
635
- sessions and extras it copied down for this host:
636
-
637
- ```text
638
- pull on host=workstation (backup=2026-05-27T14-02-09Z)
639
- Settings
640
- └ ✓ settings.json (base + workstation.json)
641
- Sessions
642
- ├ ✓ claude-nomad
643
- └ ℹ︎ 2 not in path-map (run nomad doctor to list)
644
- Extras
645
- └ ✓ claude-nomad/.planning
646
- Summary
647
- └ ✓ summary: clean
648
- ```
649
-
650
- The `Summary` row is the final verdict for the run. It reads `✓ summary: clean` when everything
651
- synced, or a `⚠︎` warning naming the counts when something was skipped:
652
-
653
- ```text
654
- ⚠︎ summary: 3 unmapped on pull (run nomad doctor to list)
655
- ⚠︎ summary: 2 unmapped on push, 1 collisions (run nomad doctor to list)
656
- ```
657
-
658
- `✓` lines go to stdout; `⚠︎` and `✗` lines go to stderr. An early, pre-tree fatal abort (for example
659
- gitleaks missing when push checks for it, or a rebase conflict before anything is staged) suppresses
660
- the tree entirely, so you do not see "summary: clean" stacked under an error. A later leak-scan
661
- finding is different: by then the tree has already been built, so it still renders in full with a
662
- `✗` Leak scan row and the recovery block below it (see
663
- [Recovery flow: gitleaks FATAL on a session JSONL](#recovery-flow-gitleaks-fatal-on-a-session-jsonl)).
664
- Projects with no entry in `path-map.json` for this host count as unmapped and fold into the
665
- collapsed `ℹ︎ ... not in path-map` count; the hint points at `nomad doctor`, which lists them by
666
- logical name.
667
-
668
- `nomad pull --dry-run` keeps its own readable preview format (a unified diff of the `settings.json`
669
- changes plus the transcripts a real pull would overwrite) rather than the grouped tree, so that
670
- preview stays easy to scan; only a real `nomad pull` prints the tree above. `nomad diff` is
671
- unchanged.
672
-
673
- ## Recovery flows
674
-
675
- ### Pruning old backups
676
-
677
- Every `nomad pull` and `nomad push` keeps you safe by copying any file it is about to overwrite into
678
- a timestamped snapshot under `~/.cache/claude-nomad/backup/<ts>/`. That is what makes an unexpected
679
- overwrite recoverable, but the snapshots are never deleted automatically, so over many syncs the
680
- folder slowly grows. It lives in your local cache and is never synced to the shared repo, so
681
- cleaning it up is purely local disk housekeeping.
682
-
683
- `nomad clean --backups` prunes those snapshots. **Always run it with `--dry-run` first** so you can
684
- see exactly which snapshots it would delete before anything is removed:
685
-
686
- ```bash
687
- $ nomad clean --backups --dry-run # list what would be deleted, remove nothing
688
- $ nomad clean --backups # delete snapshots older than 14 days (the default)
689
- ```
690
-
691
- You choose what counts as "old" in one of two ways (you cannot use both at once):
692
-
693
- - `--older-than <duration>` deletes snapshots older than the given age. The duration is a number
694
- plus a unit: `d` for days, `h` for hours, `m` for minutes (for example `7d`, `24h`, `30m`). With
695
- no retention flag at all, the default is `--older-than 14d`.
696
- - `--keep <N>` keeps the `N` most recent snapshots and deletes the rest, regardless of age.
697
-
698
- `nomad clean` only ever touches the timestamped snapshot directories directly inside the backup
699
- folder; it never follows symlinks out of it and never removes the backup folder itself. As a gentle
700
- reminder, `nomad doctor` shows a `⚠︎` warning when the backup folder grows past roughly 20 snapshots
701
- or 200 MB, nudging you to run `nomad clean --backups`. That warning is informational only and never
702
- changes the doctor exit code.
703
-
704
- ### `nomad drop-session <id>`
705
-
706
- Surgically unstages every `shared/projects/*/<id>.jsonl` plus the sibling `shared/projects/*/<id>/`
707
- subagent directory (whose nested transcripts are keyed by the same session id) from the staged tree
708
- of `~/claude-nomad/`. The local `~/.claude/projects/<encoded>/<id>.jsonl` and the local `<id>/` tree
709
- are never touched.
710
-
711
- ```bash
712
- $ nomad drop-session <id>
713
- ```
714
-
715
- Single positional id (the session filename minus `.jsonl`). Anything else (missing id, leading dash,
716
- extra arg) exits 1 with a `usage:` line.
717
-
718
- For each match in the staged tree, `cmdDropSession` (in `src/commands.drop-session.ts`) classifies
719
- the entry as tracked-in-HEAD vs newly-staged and unstages it via
720
- `git restore --staged --worktree --` or `git rm --cached -f --` respectively. The `<id>/` subagent
721
- directory is expanded into its staged entries via `git ls-files -z` so every nested transcript flows
722
- through the same per-entry classification; a session that has only a subagent directory (no flat
723
- `<id>.jsonl`) is still droppable. Idempotent: a second run on the same id sees no matching staged
724
- entries and exits 0.
725
-
726
- Exit codes:
727
-
728
- - `0` on any drop, including an idempotent re-run.
729
- - `1` with `✗ no staged session matches <id>` on stderr when neither a
730
- `shared/projects/*/<id>.jsonl` nor a `shared/projects/*/<id>/` directory with staged entries
731
- matches.
732
-
733
- What it does NOT do: touch the local `~/.claude/projects/<encoded>/<id>.jsonl` file or the local
734
- `<id>/` subagent tree. The local copies are preserved for `claude --resume`, grep recovery, or
735
- whatever the user wants. If the underlying secret is real, scrubbing or removing the local files is
736
- REQUIRED for durability, not optional housekeeping: `remapPush` (in `src/remap.ts`) re-mirrors the
737
- local content into the staged tree on the next push, so a drop without a local scrub re-stages the
738
- same secret.
739
-
740
- A successful drop prints this reminder inline, pointing at the live transcript that still needs
741
- scrubbing (the exact path when `path-map.json` maps the project to the current host, a generic
742
- `~/.claude/projects/<encoded>/<id>.jsonl` template otherwise). This is why a
743
- `nomad doctor --check-shared` run still reports the session after a drop: that scan reads the live
744
- `~/.claude/projects/` source, not the staged tree, so it keeps flagging the secret until the local
745
- transcript is scrubbed.
746
-
747
- ### `nomad redact <session-id>`
748
-
749
- Rewrites the secret span in the local source transcript at
750
- `~/.claude/projects/<encoded>/<session-id>.jsonl` in place, replacing each flagged span with
751
- `[REDACTED:<rule>]`. Before rewriting, the original transcript is backed up to
752
- `~/.cache/claude-nomad/backup/<timestamp>/`.
753
-
754
- ```bash
755
- $ nomad redact <session-id>
756
- $ nomad redact <session-id> --rule github-pat # one rule only
757
- $ nomad redact <session-id> --dry-run # preview without writing
758
- ```
759
-
760
- What it does: rewrites the LOCAL source transcript (not just the staged copy). This is the durable
761
- fix for a gitleaks finding: `nomad drop-session` only removes the staged copy, but `remapPush`
762
- re-copies from local on the next push, so the secret resurfaces. Redacting the local source means
763
- future pushes carry clean content.
764
-
765
- What it does NOT do: rotate credentials. Always rotate the secret at its provider first.
766
-
767
- Safety checks:
768
-
769
- - A session whose transcript was modified within the last 5 minutes is treated as potentially active
770
- (Claude Code may still be writing to it). `nomad redact` refuses to touch it and suggests
771
- `nomad drop-session` or waiting for the session to end.
772
- - Before every rewrite, a backup is written to `~/.cache/claude-nomad/backup/<timestamp>/`, so the
773
- original content is recoverable.
774
- - `--dry-run` prints the planned redactions and writes nothing.
775
-
776
- This command is safe to re-run: if the span was already redacted (the replacement token is already
777
- present), the content is unchanged.
778
-
779
- ### Recovery flow: gitleaks FATAL on a session JSONL
780
-
781
- `nomad push` runs `gitleaks protect --staged` before commit. To catch the same findings before you
782
- push (and without mutating anything), two read-only options are available:
783
- `nomad doctor --check-shared` scans the session transcripts a push would publish;
784
- `nomad push --dry-run` runs the same scan AND also covers opted-in extras (`.planning`,
785
- `CLAUDE.md`), which `--check-shared` does not. Both stage content into a throwaway temp copy and
786
- never write to the sync repo. A leak-scan finding is the contrast to an early, pre-tree fatal:
787
- because the scan runs after the tree is built, the push aborts but the grouped tree still renders in
788
- full, with a `✗ gitleaks detected secrets in N session transcript(s)` row in its `Leak scan`
789
- section, and then the full recovery block prints below it, naming every affected session id and the
790
- recovery command:
791
-
792
- ```text
793
- ✗ gitleaks detected secrets in 1 session transcript(s).
794
-
795
- Session <sid-aaaa>:
796
- generic-api-key (14), aws-access-token (1)
797
- Recover with: nomad drop-session <sid-aaaa>
798
-
799
- After recovery, re-run nomad push.
800
- ```
801
-
802
- Two branches from here:
803
-
804
- 1. **Real secret.** Rotate the credential at its provider first (revoke in dashboard, issue
805
- replacement) before touching anything else. Running `nomad drop-session <sid-aaaa>` clears the
806
- contaminated copy from the current staged tree, but that alone is NOT durable: `remapPush` (in
807
- `src/remap.ts`) does a full rm-and-copy mirror of your LOCAL transcripts into `shared/projects/`
808
- on every push, so the next `nomad push` re-copies the un-scrubbed local file forward and
809
- re-stages the same secret. The durable fix is to rotate AND scrub the local transcript. The
810
- easiest way: `nomad redact <sid-aaaa>` (see [`nomad redact`](#nomad-redact-session-id)), which
811
- rewrites the secret span in place with a backup. Alternatively, remove the local transcript at
812
- `~/.claude/projects/<encoded>/<sid-aaaa>.jsonl` (plus the sibling `<sid-aaaa>/` subagent
813
- directory, if present). Do not leave the local file un-scrubbed and expect the staged-tree drop
814
- to hold.
815
-
816
- 2. **False positive.** Add an allowlist regex to `.gitleaks.toml` at the repo root that matches the
817
- noise pattern but not real-secret formats, commit it, then re-run `nomad push`. The new allowlist
818
- propagates to other hosts when they run `nomad update` (CLI upgrade) or when you push the updated
819
- file to your data repo.
820
-
821
- `nomad drop-session` only acts on the staged tree of `~/claude-nomad/`. Active Claude Code sessions
822
- writing to the local file are not disturbed.
823
-
824
- ### Recovery flow: push-time interactive menu
825
-
826
- When `nomad push` detects a secret and the process is running on an interactive TTY, it presents a
827
- per-finding menu instead of aborting immediately. Each finding is shown with its rule id, file, and
828
- line number (the secret value is never printed: the scan uses `--redact`).
829
-
830
- ```text
831
- Finding: github-pat in shared/projects/my-proj/abc123.jsonl line 42 (session: abc123)
832
- [R]edact [A]llow [D]rop session [S]kip (default)
833
- >
834
- ```
835
-
836
- What the actions do:
837
-
838
- - **Redact** rewrites the secret span in the LOCAL source transcript in place (same flow as
839
- `nomad redact`), backs up first, then re-copies the file to the staged tree. Refuses if the
840
- session was modified in the last 5 minutes (potential active session): choose Drop or Skip instead
841
- and wait for the session to end.
842
- - **Allow** appends the finding's fingerprint to `.gitleaksignore` at the repo root. Use this for
843
- confirmed false positives. The fingerprint format (`file:rule:line`) is tied to the current line,
844
- so if the content moves gitleaks re-prompts rather than silently suppressing a new hit.
845
- - **Drop session** excludes this session from the current push by unstaging it from the repo's git
846
- index (same as `nomad drop-session <id>`). The local `~/.claude/projects/.../` transcript is kept
847
- intact and any running Claude session is not stopped. Not durable: the next push re-copies from
848
- local unless you also redact or remove the local transcript.
849
- - **Skip** (default on bare Enter) leaves the finding unresolved for now.
850
-
851
- After you respond to every finding, the menu applies your choices. If any finding was Skipped, the
852
- push aborts with the session-aware FATAL (same exit as a non-interactive push with findings). If all
853
- findings were resolved, the staged tree is updated and re-scanned. A clean re-scan proceeds to
854
- commit and push. If new findings appear after the first round of actions, the menu loops on the new
855
- set.
856
-
857
- On a non-TTY (CI, piped input, or scripted `nomad push`), the menu never appears and the push aborts
858
- with the existing session-aware FATAL unchanged.
859
-
860
- **Batch redact without a TTY:** `nomad push --redact-all` redacts every finding non-interactively
861
- (backup written first) without prompting and without requiring a TTY. It does not auto-Allow. After
862
- redaction the staged tree is re-scanned; any surviving finding aborts with the FATAL. Use this in
863
- scripts or when every finding is a real secret that should be scrubbed. For a single session,
864
- `nomad redact <session-id>` (see [`nomad redact`](#nomad-redact-session-id)) gives you per-session
865
- control with `--rule` and `--dry-run` options.
866
-
867
- ### `.gitleaks.toml` allowlist policy
868
-
869
- `gitleaks protect` runs against the staged tree on every `nomad push` and can flag
870
- structurally-distinguishable tool-output noise as `generic-api-key`. The repo-root `.gitleaks.toml`
871
- pre-allows four such patterns so routine pushes are not blocked:
872
-
873
- - Sonar issue keys (`AY` prefix + 20+ url-safe chars).
874
- - gitleaks fingerprint format (`<context>:<rule>:<line>` emitted by gitleaks's own reports).
875
- - npm audit advisory hashes (anchored on the JSON shape `"id":"<40..64 hex>"`).
876
- - Coverage-report line-keys (`key=<hex> <path>:<line>`).
877
-
878
- The file extends the default gitleaks ruleset, so real high-entropy secrets like `ghp_*`,
879
- `sk_live_*`, `xoxb-*`, and `AKIA*` still fire. The allowlist patterns are structurally
880
- distinguishable from real-secret formats: a malformed credential cannot match an allowlist regex by
881
- accident.
882
-
883
- ```toml
884
- [extend]
885
- useDefault = true
886
-
887
- [[allowlists]]
888
- description = "claude-nomad: structurally-distinguishable tool-output noise"
889
- regexes = [
890
- '''AY[A-Za-z0-9_-]{20,}''',
891
- '''[\w-]+:[\w-]+:\d+''',
892
- # ...see .gitleaks.toml at the repo root for the full list
893
- ]
894
- ```
895
-
896
- File location: `.gitleaks.toml` ships bundled with the CLI binary. At runtime both `probeGitleaks`
897
- (in `src/push-checks.ts`) and `runGitleaksScan` (in `src/push-gitleaks.ts`) try
898
- `<REPO_HOME>/.gitleaks.toml` first and fall back to the package-bundled copy when the repo-level
899
- file is absent. So when you have no repo-level copy the allowlist tracks the installed binary, and
900
- running `nomad update` (to get the latest CLI) is enough to receive allowlist updates. If you do
901
- place a `<REPO_HOME>/.gitleaks.toml`, it takes precedence and `nomad update` will not change it; you
902
- maintain that file yourself.
903
-
904
- #### Customizing the allowlist with an overlay
905
-
906
- What this means for you: if you only want to allow a couple of extra patterns of your own (say, an
907
- internal tool that emits a structured token that keeps tripping the scan), you do not have to copy
908
- the whole bundled allowlist into your sync repo and keep it in step by hand. Instead, drop a small
909
- `<REPO_HOME>/.gitleaks.overlay.toml` containing only your extra `[[allowlists]]` tables (and
910
- optionally `[[rules]]`). nomad layers your entries on top of the bundled allowlist at scan time, so
911
- the shipped Sonar / gitleaks / npm-audit / coverage noise allows stay in effect, the gitleaks
912
- default ruleset stays in effect, and your additions are appended to all of them.
913
-
914
- Why this is better than a full `.gitleaks.toml`: a full repo-level `.gitleaks.toml` replaces the
915
- bundled allowlist outright, so the shipped noise allows are lost and `nomad update` can no longer
916
- refresh them (you own that file). The overlay is additive instead: it never drops the bundled base,
917
- and because the base still ships with the CLI, `nomad update` keeps the base current while your
918
- overlay rides on top.
919
-
920
- How it works, briefly: on `nomad push`, when the overlay is present, nomad generates a throwaway
921
- config that extends the bundled `.gitleaks.toml` (which itself extends the gitleaks default),
922
- appends your overlay body, scans with that combined config, then deletes the throwaway file. The
923
- merge is gitleaks' own `[extend]` append, so your allowlist entries add to the shipped and default
924
- ones rather than replacing them.
925
-
926
- Two rules to keep in mind:
927
-
928
- - Your overlay must NOT contain its own `[extend]` block. nomad writes the `[extend]` line for you;
929
- if the overlay includes one, the push aborts with a clear error rather than scanning with a config
930
- you did not intend.
931
- - If you keep BOTH a full `<REPO_HOME>/.gitleaks.toml` AND an overlay, the full `.gitleaks.toml`
932
- wins and the overlay is ignored (a full repo toml means you have taken complete manual control).
933
- Pick one approach: the overlay for additive tweaks, or a full `.gitleaks.toml` for total control.
934
-
935
- Example `<REPO_HOME>/.gitleaks.overlay.toml` (note: no `[extend]` block):
936
-
937
- ```toml
938
- [[allowlists]]
939
- description = "my-org: internal build-token noise"
940
- regexes = [
941
- '''BUILDTOK-[A-Za-z0-9]{24}''',
942
- ]
943
- ```
944
-
945
- The overlay file is push-allowed (it is an exact-name entry in `PUSH_ALLOWED_STATIC` in
946
- `src/config.ts`, alongside `.gitleaksignore`), so you can commit `.gitleaks.overlay.toml` to your
947
- sync repo and it travels to your other hosts on the next `nomad pull`.
948
-
949
- Editing: amend `.gitleaks.toml` in this repo, open a PR, and merge to `main`. Use TOML literal
950
- strings (triple single quotes, `'''regex'''`) for new regex entries so backslashes do not need
951
- escaping. Verify the new pattern does not match real-secret formats (`ghp_<36>`, `sk_live_*`,
952
- `xoxb-*`, `AKIA[A-Z0-9]{16}`, etc.) before merging. The allowlist ships with the binary, so
953
- `nomad update` on each host picks up the new file.
954
-
955
- ## Cross-OS resume
956
-
957
- Claude Code embeds the original `cwd` in each session transcript. When you resume on a different
958
- host where that path doesn't exist, the picker prints a `cd <orig-cwd> && claude --resume <id>` line
959
- that fails (the source-host path isn't there).
960
-
961
- Run this instead:
962
-
963
- ```bash
964
- $ eval "$(nomad doctor --resume-cmd <session-id>)"
965
- ```
966
-
967
- Or pipe through bash:
968
-
969
- ```bash
970
- $ nomad doctor --resume-cmd <session-id> | bash
971
- ```
972
-
973
- `nomad doctor --resume-cmd <id>` reads the `.jsonl`'s recorded `cwd`, reverse-looks up the logical
974
- project in `path-map.json`, finds your current host's abspath for that logical, and prints
975
- `cd <local-abspath> && claude --resume <id>` to stdout. The command is read-only: it never modifies
976
- any transcript byte.
977
-
978
- If the session isn't mapped on this host, you'll see:
979
-
980
- ```text
981
- ✗ session <id> not mapped on this host; add the logical to path-map.json
982
- ```
983
-
984
- Other fatal surfaces: missing `~/.claude/projects/`, session id absent from every encoded dir, no
985
- `cwd` field anywhere in the transcript, missing `path-map.json`, recorded cwd not present in any
986
- logical's host map. All errors go to stderr prefixed with the red `✗` fail glyph; the success line
987
- goes to stdout as a bare shell command (no glyph) so `eval` works.
988
-
989
- ## Run tests
990
-
991
- ```bash
992
- $ npm install
993
- $ npx vitest run
994
- ```
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/)