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/CHANGELOG.md +25 -0
- package/README.md +314 -102
- package/package.json +9 -2
- package/src/commands.doctor.check-shared.scan.ts +29 -5
- package/src/commands.doctor.checks.pathmap.ts +2 -2
- package/src/commands.doctor.checks.repo.ts +85 -40
- package/src/commands.doctor.format.ts +22 -7
- package/src/commands.doctor.mirror-actions.ts +8 -3
- package/src/commands.doctor.ts +1 -1
- package/src/commands.doctor.version.ts +15 -9
- package/src/gh-actions.ts +19 -7
- package/src/init.ts +5 -0
- package/src/nomad.help.ts +51 -21
- package/src/nomad.ts +2 -6
- package/src/preview.ts +42 -42
- package/src/push-gitleaks.scan.ts +7 -0
package/README.md
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
# claude-nomad
|
|
2
2
|
|
|
3
3
|
[](https://github.com/funkadelic/claude-nomad/actions/workflows/tests.yml)
|
|
4
|
-
[](https://github.com/funkadelic/claude-nomad/actions/workflows/codeql.yml)
|
|
5
|
+
[](https://codecov.io/gh/funkadelic/claude-nomad)
|
|
6
|
+
[](https://www.npmjs.com/package/claude-nomad)
|
|
7
|
+
[](https://www.npmjs.com/package/claude-nomad)
|
|
8
|
+
[](LICENSE)
|
|
6
9
|
|
|
7
10
|

|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
>
|
|
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
|
|
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
|
|
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`
|
|
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
|
-
>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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).
|
|
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-
|
|
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
|
-
|
|
257
|
+
Results on `your-other-host`: opus 4.7, the local Ollama env var, plus the shared permissions array.
|
|
201
258
|
|
|
202
|
-
> [!CAUTION]
|
|
203
|
-
>
|
|
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
|
|
213
|
-
|
|
214
|
-
- **
|
|
215
|
-
|
|
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
|
|
220
|
-
|
|
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
|
|
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
|
|
228
|
-
|
|
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
|
|
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
|
|
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
|
|
239
|
-
|
|
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
|
|
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
|
-
>
|
|
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/
|
|
258
|
-
$ cd /tmp/
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`),
|
|
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
|
|
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
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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):
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
|
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
|
|
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 ``⚠︎
|
|
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
|
|
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
|
|
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 (`✗`)
|
|
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>/`
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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_*`,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|