claude-nomad 0.25.4 → 0.26.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
@@ -1,22 +1,41 @@
1
1
  # claude-nomad
2
2
 
3
3
  [![tests](https://img.shields.io/github/actions/workflow/status/funkadelic/claude-nomad/tests.yml?branch=main&label=tests)](https://github.com/funkadelic/claude-nomad/actions/workflows/tests.yml)
4
- [![release](https://img.shields.io/github/v/release/funkadelic/claude-nomad?label=release&sort=semver)](https://github.com/funkadelic/claude-nomad/releases)
5
- [![coverage](https://img.shields.io/codecov/c/github/funkadelic/claude-nomad/main?label=coverage)](https://codecov.io/gh/funkadelic/claude-nomad)
4
+ [![codeql](https://img.shields.io/github/actions/workflow/status/funkadelic/claude-nomad/codeql.yml?branch=main&label=codeql)](https://github.com/funkadelic/claude-nomad/actions/workflows/codeql.yml)
5
+ [![codecov](https://codecov.io/gh/funkadelic/claude-nomad/graph/badge.svg?token=5NML626POS)](https://codecov.io/gh/funkadelic/claude-nomad)
6
+ [![NPM Version](https://img.shields.io/npm/v/claude-nomad?logo=npm)](https://www.npmjs.com/package/claude-nomad)
7
+ [![node](https://img.shields.io/node/v/claude-nomad?logo=nodedotjs)](https://www.npmjs.com/package/claude-nomad)
8
+ [![license](https://img.shields.io/npm/l/claude-nomad)](LICENSE)
6
9
 
7
10
  ![claude-nomad - Sync your Claude Code setup. Same environment. Any machine.](docs/hero.svg)
8
11
 
9
12
  **Your entire Claude Code setup, on every machine. History included, every push secret-scanned.**
10
13
 
11
- Open Claude Code on a second machine and it is a blank slate: none of your custom agents, slash commands, tuned settings, or past conversations. claude-nomad keeps all of it in sync through a private Git repo you control. `nomad push` on one machine, `nomad pull` on the next, and everything is there, conversations included.
12
-
13
- - **Resume your sessions on any machine.** Start a conversation on your desktop and pick it up on your laptop. claude-nomad remaps the file paths Claude Code embeds in every transcript, so your history follows you instead of getting stranded on the box where it started.
14
- - **Secret-scanned, private by default.** Your `~/.claude/` also holds OAuth tokens, MCP credentials, and the full text of every conversation, so claude-nomad is deliberate about what leaves your machine: credentials and ephemeral state never sync, only an explicit allow-list of paths is pushed, and everything that does go up is scanned by [gitleaks](https://github.com/gitleaks/gitleaks) before it leaves your machine; the push aborts on any hit. `nomad init` also disables Actions on your private mirror by default, so transcripts can't leak through CI logs.
15
- - **One setup, every machine.** Your agents, skills, slash commands, and settings live in one place and follow you everywhere. Per-machine tweaks like model choice, MCP URLs, and env vars merge on top instead of clobbering your shared defaults.
16
-
17
- Not dotfiles, not rsync. claude-nomad understands Claude Code's state, so your session history survives different file paths and your secrets never ride along.
18
-
19
- For anyone running Claude Code on more than one machine: a laptop and a desktop, a Mac and a WSL box, a personal rig and a work machine. [Get started in three steps.](#quickstart)
14
+ Open Claude Code on a second machine and it is a blank slate: none of your custom agents, slash
15
+ commands, tuned settings, or past conversations. **claude-nomad** keeps all of it in sync through a
16
+ private Git repo you control. `nomad push` on one machine, `nomad pull` on the next, and everything
17
+ is there, conversations included.
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
+ Not dotfiles, not rsync. **claude-nomad** understands Claude Code's state, so your session history
35
+ survives different file paths and your secrets never ride along.
36
+
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)
20
39
 
21
40
  ## Table of contents
22
41
 
@@ -47,7 +66,8 @@ For anyone running Claude Code on more than one machine: a laptop and a desktop,
47
66
 
48
67
  ## Quickstart
49
68
 
50
- If you already have a private claude-nomad mirror (see [Setup](#setup) for the one-time bootstrap), adding a new host is three steps:
69
+ If you already have a private **claude-nomad** mirror (see [Setup](#setup) for the one-time
70
+ bootstrap), adding a new host is three steps:
51
71
 
52
72
  ```bash
53
73
  $ npm i -g claude-nomad
@@ -73,13 +93,16 @@ $ nomad pull # apply config to ~/.claude/
73
93
  $ nomad push # publish local changes (sessions, settings)
74
94
  ```
75
95
 
76
- First-host bootstrap and the safe-migration sequence for a populated `~/.claude/` are in [Setup](#setup) and [Migrating an existing ~/.claude/](#migrating-an-existing-claude).
96
+ First-host bootstrap and the safe-migration sequence for a populated `~/.claude/` are in
97
+ [Setup](#setup) and [Migrating an existing ~/.claude/](#migrating-an-existing-claude).
77
98
 
78
99
  ## How it works (two-repo model)
79
100
 
80
- claude-nomad is a **tool**, not a config store. You maintain a separate **private** repo that holds your actual config (`CLAUDE.md`, agents, skills, settings overrides, session transcripts). The tool's source and your config end up coexisting in one working tree on each host.
101
+ **claude-nomad** is a **tool**, not a config store. You maintain a separate **private** repo that
102
+ holds your actual config (`CLAUDE.md`, agents, skills, settings overrides, session transcripts). The
103
+ tool's source and your config end up coexisting in one working tree on each host.
81
104
 
82
- ```
105
+ ```text
83
106
  public funkadelic/claude-nomad your private <your-username>/claude-nomad
84
107
  ├── src/ (the CLI) ├── src/ (copy of the CLI)
85
108
  ├── package.json ├── package.json
@@ -96,13 +119,20 @@ public funkadelic/claude-nomad your private <your-username>/claude-noma
96
119
  └── path-map.json
97
120
  ```
98
121
 
99
- You bootstrap once by mirror-pushing this public tool repo into a fresh private repo of your own (see [Setup](#setup)), then layer your config on top. Every host afterward installs the CLI (`npm i -g claude-nomad`), clones your private repo to `~/claude-nomad/`, and runs `nomad pull` to sync.
122
+ You bootstrap once by mirror-pushing this public tool repo into a fresh private repo of your own
123
+ (see [Setup](#setup)), then layer your config on top. Every host afterward installs the CLI
124
+ (`npm i -g claude-nomad`), clones your private repo to `~/claude-nomad/`, and runs `nomad pull` to
125
+ sync.
100
126
 
101
- By default the CLI operates on `~/claude-nomad/` (see `REPO_HOME` in `src/config.ts`). Developers working from an alternate checkout can `export NOMAD_REPO=/path/to/repo` to point the CLI at their working tree without symlink gymnastics; `nomad doctor` surfaces an active override via a trailing `(NOMAD_REPO)` annotation on the repo-state line. Empty `NOMAD_REPO` falls through to the default, so a clobbered dotfile variable does not break the CLI.
127
+ By default the CLI operates on `~/claude-nomad/` (see `REPO_HOME` in `src/config.ts`). Developers
128
+ working from an alternate checkout can `export NOMAD_REPO=/path/to/repo` to point the CLI at their
129
+ working tree without symlink gymnastics; `nomad doctor` surfaces an active override via a trailing
130
+ `(NOMAD_REPO)` annotation on the repo-state line. Empty `NOMAD_REPO` falls through to the default,
131
+ so a clobbered dotfile variable does not break the CLI.
102
132
 
103
133
  ## Repo layout (what `~/claude-nomad/` looks like on a configured host)
104
134
 
105
- ```
135
+ ```text
106
136
  ~/claude-nomad/
107
137
  ├── src/ # the CLI (came from the public tool repo)
108
138
  ├── scripts/ # helper scripts you add
@@ -136,16 +166,21 @@ By default the CLI operates on `~/claude-nomad/` (see `REPO_HOME` in `src/config
136
166
  | **Never synced** | `~/.claude.json` (OAuth, MCP state), `history.jsonl`, `settings.local.json` (per-host overrides), `stats-cache.json`, `todos/`, `shell-snapshots/`, `debug/`, `file-history/`, `plans/`, `session-env/`, `statsig/`, `telemetry/`, `ide/` | Per-host ephemeral state. |
137
167
  | **Auto-rehydrated** | `~/.claude/plugins/cache/<plugin>/...` | Plugin payloads not synced. Claude Code re-downloads them on first use from the `enabledPlugins` list in the regenerated `settings.json`; no manual `claude plugins install ...` per host. |
138
168
 
139
- > [!NOTE]
140
- > Plugins that depend on host-specific state (external binaries, API keys in env, MCP server URLs) still need that side set up on each host. Put them in `hosts/<host>.json` or the plugin's own per-host config.
169
+ > [!NOTE] Plugins that depend on host-specific state (external binaries, API keys in env, MCP server
170
+ > URLs) still need that side set up on each host. Put them in `hosts/<host>.json` or the plugin's
171
+ > own per-host config.
141
172
 
142
- For the rationale behind these choices, see [What does NOT sync (deliberate trade-offs)](#what-does-not-sync-deliberate-trade-offs).
173
+ For the rationale behind these choices, see
174
+ [What does NOT sync (deliberate trade-offs)](#what-does-not-sync-deliberate-trade-offs).
143
175
 
144
176
  ## Path remapping
145
177
 
146
- The hard problem: Claude Code stores sessions in `~/.claude/projects/<encoded-path>/` where the encoded path is the absolute path with `/` replaced by `-`. So the same logical project ends up in different directories on each host.
178
+ The hard problem: Claude Code stores sessions in `~/.claude/projects/<encoded-path>/` where the
179
+ encoded path is the absolute path with `/` replaced by `-`. So the same logical project ends up in
180
+ different directories on each host.
147
181
 
148
- `path-map.json` defines logical names and where the repo lives on each host. The optional `extras` block opts a project into syncing whitelisted directories (or a single root file) at its root:
182
+ `path-map.json` defines logical names and where the repo lives on each host. The optional `extras`
183
+ block opts a project into syncing whitelisted directories (or a single root file) at its root:
149
184
 
150
185
  ```json
151
186
  {
@@ -162,22 +197,44 @@ The hard problem: Claude Code stores sessions in `~/.claude/projects/<encoded-pa
162
197
  }
163
198
  ```
164
199
 
165
- > [!IMPORTANT]
166
- > The host-label keys must match whatever you set `NOMAD_HOST=` to on each host (see [Setup](#setup)). Mismatched labels silently skip remap, so sessions land in the wrong host's encoded dir.
200
+ > [!IMPORTANT] The host-label keys must match whatever you set `NOMAD_HOST=` to on each host (see
201
+ > [Setup](#setup)). Mismatched labels silently skip remap, so sessions land in the wrong host's
202
+ > encoded dir.
167
203
 
168
- Use the literal string `"TBD"` for hosts you haven't onboarded yet; `remapPull` skips TBD entries cleanly instead of creating an orphan `~/.claude/projects/TBD/`. Replace each `"TBD"` with the real path when you bring up that host.
204
+ Use the literal string `"TBD"` for hosts you haven't onboarded yet; `remapPull` skips TBD entries
205
+ cleanly instead of creating an orphan `~/.claude/projects/TBD/`. Replace each `"TBD"` with the real
206
+ path when you bring up that host.
169
207
 
170
- On `push`, sessions in `~/.claude/projects/-Users-you-code-my-example-repo/` get copied to `shared/projects/my-example-repo/`. On `pull` on another machine, they get copied to that host's encoded path. `claude --resume` then finds them (see [What does NOT sync (deliberate trade-offs)](#what-does-not-sync-deliberate-trade-offs) for the cross-OS cwd-binding gotcha).
208
+ On `push`, sessions in `~/.claude/projects/-Users-you-code-my-example-repo/` get copied to
209
+ `shared/projects/my-example-repo/`. On `nomad pull` on another machine, they get copied to that
210
+ host's encoded path. `claude --resume` then finds them (see
211
+ [What does NOT sync (deliberate trade-offs)](#what-does-not-sync-deliberate-trade-offs) for the
212
+ cross-OS cwd-binding gotcha).
171
213
 
172
- The `extras` block is additive and back-compatible: legacy `path-map.json` files without it keep working unchanged. Each value is an array of directory or root-file names (e.g. `.planning`, `CLAUDE.md`) checked against `SUPPORTED_EXTRAS` in `src/config.ts`; anything outside that whitelist is skipped with a log line, so an unrecognized name cannot widen the sync surface.
214
+ The `extras` block is additive and back-compatible: legacy `path-map.json` files without it keep
215
+ working unchanged. Each value is an array of directory or root-file names (e.g. `.planning`,
216
+ `CLAUDE.md`) checked against `SUPPORTED_EXTRAS` in `src/config.ts`; anything outside that whitelist
217
+ is skipped with a log line, so an unrecognized name cannot widen the sync surface.
173
218
 
174
- On `push`, opted-in content at `<localRoot>/<name>` (a directory subtree or a single file) is copied to `shared/extras/<logical>/<name>` and goes through the same staged-tree gitleaks scan as everything else. On `pull`, the reverse copy runs after `git pull --rebase`, and just before it overwrites your working tree a divergence check compares the incoming content against your local copy and prints a per-file WARN naming anything that differs.
219
+ On `nomad push`, opted-in content at `<localRoot>/<name>` (a directory subtree or a single file) is
220
+ copied to `shared/extras/<logical>/<name>` and goes through the same staged-tree gitleaks scan as
221
+ everything else. On `nomad pull`, the reverse copy runs after `git pull --rebase`, and just before
222
+ it overwrites your working tree a divergence check compares the incoming content against your local
223
+ copy and prints a per-file WARN naming anything that differs.
175
224
 
176
- Your existing local content is backed up under `~/.cache/claude-nomad/backup/<ts>/extras/` before the pull copy lands, so an unexpected overwrite is always recoverable.
225
+ Your existing local content is backed up under `~/.cache/claude-nomad/backup/<ts>/extras/` before
226
+ the pull copy lands, so an unexpected overwrite is always recoverable.
177
227
 
178
228
  ## Per-host overrides
179
229
 
180
- `settings.base.json` holds portable defaults (model, permissions, plugins). `hosts/<NOMAD_HOST>.json` holds machine-specific patches. They're deep-merged on every pull (scalars override, objects merge recursively, arrays replace). Keys that used to be force-marked per-host because they embedded absolute paths (`statusLine.command`, `hooks`) can live in base if you write the commands with `$HOME` (e.g. `"command": "node \"$HOME/.claude/my-statusline.cjs\""`); Claude Code runs them through a shell so shell expansion applies. Reserve per-host files for truly machine-specific values (env, MCP URLs, host-only model overrides).
230
+ `settings.base.json` holds portable defaults (model, permissions, plugins).
231
+ `hosts/<NOMAD_HOST>.json` holds machine-specific patches. They're deep-merged on every pull (scalars
232
+ override, objects merge recursively, arrays replace). Keys that used to be force-marked per-host
233
+ because they embedded absolute paths (`statusLine.command`, `hooks`) can live in
234
+ `settings.base.json` if you write the commands with `$HOME` (e.g.
235
+ `"command": "node \"$HOME/.claude/my-statusline.cjs\""`); Claude Code runs them through a shell so
236
+ shell expansion applies. Reserve per-host files for truly machine-specific values (env, MCP URLs,
237
+ host-only model overrides).
181
238
 
182
239
  `shared/settings.base.json`:
183
240
 
@@ -188,7 +245,7 @@ Your existing local content is backed up under `~/.cache/claude-nomad/backup/<ts
188
245
  }
189
246
  ```
190
247
 
191
- `hosts/<your-wsl-host>.json`:
248
+ `hosts/<your-other-host>.json`:
192
249
 
193
250
  ```json
194
251
  {
@@ -197,10 +254,11 @@ Your existing local content is backed up under `~/.cache/claude-nomad/backup/<ts
197
254
  }
198
255
  ```
199
256
 
200
- Result on that host: opus model, the local Ollama env var, plus the shared permissions array.
257
+ Results on `your-other-host`: opus 4.7, the local Ollama env var, plus the shared permissions array.
201
258
 
202
- > [!CAUTION]
203
- > Never hand-edit `~/.claude/settings.json` on a synced host. It's regenerated on every `nomad pull` from base + host, so your edits will be clobbered. Edit the base or host file in the repo instead.
259
+ > [!CAUTION] Never hand-edit `~/.claude/settings.json` on a synced host. It's regenerated on every
260
+ > `nomad pull` from base + host, so your edits will be clobbered. Edit the base or host file in the
261
+ > repo instead.
204
262
 
205
263
  ## What does NOT sync (deliberate trade-offs)
206
264
 
@@ -209,39 +267,72 @@ Read these before adopting so you opt in with eyes open.
209
267
  - **Last-write-wins on conflicts.** Git surfaces them on merge; no field-level JSON merging.
210
268
  - **Manual push/pull.** No file watcher. Shell hooks recommended.
211
269
  - **OAuth doesn't sync.** You'll log in once per host. Intentional.
212
- - **Only sessions in `path-map.json` are remapped.** Drive-by sessions on un-mapped paths are left alone.
213
- - **Extras are opt-in and whitelisted.** Projects without an `extras` entry in `path-map.json` are unaffected. Names (a directory or a single root file) outside `SUPPORTED_EXTRAS` are skipped with a `skip ... not in SUPPORTED_EXTRAS` log line so an unrecognized name cannot widen the sync surface. Unsafe path-map values (path-traversal in `logical` keys, non-absolute or unnormalized `localRoot` values) abort the run before any file is touched, so a malformed entry fails loudly instead of corrupting state.
214
- - **Cross-OS `claude --resume` cwd binding.** Sessions embed the cwd where they were created, so the picker's `cd ... && claude --resume <id>` line fails on a different host. Use `nomad doctor --resume-cmd <id>` for a host-local equivalent (see [Cross-OS resume](#cross-os-resume)). The sidecar approach preserves transcript byte-equality.
215
- - **Empty directories don't survive sync.** Git doesn't track empty dirs; `nomad doctor` reports them as `missing` (benign). Drop a `.gitkeep` to force materialization.
270
+ - **Only sessions in `path-map.json` are remapped.** Drive-by sessions on un-mapped paths are left
271
+ alone.
272
+ - **Extras are opt-in and whitelisted.** Projects without an `extras` entry in `path-map.json` are
273
+ unaffected. Names (a directory or a single root file) outside `SUPPORTED_EXTRAS` are skipped with
274
+ a `skip ... not in SUPPORTED_EXTRAS` log line so an unrecognized name cannot widen the sync
275
+ surface. Unsafe path-map values (path-traversal in `logical` keys, non-absolute or unnormalized
276
+ `localRoot` values) abort the run before any file is touched, so a malformed entry fails loudly
277
+ instead of corrupting state.
278
+ - **Cross-OS `claude --resume` cwd binding.** Sessions embed the cwd where they were created, so
279
+ Claude Code's picker's `cd ... && claude --resume <id>` line fails on a different host. Use
280
+ `nomad doctor --resume-cmd <id>` for a host-local equivalent (see
281
+ [Cross-OS resume](#cross-os-resume)). The sidecar approach preserves transcript byte-equality.
282
+ - **Empty directories don't survive sync.** Git doesn't track empty dirs; `nomad doctor` reports
283
+ them as `missing` (benign). Drop a `.gitkeep` to force materialization.
216
284
 
217
285
  ## Requirements
218
286
 
219
- - Node.js 22.22.1 or newer (24 LTS recommended; the npm `engines` field declares the 22.22.1 floor and surfaces a warning on older runtimes - npm only blocks the install when `engine-strict=true` is configured)
220
- - `tsx` (ships as a runtime dependency of the published package; no separate global install required)
287
+ - Node.js 22.22.1 or newer (24 LTS recommended; the npm `engines` field declares the 22.22.1 floor
288
+ and surfaces a warning on older runtimes - npm only blocks the install when `engine-strict=true`
289
+ is configured)
290
+ - `tsx` (ships as a runtime dependency of the published package; no separate global install
291
+ required)
221
292
  - Git
222
- - [`gitleaks`](https://github.com/gitleaks/gitleaks) (required for `nomad push`, which exits with an error if it is not on PATH; `nomad doctor` also checks it against the pinned 8.30.x and warns when it is absent or mismatched)
293
+ - [`gitleaks`](https://github.com/gitleaks/gitleaks) (required for `nomad push`, which exits with an
294
+ error if it is not on PATH; `nomad doctor` also checks it against the pinned 8.30.x and warns when
295
+ it is absent or mismatched)
223
296
  - A **private** GitHub repo (or any Git remote you control)
224
297
 
225
- **Optional:**
298
+ **Optional, but recommended:**
226
299
 
227
- - `gh` (GitHub CLI), used only by `nomad init` to auto-disable Actions on the private repo; if it is missing or unauthenticated, init prints a manual fallback tip and continues
228
- - `curl`, used only by the version/update check (the `nomad doctor` latest-release line and the post-`nomad update` check); it degrades silently when curl is absent or offline, so the rest of the CLI works without it
300
+ - `gh` ([GitHub CLI](https://cli.github.com/)), used only by `nomad init` to auto-disable Actions on
301
+ the private repo; if it is missing or unauthenticated, init prints a manual fallback tip and
302
+ continues
303
+ - [curl](https://curl.se/), used only by the version/update check (the `nomad doctor` latest-release
304
+ line and the post-`nomad update` check); it degrades silently when curl is absent or offline, so
305
+ the rest of the CLI works without it
229
306
 
230
307
  ## Setup
231
308
 
232
- **Why not just fork?** GitHub doesn't let you flip a public fork to private, and your config (especially session transcripts) must stay private. So the bootstrap is a one-time mirror-push into a fresh private repo, not a fork.
309
+ **Why not just fork?** GitHub doesn't let you flip a public fork to private, and your config
310
+ (especially session transcripts) must stay private. So the bootstrap is a one-time mirror-push into
311
+ a fresh private repo, not a fork.
233
312
 
234
313
  ### Privacy by default
235
314
 
236
- When you mirror-push the tool into your repo, you copy its automation along with its code: the `.github/workflows/` directory holds the public project's own CI (running its test suite, linting, secret and code scanning, release tagging, and npm publishing). That CI is meant for the public project, not your config; if it ran on your private mirror, a job could echo transcript contents into build logs. So your mirror gets two independent layers of defense against that, both applied automatically:
315
+ When you mirror-push the tool into your repo, you copy its automation along with its code: the
316
+ `.github/workflows/` directory holds the public project's own CI (running its test suite, linting,
317
+ secret and code scanning, release tagging, and npm publishing). That CI is meant for the public
318
+ project, not your config; if it ran on your private mirror, a job could echo transcript contents
319
+ into build logs. So your mirror gets two independent layers of defense against that, both applied
320
+ automatically:
237
321
 
238
- 1. **The workflows are written to skip private repos.** Each one carries the run condition `${{ !github.event.repository.private }}` (in plain terms: "run only when this repo is NOT private"), so even with Actions enabled the jobs do not run on your mirror.
239
- 2. **`nomad init` turns Actions off for the whole repo** on first run, via the GitHub API call `gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false`. This needs the `gh` CLI installed and authed; if it is missing or unauthed, init logs a manual fallback tip and continues.
322
+ 1. **The workflows are written to skip private repos.** Each one carries the run condition
323
+ `${{ !github.event.repository.private }}` (in plain terms: "run only when this repo is NOT
324
+ private"), so even with Actions enabled the jobs do not run on your mirror.
325
+ 2. **`nomad init` turns Actions off for the whole repo** on first run, via the GitHub API call
326
+ `gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false`. This needs the `gh`
327
+ CLI installed and authed; if it is missing or unauthed, init logs a manual fallback tip and
328
+ continues.
240
329
 
241
- Pass `--keep-actions` to either form of init to skip step 2 (for example, when your org already enforces an Actions policy upstream).
330
+ Pass `--keep-actions` to either form of init to skip step 2 (for example, when your org already
331
+ enforces an Actions policy upstream).
242
332
 
243
- > [!WARNING]
244
- > If you ever flip the mirror to public, both protections evaporate: CI starts firing on every `nomad push` against `main`, and your session transcripts (which include conversation content) become world-readable. Keep it private.
333
+ > [!WARNING] If you ever flip the mirror to public, both protections evaporate: CI starts firing on
334
+ > every `nomad push` against `main`, and your session transcripts (which include conversation
335
+ > content) become world-readable. **Keep it private.**
245
336
 
246
337
  ### Bootstrap
247
338
 
@@ -254,10 +345,10 @@ $ gh repo create <your-username>/claude-nomad --private
254
345
  # 2. Copy the public tool into your private repo. A bare clone followed by a
255
346
  # mirror push makes a complete, independent copy (every branch and tag) with
256
347
  # no fork link back to upstream, which is what lets you keep it private. Once, ever.
257
- $ git clone --bare git@github.com:funkadelic/claude-nomad.git /tmp/cn.git # download a full copy
258
- $ cd /tmp/cn.git
348
+ $ git clone --bare git@github.com:funkadelic/claude-nomad.git /tmp/claude-nomad.git # download a full copy
349
+ $ cd /tmp/claude-nomad.git
259
350
  $ git push --mirror git@github.com:<your-username>/claude-nomad.git # upload it to your private repo
260
- $ cd .. && rm -rf /tmp/cn.git
351
+ $ cd .. && rm -rf /tmp/claude-nomad.git
261
352
 
262
353
  # 3. Install the CLI globally and clone your private copy. Repeat on every host.
263
354
  $ npm i -g claude-nomad
@@ -267,15 +358,22 @@ $ git clone git@github.com:<your-username>/claude-nomad.git ~/claude-nomad
267
358
  export NOMAD_HOST=<your-host-label> # any short, stable label; nomad reads this instead of os.hostname()
268
359
  ```
269
360
 
270
- `npm i -g claude-nomad` puts a `nomad` binary on your PATH. The bin shim is the existing `src/nomad.ts` entrypoint resolved through tsx (a runtime dependency); no compile step. (The Node version floor and the `engine-strict` caveat are in [Requirements](#requirements).)
361
+ `npm i -g claude-nomad` puts a `nomad` binary on your PATH. The bin shim is the existing
362
+ `src/nomad.ts` entrypoint resolved through tsx (a runtime dependency); no compile step. (The Node
363
+ version floor and the `engine-strict` caveat are in [Requirements](#requirements).)
271
364
 
272
- On every additional host you repeat only steps 3-4; steps 1-2 are already done, since your private repo lives on the remote from step 2.
365
+ On every additional host you repeat only steps 3-4; steps 1-2 are already done, since your private
366
+ repo lives on the remote from step 2.
273
367
 
274
- `NOMAD_HOST` overrides `os.hostname()`, which returns noisy values like `WINDOWS-I5NT6OH` on WSL or `<name>.local` on macOS. Pick a clean label per machine (e.g., `wsl-laptop`, `macbook`, `homelab-nuc`). `nomad doctor` reports the resolved host so you can confirm.
368
+ `NOMAD_HOST` overrides `os.hostname()`, which returns noisy values like `WINDOWS-I5NT6OH` on WSL or
369
+ `<name>.local` on macOS. Pick a clean label per machine (e.g., `wsl-laptop`, `macbook`,
370
+ `homelab-nuc`). `nomad doctor` reports the resolved host so you can confirm.
275
371
 
276
372
  ### Initialize the repo layout
277
373
 
278
- First host only; subsequent hosts just clone and `nomad pull`. Both forms below auto-disable Actions on a detected private GitHub mirror as described in [Privacy by default](#privacy-by-default). Pick one:
374
+ First host only; subsequent hosts just clone and `nomad pull`. Both forms below auto-disable Actions
375
+ on a detected private GitHub mirror as described in [Privacy by default](#privacy-by-default). Pick
376
+ one:
279
377
 
280
378
  ```bash
281
379
  # Fresh start: scaffold an empty shared/, hosts/, path-map.json skeleton.
@@ -285,12 +383,12 @@ $ nomad init
285
383
  # starting point. Stages shared/ and writes hosts/<NOMAD_HOST>.json from
286
384
  # your current ~/.claude/settings.json. Does NOT touch the originals.
287
385
  $ nomad init --snapshot
288
-
289
- # Either form accepts --keep-actions to skip the auto-disable.
290
- $ nomad init --keep-actions
291
386
  ```
292
387
 
293
- `nomad init` refuses to clobber existing scaffold artifacts, so re-running on a populated repo is a safe no-op (it errors out naming the offender). `nomad pull` against an unscaffolded repo fails fast with `FATAL: repo not initialized; run 'nomad init' to scaffold` instead of silently leaving a half-state.
388
+ `nomad init` refuses to clobber existing scaffold artifacts, so re-running on a populated repo is a
389
+ safe no-op (it errors out naming the offender). `nomad pull` against an unscaffolded repo fails fast
390
+ with `FATAL: repo not initialized; run 'nomad init' to scaffold` instead of silently leaving a
391
+ half-state.
294
392
 
295
393
  Edit `path-map.json` to add your logical projects (see [Path remapping](#path-remapping)), then:
296
394
 
@@ -302,13 +400,19 @@ $ nomad push # send current state to the private remote
302
400
  $ nomad pull # apply on another host (or this one after a remote update)
303
401
  ```
304
402
 
305
- `nomad pull --dry-run` is the network-aware twin of `nomad diff`: it acquires the lock and runs `git pull` so you see what the next real pull would do given the latest remote, then exits without mutating.
403
+ `nomad pull --dry-run` is the network-aware twin of `nomad diff`: it acquires the lock and runs
404
+ `git pull` so you see what the next real pull would do given the latest remote, then exits without
405
+ mutating.
306
406
 
307
- If the destination host already has populated `~/.claude/{CLAUDE.md, agents/, ...}`, the first `nomad pull` will refuse to overwrite real files. See [Migrating an existing ~/.claude/](#migrating-an-existing-claude) for the safe migration flow.
407
+ If the destination host already has populated `~/.claude/{CLAUDE.md, agents/, ...}`, the first
408
+ `nomad pull` will refuse to overwrite real files. See
409
+ [Migrating an existing ~/.claude/](#migrating-an-existing-claude) for the safe migration flow.
308
410
 
309
411
  ## Migrating an existing ~/.claude/
310
412
 
311
- If a host already has real files at `~/.claude/{CLAUDE.md, agents/, skills/, ...}` and you want to bring them into the sync, the required sequence is `nomad init --snapshot` → `nomad push` → `nomad pull`:
413
+ If a host already has real files at `~/.claude/{CLAUDE.md, agents/, skills/, ...}` and you want to
414
+ bring them into the sync, the required sequence is `nomad init --snapshot` → `nomad push` →
415
+ `nomad pull`:
312
416
 
313
417
  ```bash
314
418
  # From the host that has the canonical config (the originals are not modified):
@@ -319,22 +423,44 @@ $ nomad push # publish the captured state to the private remote
319
423
  $ nomad pull # materializes the symlinks
320
424
  ```
321
425
 
322
- `nomad pull` is what actually migrates the host. `applySharedLinks` runs a two-pass scan: any pre-existing non-symlink at a `SHARED_LINKS` path whose counterpart exists under `shared/` is renamed into `~/.cache/claude-nomad/backup/<ts>/` first, then the symlink is created. Your originals are preserved under that timestamped backup directory, not deleted. Paths whose `shared/<name>` is absent from the remote are left untouched, so a partial publish does not delete data on the destination host.
426
+ `nomad pull` is what actually migrates the host. `applySharedLinks` runs a two-pass scan: any
427
+ pre-existing non-symlink at a `SHARED_LINKS` path whose counterpart exists under `shared/` is
428
+ renamed into `~/.cache/claude-nomad/backup/<ts>/` first, then the symlink is created. Your originals
429
+ are preserved under that timestamped backup directory, not deleted. Paths whose `shared/<name>` is
430
+ absent from the remote are left untouched, so a partial publish does not delete data on the
431
+ destination host.
323
432
 
324
- If the remote has not been populated yet (you skipped `nomad init --snapshot` and `nomad push`), `nomad pull` is a no-op for SHARED_LINKS: there is nothing on the remote to symlink against, so your local `~/.claude/` files stay in place. The auto-move only triggers once the canonical state is published.
433
+ If the remote has not been populated yet (you skipped `nomad init --snapshot` and `nomad push`),
434
+ `nomad pull` is a no-op for SHARED_LINKS: there is nothing on the remote to symlink against, so your
435
+ local `~/.claude/` files stay in place. The auto-move only triggers once the canonical state is
436
+ published.
325
437
 
326
- Prefer an explicit tarball rollback and a confirmation prompt before any deletion? Write the equivalent under `scripts/`: tar the `SHARED_LINKS` entries under `~/.claude/` first, copy into `shared/`, prompt, then `nomad pull`. The auto-move path above is the recommended default.
438
+ Prefer an explicit tarball rollback and a confirmation prompt before any deletion? Write the
439
+ equivalent under `scripts/`: tar the `SHARED_LINKS` entries under `~/.claude/` first, copy into
440
+ `shared/`, prompt, then `nomad pull`. The auto-move path above is the recommended default.
327
441
 
328
442
  ## Upgrading the tool
329
443
 
330
444
  Two different things can fall behind, and they update independently:
331
445
 
332
- - **The `nomad` CLI binary** (what runs when you type `nomad`). If you installed it with `npm i -g claude-nomad`, upgrade it with `npm update -g claude-nomad`. This refreshes only the binary on your PATH; it does not touch anything inside your private `~/claude-nomad/` repo.
333
- - **The synced tool files inside your private repo:** `src/`, `.gitleaks.toml` (the secret-scan allowlist), and the `.github/workflows/` privacy gating. These were copied from the public repo at bootstrap and then froze, so `npm update -g` does not refresh them. `nomad update`, run from `~/claude-nomad/`, is what pulls newer versions of these files in. Topology-aware: detects vanilla vs fork remotes, pulls or merges upstream, and re-runs `npm install` when `package-lock.json` shifted.
334
-
335
- Most people who followed the Quickstart need both: `npm update -g` for the binary, and an occasional `nomad update` for the repo files (notably to receive `.gitleaks.toml` allowlist changes and any update to the privacy gating itself). The mirror-push bootstrap leaves your repo with `origin` on your private mirror and no `upstream` remote; that becomes the "fork" topology `nomad update` expects once you add the upstream remote (the one-time `git remote add upstream ...` step is below).
336
-
337
- Your private repo is not a fork, so GitHub's "Sync fork" UI doesn't apply. The shortcut on a source-checkout host is:
446
+ - **The `nomad` CLI binary** (what runs when you type `nomad`). If you installed it with
447
+ `npm i -g claude-nomad`, upgrade it with `npm update -g claude-nomad`. This refreshes only the
448
+ binary on your PATH; it does not touch anything inside your private `~/claude-nomad/` repo.
449
+ - **The synced tool files inside your private repo:** `src/`, `.gitleaks.toml` (the secret-scan
450
+ allowlist), and the `.github/workflows/` privacy gating. These were copied from the public repo at
451
+ bootstrap and then froze, so `npm update -g` does not refresh them. `nomad update`, run from
452
+ `~/claude-nomad/`, is what pulls newer versions of these files in. Topology-aware: detects vanilla
453
+ vs fork remotes, pulls or merges upstream, and re-runs `npm install` when `package-lock.json`
454
+ shifted.
455
+
456
+ Most people who followed the Quickstart need both: `npm update -g` for the binary, and an occasional
457
+ `nomad update` for the repo files (notably to receive `.gitleaks.toml` allowlist changes and any
458
+ update to the privacy gating itself). The mirror-push bootstrap leaves your repo with `origin` on
459
+ your private mirror and no `upstream` remote; that becomes the "fork" topology `nomad update`
460
+ expects once you add the upstream remote (the one-time `git remote add upstream ...` step is below).
461
+
462
+ Your private repo is not a fork, so GitHub's "Sync fork" UI doesn't apply. The shortcut on a
463
+ source-checkout host is:
338
464
 
339
465
  ```bash
340
466
  $ cd ~/claude-nomad
@@ -344,11 +470,23 @@ $ nomad update
344
470
  `nomad update` detects which layout your `~/claude-nomad/` uses and does the right thing:
345
471
 
346
472
  - **vanilla** (`origin` points at the public repo): `git pull --ff-only origin main`.
347
- - **fork** (`upstream` points at the public repo, `origin` points at your private mirror): `git fetch upstream`, then (before merging) commit any whitelisted `shared/extras/` content that is still untracked locally so an overlap with upstream becomes a normal file merge instead of an untracked-overwrite abort, `git merge upstream/main`, then prompt before pushing the merge to `origin/main`. Pass `--push-origin` to skip the prompt. When the merge is a no-op (HEAD unchanged, nothing new to push) the prompt is skipped entirely and `nomad update` logs `already in sync with origin/main`.
348
-
349
- Pre-flight checks run before any mutation: `REPO_HOME` exists, the topology resolves to `vanilla` or `fork`, the current branch is `main`, the working tree is clean (override with `--force`), and `--push-origin` is rejected on vanilla topology.
350
-
351
- After the merge or pull, `nomad update` re-runs `npm install` only when `package-lock.json` actually shifted, commits the regenerated `package-lock.json` (fork topology) if the reinstall changed it, then invokes `nomad doctor`. The trailing version-check is non-fatal: `✓` when local matches the latest release, `⚠︎` when behind, an informational `ℹ︎ ... ahead of latest release` line when ahead (e.g. a `-dev` build between releases), and silent on network failures.
473
+ - **fork** (`upstream` points at the public repo, `origin` points at your private mirror):
474
+ `git fetch upstream`, then (before merging) commit any whitelisted `shared/extras/` content that
475
+ is still untracked locally so an overlap with upstream becomes a normal file merge instead of an
476
+ untracked-overwrite abort, `git merge upstream/main`, then prompt before pushing the merge to
477
+ `origin/main`. Pass `--push-origin` to skip the prompt. When the merge is a no-op (HEAD unchanged,
478
+ nothing new to push) the prompt is skipped entirely and `nomad update` logs
479
+ `already in sync with origin/main`.
480
+
481
+ Pre-flight checks run before any mutation: `REPO_HOME` exists, the topology resolves to `vanilla` or
482
+ `fork`, the current branch is `main`, the working tree is clean (override with `--force`), and
483
+ `--push-origin` is rejected on vanilla topology.
484
+
485
+ After the merge or pull, `nomad update` re-runs `npm install` only when `package-lock.json` actually
486
+ shifted, commits the regenerated `package-lock.json` (fork topology) if the reinstall changed it,
487
+ then invokes `nomad doctor`. The trailing version-check is non-fatal: `✓` when local matches the
488
+ latest release, `⚠︎` when behind, an informational `ℹ︎ ... ahead of latest release` line when ahead
489
+ (e.g. a `-dev` build between releases), and silent on network failures.
352
490
 
353
491
  Common cases:
354
492
 
@@ -365,9 +503,16 @@ One-time setup if you're running a fork layout and don't have the `upstream` rem
365
503
  $ git remote add upstream git@github.com:funkadelic/claude-nomad.git
366
504
  ```
367
505
 
368
- To pin to a specific release (`vX.Y.Z`, tagged by release-please) instead of tracking `main`, fetch tags from the public repo and check out the tag (detached HEAD). On vanilla topology that's `origin`; on fork topology that's `upstream` (the private mirror at `origin` does not accumulate upstream release tags). Example: `git fetch upstream --tags && git switch --detach vX.Y.Z` (substitute `origin` for vanilla; use `git checkout vX.Y.Z` on older Git).
506
+ To pin to a specific release (`vX.Y.Z`, tagged by release-please) instead of tracking `main`, fetch
507
+ tags from the public repo and check out the tag (detached HEAD). On vanilla topology that's
508
+ `origin`; on fork topology that's `upstream` (the private mirror at `origin` does not accumulate
509
+ upstream release tags). Example: `git fetch upstream --tags && git switch --detach vX.Y.Z`
510
+ (substitute `origin` for vanilla; use `git checkout vX.Y.Z` on older Git).
369
511
 
370
- If you installed an earlier version via `./install.sh` and a shell alias (the pre-npm path), your existing alias keeps working unchanged. Run `npm i -g claude-nomad` whenever you're ready to switch to the global binary, confirm `nomad --version` resolves to the npm install (`which nomad` should point under your npm prefix's `bin/`), then delete the alias line from your shell rc.
512
+ If you installed an earlier version via `./install.sh` and a shell alias (the pre-npm path), your
513
+ existing alias keeps working unchanged. Run `npm i -g claude-nomad` whenever you're ready to switch
514
+ to the global binary, confirm `nomad --version` resolves to the npm install (`which nomad` should
515
+ point under your npm prefix's `bin/`), then delete the alias line from your shell rc.
371
516
 
372
517
  ## Commands
373
518
 
@@ -388,11 +533,23 @@ If you installed an earlier version via `./install.sh` and a shell alias (the pr
388
533
  | `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). |
389
534
  | `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. |
390
535
 
391
- The version-check emits ``⚠︎ version: <local> -> <latest> (run `nomad update`)`` when the local install is behind the latest upstream release, and `✓ version: <local> (latest)` when current. It silently skips on network failures.
536
+ The version-check emits ``⚠︎ claude-nomad: <local> -> <latest> (run `nomad update`)`` when the local
537
+ install is behind the latest upstream release, and `✓ claude-nomad: <local> (latest)` when current.
538
+ It silently skips on network failures.
392
539
 
393
- Two further `⚠︎`-only drift checks run in `nomad doctor`. The gitleaks version-drift line `⚠︎ gitleaks: <local> -> <pinned> (...)` fires when the local gitleaks major.minor differs from the CI-pinned `GITLEAKS_PINNED_VERSION` (gitleaks rule and allowlist behavior tracks the minor line, so a patch-only difference stays `✓`), and is silent when gitleaks is not on PATH. The mirror-Actions line (carrying a `gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false` remediation hint) fires when origin is a private GitHub mirror that is gh-authed with Actions re-enabled, complementing the auto-disable that runs on `nomad init` (see [Privacy by default](#privacy-by-default)); it is silent on every prerequisite miss (non-GitHub origin, `gh` unauthed, public repo, or Actions already off).
540
+ Two further `⚠︎`-only drift checks run in `nomad doctor`. The gitleaks version-drift line
541
+ `⚠︎ gitleaks: <local> -> <pinned> (...)` fires when the local gitleaks major.minor differs from the
542
+ CI-pinned `GITLEAKS_PINNED_VERSION` (gitleaks rule and allowlist behavior tracks the minor line, so
543
+ a patch-only difference stays `✓`), and is silent when gitleaks is not on PATH. The mirror-Actions
544
+ line (carrying a `gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false`
545
+ remediation hint) fires when origin is a private GitHub mirror that is gh-authed with Actions
546
+ re-enabled, complementing the auto-disable that runs on `nomad init` (see
547
+ [Privacy by default](#privacy-by-default)); it is silent on every prerequisite miss (non-GitHub
548
+ origin, `gh` unauthed, public repo, or Actions already off).
394
549
 
395
- Every `nomad pull`, `nomad push`, and `nomad diff` run ends with a single `summary:` line. The status glyph (`✓` green / `⚠︎` yellow / `✗` red / `ℹ︎` dim) carries the severity, mirroring `nomad doctor`'s left-gutter format:
550
+ Every `nomad pull`, `nomad push`, and `nomad diff` run ends with a single `summary:` line. The
551
+ status glyph (`✓` green / `⚠︎` yellow / `✗` red / `ℹ︎` dim) carries the severity, mirroring
552
+ `nomad doctor`'s left-gutter format:
396
553
 
397
554
  ```text
398
555
  ✓ summary: clean
@@ -400,32 +557,55 @@ Every `nomad pull`, `nomad push`, and `nomad diff` run ends with a single `summa
400
557
  ⚠︎ summary: 2 unmapped on push, 1 collisions (run nomad doctor to list)
401
558
  ```
402
559
 
403
- `✓` lines go to stdout; `⚠︎` and `✗` lines go to stderr. The summary is suppressed when a fatal (`✗`) fires mid-run so you do not see "summary: clean" stacked under an error. Drive-by projects that have no entry in `path-map.json` for this host count as unmapped; the hint points at `nomad doctor`, which lists them by logical name.
560
+ `✓` lines go to stdout; `⚠︎` and `✗` lines go to stderr. The summary is suppressed when a fatal (`✗`)
561
+ fires mid-run so you do not see "summary: clean" stacked under an error. Drive-by projects that have
562
+ no entry in `path-map.json` for this host count as unmapped; the hint points at `nomad doctor`,
563
+ which lists them by logical name.
404
564
 
405
565
  ## Recovery flows
406
566
 
407
567
  ### `nomad drop-session <id>`
408
568
 
409
- Surgically unstages every `shared/projects/*/<id>.jsonl` plus the sibling `shared/projects/*/<id>/` subagent directory (whose nested transcripts are keyed by the same session id) from the staged tree of `~/claude-nomad/`. The local `~/.claude/projects/<encoded>/<id>.jsonl` and the local `<id>/` tree are never touched.
569
+ Surgically unstages every `shared/projects/*/<id>.jsonl` plus the sibling `shared/projects/*/<id>/`
570
+ subagent directory (whose nested transcripts are keyed by the same session id) from the staged tree
571
+ of `~/claude-nomad/`. The local `~/.claude/projects/<encoded>/<id>.jsonl` and the local `<id>/` tree
572
+ are never touched.
410
573
 
411
574
  ```bash
412
575
  $ nomad drop-session <id>
413
576
  ```
414
577
 
415
- Single positional id (the session filename minus `.jsonl`). Anything else (missing id, leading dash, extra arg) exits 1 with a `usage:` line.
578
+ Single positional id (the session filename minus `.jsonl`). Anything else (missing id, leading dash,
579
+ extra arg) exits 1 with a `usage:` line.
416
580
 
417
- For each match in the staged tree, `cmdDropSession` (in `src/commands.drop-session.ts`) classifies the entry as tracked-in-HEAD vs newly-staged and unstages it via `git restore --staged --worktree --` or `git rm --cached -f --` respectively. The `<id>/` subagent directory is expanded into its staged entries via `git ls-files -z` so every nested transcript flows through the same per-entry classification; a session that has only a subagent directory (no flat `<id>.jsonl`) is still droppable. Idempotent: a second run on the same id sees no matching staged entries and exits 0.
581
+ For each match in the staged tree, `cmdDropSession` (in `src/commands.drop-session.ts`) classifies
582
+ the entry as tracked-in-HEAD vs newly-staged and unstages it via
583
+ `git restore --staged --worktree --` or `git rm --cached -f --` respectively. The `<id>/` subagent
584
+ directory is expanded into its staged entries via `git ls-files -z` so every nested transcript flows
585
+ through the same per-entry classification; a session that has only a subagent directory (no flat
586
+ `<id>.jsonl`) is still droppable. Idempotent: a second run on the same id sees no matching staged
587
+ entries and exits 0.
418
588
 
419
589
  Exit codes:
420
590
 
421
591
  - `0` on any drop, including an idempotent re-run.
422
- - `1` with `✗ no staged session matches <id>` on stderr when neither a `shared/projects/*/<id>.jsonl` nor a `shared/projects/*/<id>/` directory with staged entries matches.
592
+ - `1` with `✗ no staged session matches <id>` on stderr when neither a
593
+ `shared/projects/*/<id>.jsonl` nor a `shared/projects/*/<id>/` directory with staged entries
594
+ matches.
423
595
 
424
- What it does NOT do: touch the local `~/.claude/projects/<encoded>/<id>.jsonl` file or the local `<id>/` subagent tree. The local copies are preserved for `claude --resume`, grep recovery, or whatever the user wants. If the underlying secret is real, scrubbing or removing the local files is REQUIRED for durability, not optional housekeeping: `remapPush` (in `src/remap.ts`) re-mirrors the local content into the staged tree on the next push, so a drop without a local scrub re-stages the same secret.
596
+ What it does NOT do: touch the local `~/.claude/projects/<encoded>/<id>.jsonl` file or the local
597
+ `<id>/` subagent tree. The local copies are preserved for `claude --resume`, grep recovery, or
598
+ whatever the user wants. If the underlying secret is real, scrubbing or removing the local files is
599
+ REQUIRED for durability, not optional housekeeping: `remapPush` (in `src/remap.ts`) re-mirrors the
600
+ local content into the staged tree on the next push, so a drop without a local scrub re-stages the
601
+ same secret.
425
602
 
426
603
  ### Recovery flow: gitleaks FATAL on a session JSONL
427
604
 
428
- `nomad push` runs `gitleaks protect --staged` before commit. To catch the same findings before you push (and without mutating anything), run the read-only preflight `nomad doctor --check-shared`, which stages and scans the exact transcripts a push would publish. When findings live in a session transcript, the push aborts and names every affected session id and the recovery command:
605
+ `nomad push` runs `gitleaks protect --staged` before commit. To catch the same findings before you
606
+ push (and without mutating anything), run the read-only preflight `nomad doctor --check-shared`,
607
+ which stages and scans the exact transcripts a push would publish. When findings live in a session
608
+ transcript, the push aborts and names every affected session id and the recovery command:
429
609
 
430
610
  ```text
431
611
  ✗ gitleaks detected secrets in 1 session transcript(s).
@@ -439,22 +619,38 @@ After recovery, re-run nomad push.
439
619
 
440
620
  Two branches from here:
441
621
 
442
- 1. **Real secret.** Rotate the credential at its provider first (revoke in dashboard, issue replacement) before touching anything else. Running `nomad drop-session <sid-aaaa>` clears the contaminated copy from the current staged tree, but that alone is NOT durable: `remapPush` (in `src/remap.ts`) does a full rm-and-copy mirror of your LOCAL transcripts into `shared/projects/` on every push, so the next `nomad push` re-copies the un-scrubbed local file forward and re-stages the same secret. The durable fix is to rotate AND scrub or remove the local transcript at `~/.claude/projects/<encoded>/<sid-aaaa>.jsonl` (plus the sibling `<sid-aaaa>/` subagent directory under that encoded dir, if present) so the next `remapPush` carries clean content forward. Do not leave the local file un-scrubbed and expect the staged-tree drop to hold.
622
+ 1. **Real secret.** Rotate the credential at its provider first (revoke in dashboard, issue
623
+ replacement) before touching anything else. Running `nomad drop-session <sid-aaaa>` clears the
624
+ contaminated copy from the current staged tree, but that alone is NOT durable: `remapPush` (in
625
+ `src/remap.ts`) does a full rm-and-copy mirror of your LOCAL transcripts into `shared/projects/`
626
+ on every push, so the next `nomad push` re-copies the un-scrubbed local file forward and
627
+ re-stages the same secret. The durable fix is to rotate AND scrub or remove the local transcript
628
+ at `~/.claude/projects/<encoded>/<sid-aaaa>.jsonl` (plus the sibling `<sid-aaaa>/` subagent
629
+ directory under that encoded dir, if present) so the next `remapPush` carries clean content
630
+ forward. Do not leave the local file un-scrubbed and expect the staged-tree drop to hold.
443
631
 
444
- 2. **False positive.** Add an allowlist regex to `.gitleaks.toml` at the repo root that matches the noise pattern but not real-secret formats, commit it, then re-run `nomad push`. The new allowlist propagates to deploy hosts via `nomad update`.
632
+ 2. **False positive.** Add an allowlist regex to `.gitleaks.toml` at the repo root that matches the
633
+ noise pattern but not real-secret formats, commit it, then re-run `nomad push`. The new allowlist
634
+ propagates to deploy hosts via `nomad update`.
445
635
 
446
- `nomad drop-session` only acts on the staged tree of `~/claude-nomad/`. Active Claude Code sessions writing to the local file are not disturbed.
636
+ `nomad drop-session` only acts on the staged tree of `~/claude-nomad/`. Active Claude Code sessions
637
+ writing to the local file are not disturbed.
447
638
 
448
639
  ### `.gitleaks.toml` allowlist policy
449
640
 
450
- `gitleaks protect` runs against the staged tree on every `nomad push` and can flag structurally-distinguishable tool-output noise as `generic-api-key`. The repo-root `.gitleaks.toml` pre-allows four such patterns so routine pushes are not blocked:
641
+ `gitleaks protect` runs against the staged tree on every `nomad push` and can flag
642
+ structurally-distinguishable tool-output noise as `generic-api-key`. The repo-root `.gitleaks.toml`
643
+ pre-allows four such patterns so routine pushes are not blocked:
451
644
 
452
645
  - Sonar issue keys (`AY` prefix + 20+ url-safe chars).
453
646
  - gitleaks fingerprint format (`<context>:<rule>:<line>` emitted by gitleaks's own reports).
454
647
  - npm audit advisory hashes (anchored on the JSON shape `"id":"<40..64 hex>"`).
455
648
  - Coverage-report line-keys (`key=<hex> <path>:<line>`).
456
649
 
457
- The file extends the default gitleaks ruleset, so real high-entropy secrets like `ghp_*`, `sk_live_*`, `xoxb-*`, and `AKIA*` still fire. The allowlist patterns are structurally distinguishable from real-secret formats: a malformed credential cannot match an allowlist regex by accident.
650
+ The file extends the default gitleaks ruleset, so real high-entropy secrets like `ghp_*`,
651
+ `sk_live_*`, `xoxb-*`, and `AKIA*` still fire. The allowlist patterns are structurally
652
+ distinguishable from real-secret formats: a malformed credential cannot match an allowlist regex by
653
+ accident.
458
654
 
459
655
  ```toml
460
656
  [extend]
@@ -469,13 +665,23 @@ regexes = [
469
665
  ]
470
666
  ```
471
667
 
472
- File location: `.gitleaks.toml` at the public repo root (alongside `package.json`). At runtime both `probeGitleaks` (in `src/push-checks.ts`) and `runGitleaksScan` (in `src/push-gitleaks.ts`) conditionally pass `--config <REPO_HOME>/.gitleaks.toml` when the file exists. Hosts that have not yet run `nomad update` (or fresh clones predating the allowlist) fall back silently to the default gitleaks ruleset; there is no warning. Run `nomad update` to receive the latest allowlist.
668
+ File location: `.gitleaks.toml` at the public repo root (alongside `package.json`). At runtime both
669
+ `probeGitleaks` (in `src/push-checks.ts`) and `runGitleaksScan` (in `src/push-gitleaks.ts`)
670
+ conditionally pass `--config <REPO_HOME>/.gitleaks.toml` when the file exists. Hosts that have not
671
+ yet run `nomad update` (or fresh clones predating the allowlist) fall back silently to the default
672
+ gitleaks ruleset; there is no warning. Run `nomad update` to receive the latest allowlist.
473
673
 
474
- Editing: amend `.gitleaks.toml` in this repo, open a PR, and merge to `main`. Use TOML literal strings (triple single quotes, `'''regex'''`) for new regex entries so backslashes do not need escaping. Verify the new pattern does not match real-secret formats (`ghp_<36>`, `sk_live_*`, `xoxb-*`, `AKIA[A-Z0-9]{16}`, etc.) before merging. The propagation path is the same as any other repo update: `nomad update` on each host pulls the new file in.
674
+ Editing: amend `.gitleaks.toml` in this repo, open a PR, and merge to `main`. Use TOML literal
675
+ strings (triple single quotes, `'''regex'''`) for new regex entries so backslashes do not need
676
+ escaping. Verify the new pattern does not match real-secret formats (`ghp_<36>`, `sk_live_*`,
677
+ `xoxb-*`, `AKIA[A-Z0-9]{16}`, etc.) before merging. The propagation path is the same as any other
678
+ repo update: `nomad update` on each host pulls the new file in.
475
679
 
476
680
  ## Cross-OS resume
477
681
 
478
- Claude Code embeds the original `cwd` in each session transcript. When you resume on a different host where that path doesn't exist, the picker prints a `cd <orig-cwd> && claude --resume <id>` line that fails (the source-host path isn't there).
682
+ Claude Code embeds the original `cwd` in each session transcript. When you resume on a different
683
+ host where that path doesn't exist, the picker prints a `cd <orig-cwd> && claude --resume <id>` line
684
+ that fails (the source-host path isn't there).
479
685
 
480
686
  Run this instead:
481
687
 
@@ -489,7 +695,10 @@ Or pipe through bash:
489
695
  $ nomad doctor --resume-cmd <session-id> | bash
490
696
  ```
491
697
 
492
- `nomad doctor --resume-cmd <id>` reads the `.jsonl`'s recorded `cwd`, reverse-looks up the logical project in `path-map.json`, finds your current host's abspath for that logical, and prints `cd <local-abspath> && claude --resume <id>` to stdout. The command is read-only: it never modifies any transcript byte.
698
+ `nomad doctor --resume-cmd <id>` reads the `.jsonl`'s recorded `cwd`, reverse-looks up the logical
699
+ project in `path-map.json`, finds your current host's abspath for that logical, and prints
700
+ `cd <local-abspath> && claude --resume <id>` to stdout. The command is read-only: it never modifies
701
+ any transcript byte.
493
702
 
494
703
  If the session isn't mapped on this host, you'll see:
495
704
 
@@ -497,7 +706,10 @@ If the session isn't mapped on this host, you'll see:
497
706
  ✗ session <id> not mapped on this host; add the logical to path-map.json
498
707
  ```
499
708
 
500
- Other fatal surfaces: missing `~/.claude/projects/`, session id absent from every encoded dir, no `cwd` field anywhere in the transcript, missing `path-map.json`, recorded cwd not present in any logical's host map. All errors go to stderr prefixed with the red `✗` fail glyph; the success line goes to stdout as a bare shell command (no glyph) so `eval` works.
709
+ Other fatal surfaces: missing `~/.claude/projects/`, session id absent from every encoded dir, no
710
+ `cwd` field anywhere in the transcript, missing `path-map.json`, recorded cwd not present in any
711
+ logical's host map. All errors go to stderr prefixed with the red `✗` fail glyph; the success line
712
+ goes to stdout as a bare shell command (no glyph) so `eval` works.
501
713
 
502
714
  ## Run tests
503
715