claude-nomad 0.27.0 → 0.28.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 +13 -0
- package/README.md +54 -21
- package/package.json +1 -1
- package/src/commands.doctor.check-schema.ts +72 -0
- package/src/commands.doctor.ts +20 -2
- package/src/commands.drop-session.scrub-hint.ts +72 -0
- package/src/commands.drop-session.ts +5 -1
- package/src/config.ts +13 -42
- package/src/nomad.help.ts +2 -0
- package/src/nomad.ts +9 -4
- package/src/settings-keys.ts +124 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.28.0](https://github.com/funkadelic/claude-nomad/compare/v0.27.0...v0.28.0) (2026-05-28)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
* **doctor:** settings schema drift tooling (auto-sync PR + --check-schema) ([#168](https://github.com/funkadelic/claude-nomad/issues/168)) ([ac4ac21](https://github.com/funkadelic/claude-nomad/commit/ac4ac21f90148d7261b0b907bfdad43b3758f9fd))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
* **doctor:** resync KNOWN_SETTINGS_KEYS with official settings schema ([#166](https://github.com/funkadelic/claude-nomad/issues/166)) ([2b453e1](https://github.com/funkadelic/claude-nomad/commit/2b453e18c18520dd0a4df035ace3825709097bc1))
|
|
14
|
+
* drop-session scrub hint and README rendering/layout fixes ([#165](https://github.com/funkadelic/claude-nomad/issues/165)) ([0840ab4](https://github.com/funkadelic/claude-nomad/commit/0840ab408b72174b23532a0ea32c27df522cfe39))
|
|
15
|
+
|
|
3
16
|
## [0.27.0](https://github.com/funkadelic/claude-nomad/compare/v0.26.2...v0.27.0) (2026-05-28)
|
|
4
17
|
|
|
5
18
|
|
package/README.md
CHANGED
|
@@ -67,7 +67,7 @@ box, a personal rig and a work machine. [Get started in three steps.](#quickstar
|
|
|
67
67
|
## Quickstart
|
|
68
68
|
|
|
69
69
|
If you already have a private **claude-nomad** mirror (see [Setup](#setup) for the one-time
|
|
70
|
-
bootstrap), adding a new host is
|
|
70
|
+
bootstrap), adding a new host is two one-time steps, then the everyday loop:
|
|
71
71
|
|
|
72
72
|
```bash
|
|
73
73
|
$ npm i -g claude-nomad
|
|
@@ -157,16 +157,28 @@ so a clobbered dotfile variable does not break the CLI.
|
|
|
157
157
|
|
|
158
158
|
## What gets synced vs. not
|
|
159
159
|
|
|
160
|
-
| Category | Items
|
|
161
|
-
| ---------------------- |
|
|
162
|
-
| **Synced** | `CLAUDE.md`, `agents/`, `skills/`, `commands/`, `rules/`, `my-statusline.cjs`
|
|
163
|
-
| **Generated** | `settings.json`
|
|
164
|
-
| **Remapped** | `projects/` session transcripts
|
|
165
|
-
| **Per-project extras** |
|
|
166
|
-
| **Never synced** |
|
|
167
|
-
| **Auto-rehydrated** | `~/.claude/plugins/cache/<plugin>/...`
|
|
168
|
-
|
|
169
|
-
|
|
160
|
+
| Category | Items | Behavior |
|
|
161
|
+
| ---------------------- | ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
|
162
|
+
| **Synced** | `CLAUDE.md`, `agents/`, `skills/`, `commands/`, `rules/`, `my-statusline.cjs` | Symlinked into `~/.claude/` from `shared/`. |
|
|
163
|
+
| **Generated** | `settings.json` | Deep-merge of `settings.base.json` with `hosts/<hostname>.json`; rewritten every pull. |
|
|
164
|
+
| **Remapped** | `projects/` session transcripts | Copied with path translation per `path-map.json`. |
|
|
165
|
+
| **Per-project extras** | Whitelisted dirs like `.planning/`, or a root file like `CLAUDE.md` | Opt-in via the `extras` field in `path-map.json`; mirrored to/from `shared/extras/<logical>/`. |
|
|
166
|
+
| **Never synced** | OAuth and MCP state, shell history, per-host overrides, caches, scratch dirs | Per-host ephemeral state; left untouched in both directions. |
|
|
167
|
+
| **Auto-rehydrated** | `~/.claude/plugins/cache/<plugin>/...` | Re-downloaded by Claude Code from the `enabledPlugins` list; no per-host install. |
|
|
168
|
+
|
|
169
|
+
Pointers and specifics:
|
|
170
|
+
|
|
171
|
+
- **Synced** link names live in `SHARED_LINKS`, **whitelisted extras** names in `SUPPORTED_EXTRAS`,
|
|
172
|
+
and the full **never-synced** set in `NEVER_SYNC` (all in `src/config.ts`).
|
|
173
|
+
- **Never synced**, in full: `~/.claude.json` (OAuth, MCP state), `history.jsonl`,
|
|
174
|
+
`settings.local.json` (per-host overrides), `stats-cache.json`, `todos/`, `shell-snapshots/`,
|
|
175
|
+
`debug/`, `file-history/`, `plans/`, `session-env/`, `statsig/`, `telemetry/`, `ide/`.
|
|
176
|
+
- **Per-project extras** run a pre-pull divergence WARN that flags local edits before they get
|
|
177
|
+
overwritten.
|
|
178
|
+
|
|
179
|
+
<!-- prettier-ignore -->
|
|
180
|
+
> [!NOTE]
|
|
181
|
+
> Plugins that depend on host-specific state (external binaries, API keys in env, MCP server
|
|
170
182
|
> URLs) still need that side set up on each host. Put them in `hosts/<host>.json` or the plugin's
|
|
171
183
|
> own per-host config.
|
|
172
184
|
|
|
@@ -197,7 +209,9 @@ block opts a project into syncing whitelisted directories (or a single root file
|
|
|
197
209
|
}
|
|
198
210
|
```
|
|
199
211
|
|
|
200
|
-
|
|
212
|
+
<!-- prettier-ignore -->
|
|
213
|
+
> [!IMPORTANT]
|
|
214
|
+
> The host-label keys must match whatever you set `NOMAD_HOST=` to on each host (see
|
|
201
215
|
> [Setup](#setup)). Mismatched labels silently skip remap, so sessions land in the wrong host's
|
|
202
216
|
> encoded dir.
|
|
203
217
|
|
|
@@ -249,17 +263,25 @@ host-only model overrides).
|
|
|
249
263
|
|
|
250
264
|
```json
|
|
251
265
|
{
|
|
252
|
-
"model": "claude-opus-4-
|
|
266
|
+
"model": "claude-opus-4-8",
|
|
253
267
|
"env": { "OLLAMA_HOST": "http://localhost:11434" }
|
|
254
268
|
}
|
|
255
269
|
```
|
|
256
270
|
|
|
257
|
-
Results on `your-other-host`: opus 4.
|
|
271
|
+
Results on `your-other-host`: opus 4.8, the local Ollama env var, plus the shared permissions array.
|
|
258
272
|
|
|
259
|
-
|
|
273
|
+
<!-- prettier-ignore -->
|
|
274
|
+
> [!CAUTION]
|
|
275
|
+
> Never hand-edit `~/.claude/settings.json` on a synced host. It's regenerated on every
|
|
260
276
|
> `nomad pull` from base + host, so your edits will be clobbered. Edit the base or host file in the
|
|
261
277
|
> repo instead.
|
|
262
278
|
|
|
279
|
+
`nomad doctor` warns when `settings.json` carries a top-level key it does not recognize (a cue that
|
|
280
|
+
Claude Code added a setting). The recognized set is kept current against Claude Code's published
|
|
281
|
+
settings schema by a weekly automated PR in the public repo, so a periodic `nomad update` is what
|
|
282
|
+
keeps that warning quiet on your hosts. To check your own `settings.json` against the live schema on
|
|
283
|
+
demand, run `nomad doctor --check-schema`.
|
|
284
|
+
|
|
263
285
|
## What does NOT sync (deliberate trade-offs)
|
|
264
286
|
|
|
265
287
|
Read these before adopting so you opt in with eyes open.
|
|
@@ -300,9 +322,9 @@ Read these before adopting so you opt in with eyes open.
|
|
|
300
322
|
- `gh` ([GitHub CLI](https://cli.github.com/)), used only by `nomad init` to auto-disable Actions on
|
|
301
323
|
the private repo; if it is missing or unauthenticated, init prints a manual fallback tip and
|
|
302
324
|
continues
|
|
303
|
-
- [curl](https://curl.se/), used
|
|
304
|
-
|
|
305
|
-
the rest of the CLI works without it
|
|
325
|
+
- [curl](https://curl.se/), used by the version/update check (the `nomad doctor` latest-release line
|
|
326
|
+
and the post-`nomad update` check) and by `nomad doctor --check-schema`; it degrades silently when
|
|
327
|
+
curl is absent or offline, so the rest of the CLI works without it
|
|
306
328
|
|
|
307
329
|
## Setup
|
|
308
330
|
|
|
@@ -330,7 +352,9 @@ automatically:
|
|
|
330
352
|
Pass `--keep-actions` to either form of init to skip step 2 (for example, when your org already
|
|
331
353
|
enforces an Actions policy upstream).
|
|
332
354
|
|
|
333
|
-
|
|
355
|
+
<!-- prettier-ignore -->
|
|
356
|
+
> [!WARNING]
|
|
357
|
+
> If you ever flip the mirror to public, both protections evaporate: CI starts firing on
|
|
334
358
|
> every `nomad push` against `main`, and your session transcripts (which include conversation
|
|
335
359
|
> content) become world-readable. **Keep it private.**
|
|
336
360
|
|
|
@@ -531,6 +555,7 @@ point under your npm prefix's `bin/`), then delete the alias line from your shel
|
|
|
531
555
|
| `nomad doctor` | Read-only health check. Each line carries a status glyph (`✓` pass, `✗` fail, `⚠︎` warn); any `✗` sets `process.exitCode = 1` (`⚠︎` does not). Includes an offline-tolerant release-version staleness check plus two `⚠︎`-only drift checks: gitleaks version drift and, on a private GitHub mirror, re-enabled Actions. |
|
|
532
556
|
| `nomad doctor --resume-cmd <id>` | Print a host-local `cd ... && claude --resume <id>` line for a session (see [Cross-OS resume](#cross-os-resume)). |
|
|
533
557
|
| `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). |
|
|
558
|
+
| `nomad doctor --check-schema` | Read-only: fetches the live Claude Code settings schema and lists any `~/.claude/settings.json` key absent from it (candidates for the hand-maintained `APP_ONLY_KEYS` list). Non-fatal and offline-tolerant: skips with a `⚠︎` when curl is missing or the schema is unreachable. |
|
|
534
559
|
| `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. |
|
|
535
560
|
|
|
536
561
|
The version-check emits ``⚠︎ claude-nomad: <local> -> <latest> (run `nomad update`)`` when the local
|
|
@@ -614,8 +639,9 @@ synced, or a `⚠︎` warning naming the counts when something was skipped:
|
|
|
614
639
|
gitleaks missing when push checks for it, or a rebase conflict before anything is staged) suppresses
|
|
615
640
|
the tree entirely, so you do not see "summary: clean" stacked under an error. A later leak-scan
|
|
616
641
|
finding is different: by then the tree has already been built, so it still renders in full with a
|
|
617
|
-
`✗` Leak scan row and the recovery block below it (see
|
|
618
|
-
|
|
642
|
+
`✗` Leak scan row and the recovery block below it (see
|
|
643
|
+
[Recovery flow: gitleaks FATAL on a session JSONL](#recovery-flow-gitleaks-fatal-on-a-session-jsonl)).
|
|
644
|
+
Projects with no entry in `path-map.json` for this host count as unmapped and fold into the
|
|
619
645
|
collapsed `ℹ︎ ... not in path-map` count; the hint points at `nomad doctor`, which lists them by
|
|
620
646
|
logical name.
|
|
621
647
|
|
|
@@ -662,6 +688,13 @@ REQUIRED for durability, not optional housekeeping: `remapPush` (in `src/remap.t
|
|
|
662
688
|
local content into the staged tree on the next push, so a drop without a local scrub re-stages the
|
|
663
689
|
same secret.
|
|
664
690
|
|
|
691
|
+
A successful drop prints this reminder inline, pointing at the live transcript that still needs
|
|
692
|
+
scrubbing (the exact path when `path-map.json` maps the project to the current host, a generic
|
|
693
|
+
`~/.claude/projects/<encoded>/<id>.jsonl` template otherwise). This is why a
|
|
694
|
+
`nomad doctor --check-shared` run still reports the session after a drop: that scan reads the live
|
|
695
|
+
`~/.claude/projects/` source, not the staged tree, so it keeps flagging the secret until the local
|
|
696
|
+
transcript is scrubbed.
|
|
697
|
+
|
|
665
698
|
### Recovery flow: gitleaks FATAL on a session JSONL
|
|
666
699
|
|
|
667
700
|
`nomad push` runs `gitleaks protect --staged` before commit. To catch the same findings before you
|
package/package.json
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { dim, green, infoGlyph, okGlyph, warnGlyph, yellow } from './color.ts';
|
|
6
|
+
import { addItem, readJsonSafe, type DoctorSection } from './commands.doctor.format.ts';
|
|
7
|
+
import { CLAUDE_HOME, SETTINGS_SCHEMA_URL } from './config.ts';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Opt-in `nomad doctor --check-schema` reporter. Fetches the live Claude Code
|
|
11
|
+
* settings JSON schema and lists any top-level key in this host's
|
|
12
|
+
* `~/.claude/settings.json` that the published schema does not define, i.e.
|
|
13
|
+
* candidates for the hand-maintained `APP_ONLY_KEYS` list. Offline-tolerant by
|
|
14
|
+
* design (mirrors the release version check): curl missing, a network failure,
|
|
15
|
+
* or a malformed schema all degrade to a single `⚠︎` skip line. Never sets
|
|
16
|
+
* `process.exitCode`; this is informational discovery, not a gate.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Fetch the live settings schema via curl and return its top-level property
|
|
21
|
+
* names. curl is optional (matches the version check): a missing binary,
|
|
22
|
+
* non-2xx response, or malformed payload all surface as `null` so the caller
|
|
23
|
+
* skips cleanly. 3s timeout, fail-fast (`-f`), silent (`-s`), follow redirects.
|
|
24
|
+
*/
|
|
25
|
+
function fetchSchemaKeys(): string[] | null {
|
|
26
|
+
try {
|
|
27
|
+
const raw = execFileSync('curl', ['-fsSL', '-m', '3', SETTINGS_SCHEMA_URL], {
|
|
28
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
29
|
+
}).toString();
|
|
30
|
+
const parsed = JSON.parse(raw) as { properties?: Record<string, unknown> };
|
|
31
|
+
if (typeof parsed.properties !== 'object' || parsed.properties === null) return null;
|
|
32
|
+
return Object.keys(parsed.properties);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Append the `--check-schema` result to the supplied section: an info line when
|
|
40
|
+
* there is no local settings.json, a `⚠︎` skip when the schema cannot be
|
|
41
|
+
* fetched, an OK line when every key is in the schema, or a `⚠︎` line naming the
|
|
42
|
+
* keys absent from it (APP_ONLY_KEYS candidates).
|
|
43
|
+
*/
|
|
44
|
+
export function reportCheckSchema(section: DoctorSection): void {
|
|
45
|
+
const settingsPath = join(CLAUDE_HOME, 'settings.json');
|
|
46
|
+
if (!existsSync(settingsPath)) {
|
|
47
|
+
addItem(section, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const settings = readJsonSafe<Record<string, unknown>>(settingsPath, settingsPath, section);
|
|
51
|
+
if (settings === null) return;
|
|
52
|
+
|
|
53
|
+
const liveKeys = fetchSchemaKeys();
|
|
54
|
+
if (liveKeys === null) {
|
|
55
|
+
addItem(
|
|
56
|
+
section,
|
|
57
|
+
`${yellow(warnGlyph)} schema check skipped (offline, curl missing, or schema unreachable)`,
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const liveSet = new Set(liveKeys);
|
|
63
|
+
const candidates = Object.keys(settings).filter((k) => !liveSet.has(k));
|
|
64
|
+
if (candidates.length === 0) {
|
|
65
|
+
addItem(section, `${green(okGlyph)} settings.json keys all present in the published schema`);
|
|
66
|
+
} else {
|
|
67
|
+
addItem(
|
|
68
|
+
section,
|
|
69
|
+
`${yellow(warnGlyph)} settings.json keys absent from published schema (APP_ONLY_KEYS candidates): ${candidates.join(', ')}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/commands.doctor.ts
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
reportRebaseClean,
|
|
16
16
|
reportRemote,
|
|
17
17
|
} from './commands.doctor.checks.repository.ts';
|
|
18
|
+
import { reportCheckSchema } from './commands.doctor.check-schema.ts';
|
|
18
19
|
import { reportCheckShared } from './commands.doctor.check-shared.ts';
|
|
19
20
|
import { reportNodeEngineCheck } from './commands.doctor.engine.ts';
|
|
20
21
|
import { renderDoctor, section } from './commands.doctor.format.ts';
|
|
@@ -35,8 +36,12 @@ import { reportVersionCheck } from './commands.doctor.version.ts';
|
|
|
35
36
|
* section that runs the gitleaks preflight over the session transcripts a
|
|
36
37
|
* `nomad push` would stage. It is OFF by default so plain `nomad doctor`
|
|
37
38
|
* stays the fast read-only smoke test (no scan, no temp tree).
|
|
39
|
+
*
|
|
40
|
+
* `opts.checkSchema` (the `--check-schema` sub-flag) appends a "Schema scan"
|
|
41
|
+
* section that fetches the live settings schema and flags local settings.json
|
|
42
|
+
* keys absent from it. Also OFF by default (it needs the network).
|
|
38
43
|
*/
|
|
39
|
-
export function cmdDoctor(opts: { checkShared?: boolean } = {}): void {
|
|
44
|
+
export function cmdDoctor(opts: { checkShared?: boolean; checkSchema?: boolean } = {}): void {
|
|
40
45
|
const host = section('Host');
|
|
41
46
|
reportHostAndPaths(host);
|
|
42
47
|
reportRepoState(host);
|
|
@@ -74,5 +79,18 @@ export function cmdDoctor(opts: { checkShared?: boolean } = {}): void {
|
|
|
74
79
|
// drift check above spawns `gitleaks version` separately, by design.)
|
|
75
80
|
if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
const schemaScan = section('Schema scan');
|
|
83
|
+
if (opts.checkSchema === true) reportCheckSchema(schemaScan);
|
|
84
|
+
|
|
85
|
+
renderDoctor([
|
|
86
|
+
version,
|
|
87
|
+
host,
|
|
88
|
+
links,
|
|
89
|
+
settings,
|
|
90
|
+
pathMap,
|
|
91
|
+
neverSync,
|
|
92
|
+
repository,
|
|
93
|
+
sharedScan,
|
|
94
|
+
schemaScan,
|
|
95
|
+
]);
|
|
78
96
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { CLAUDE_HOME, HOST, REPO_HOME, type PathMap } from './config.ts';
|
|
5
|
+
import { log } from './utils.ts';
|
|
6
|
+
import { encodePath, readJson } from './utils.json.ts';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Repo-relative session-match shape `shared/projects/<logical>/...`; the single
|
|
10
|
+
* capture group is the `<logical>` segment fed to the path-map reverse lookup.
|
|
11
|
+
*/
|
|
12
|
+
const SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* After a successful drop, remind the operator that the unstage is per-push
|
|
16
|
+
* only: the leaked secret still lives in the local transcript, so the next
|
|
17
|
+
* `nomad push` re-copies it (via `remapPush`) and `nomad doctor --check-shared`
|
|
18
|
+
* keeps reporting it (it scans the live `~/.claude/projects/` source, not the
|
|
19
|
+
* repo index). Points at the exact live transcript when it resolves for this
|
|
20
|
+
* host, or a generic `~/.claude/projects/<encoded>/<id>.jsonl` template
|
|
21
|
+
* otherwise. Advisory output only; never mutates state.
|
|
22
|
+
*
|
|
23
|
+
* @param id Already-validated session id.
|
|
24
|
+
* @param matches Repo-relative paths collected by `collectMatches`.
|
|
25
|
+
*/
|
|
26
|
+
export function reportScrubHint(id: string, matches: string[]): void {
|
|
27
|
+
const live = resolveLiveTranscript(id, matches);
|
|
28
|
+
const target = live ?? `~/.claude/projects/<encoded>/${id}.jsonl`;
|
|
29
|
+
log(
|
|
30
|
+
'note: this only un-stages the session from the next push. The leaked secret\n' +
|
|
31
|
+
' is still in your local transcript, so nomad push re-stages it and nomad\n' +
|
|
32
|
+
' doctor --check-shared keeps reporting it. To remediate, rotate the\n' +
|
|
33
|
+
` credential, then scrub ${target}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Reverse-map a dropped session to its live transcript
|
|
39
|
+
* `~/.claude/projects/<encoded>/<id>.jsonl` on THIS host via `path-map.json`
|
|
40
|
+
* (`<logical>` -> `hosts[HOST]` -> `encodePath`). Best-effort: returns the path
|
|
41
|
+
* only when it resolves AND exists on disk; null when `path-map.json` is
|
|
42
|
+
* absent or malformed, no match maps to this host, or the live file is already
|
|
43
|
+
* gone. A `'TBD'` host placeholder also yields null (its bogus path never
|
|
44
|
+
* exists). The whole body is wrapped so a malformed map (parse error, `null`
|
|
45
|
+
* projects) degrades to the generic hint instead of crashing the drop.
|
|
46
|
+
*
|
|
47
|
+
* @param id Already-validated session id.
|
|
48
|
+
* @param matches Repo-relative paths collected by `collectMatches`.
|
|
49
|
+
* @returns Absolute live transcript path, or null when unresolvable.
|
|
50
|
+
*/
|
|
51
|
+
function resolveLiveTranscript(id: string, matches: string[]): string | null {
|
|
52
|
+
try {
|
|
53
|
+
const mapPath = join(REPO_HOME, 'path-map.json');
|
|
54
|
+
if (!existsSync(mapPath)) return null;
|
|
55
|
+
const projects = readJson<PathMap>(mapPath).projects;
|
|
56
|
+
for (const rel of matches) {
|
|
57
|
+
const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
|
|
58
|
+
/* c8 ignore next -- defensive: every collectMatches path is rooted at shared/projects/<logical>/ */
|
|
59
|
+
if (logical === undefined) continue;
|
|
60
|
+
const abs = projects[logical]?.[HOST];
|
|
61
|
+
// A 'TBD' host placeholder needs no special case: encodePath('TBD') yields
|
|
62
|
+
// a directory that cannot exist among the absolute-path-encoded dirs, so
|
|
63
|
+
// the existsSync guard below rejects it and falls through to the generic.
|
|
64
|
+
if (abs === undefined) continue;
|
|
65
|
+
const live = join(CLAUDE_HOME, 'projects', encodePath(abs), `${id}.jsonl`);
|
|
66
|
+
if (existsSync(live)) return live;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -4,6 +4,7 @@ import { join, relative } from 'node:path';
|
|
|
4
4
|
|
|
5
5
|
import { REPO_HOME } from './config.ts';
|
|
6
6
|
import { expandStagedDir, isInIndex, isTrackedInHead } from './commands.drop-session.git.ts';
|
|
7
|
+
import { reportScrubHint } from './commands.drop-session.scrub-hint.ts';
|
|
7
8
|
import { die, fail, log, NomadFatal } from './utils.ts';
|
|
8
9
|
import { acquireLock, releaseLock } from './utils.lockfile.ts';
|
|
9
10
|
|
|
@@ -13,7 +14,9 @@ import { acquireLock, releaseLock } from './utils.lockfile.ts';
|
|
|
13
14
|
* lock, collects every staged path matching the flat `<id>.jsonl` and the
|
|
14
15
|
* sibling subagent directory `<id>/` (via `collectMatches`), then unstages
|
|
15
16
|
* each (via `unstageOne`). This closes the leak where a "dropped" session
|
|
16
|
-
* still shipped its subagent transcripts.
|
|
17
|
+
* still shipped its subagent transcripts. A successful drop ends with
|
|
18
|
+
* `reportScrubHint`, which reminds the operator that the unstage is per-push
|
|
19
|
+
* only and points at the live transcript that still needs scrubbing.
|
|
17
20
|
*
|
|
18
21
|
* Idempotent: entries not in the index are skipped silently. Exits 0 on
|
|
19
22
|
* any drop (including an idempotent re-run); exits 1 with `✗ no staged
|
|
@@ -53,6 +56,7 @@ export function cmdDropSession(id: string): void {
|
|
|
53
56
|
throw new NomadFatal(`no staged session matches ${id}`);
|
|
54
57
|
}
|
|
55
58
|
for (const rel of matches) unstageOne(rel);
|
|
59
|
+
reportScrubHint(id, matches);
|
|
56
60
|
} catch (err) {
|
|
57
61
|
// Defensive escape hatch: only fires if a non-NomadFatal error escapes
|
|
58
62
|
// the try block. All execFileSync mutation failures are wrapped in
|
package/src/config.ts
CHANGED
|
@@ -36,6 +36,14 @@ export const REPO_HOME = process.env.NOMAD_REPO || resolve(HOME, 'claude-nomad')
|
|
|
36
36
|
*/
|
|
37
37
|
export const UPSTREAM_REPO_SLUG = 'funkadelic/claude-nomad';
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* The official Claude Code settings JSON schema. Source of truth for
|
|
41
|
+
* `SCHEMA_KEYS` (kept current by `scripts/sync-settings-keys.ts`) and the
|
|
42
|
+
* on-demand `nomad doctor --check-schema` reporter, which fetches it live to
|
|
43
|
+
* flag local `settings.json` keys absent from the published schema.
|
|
44
|
+
*/
|
|
45
|
+
export const SETTINGS_SCHEMA_URL = 'https://json.schemastore.org/claude-code-settings.json';
|
|
46
|
+
|
|
39
47
|
/**
|
|
40
48
|
* Pinned gitleaks version. Single source of truth for the gitleaks pin used by
|
|
41
49
|
* `nomad doctor`'s version-drift check (`reportGitleaksVersionCheck`), which
|
|
@@ -103,48 +111,11 @@ export const NEVER_SYNC = new Set([
|
|
|
103
111
|
'ide',
|
|
104
112
|
]);
|
|
105
113
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
*/
|
|
112
|
-
export const KNOWN_SETTINGS_KEYS = new Set<string>([
|
|
113
|
-
'$schema',
|
|
114
|
-
'agent',
|
|
115
|
-
'agents',
|
|
116
|
-
'agentPushNotifEnabled',
|
|
117
|
-
'allowedHttpHookUrls',
|
|
118
|
-
'apiKeyHelper',
|
|
119
|
-
'apiKeyHelperTimeoutMs',
|
|
120
|
-
'awsAuthRefresh',
|
|
121
|
-
'awsCredentialExport',
|
|
122
|
-
'awsLoginRefresh',
|
|
123
|
-
'awsRegion',
|
|
124
|
-
'awsRetryMode',
|
|
125
|
-
'cleanupPeriodDays',
|
|
126
|
-
'disableNonEssentialModelCalls',
|
|
127
|
-
'enabledExperimentalFeatures',
|
|
128
|
-
'enabledPlugins',
|
|
129
|
-
'env',
|
|
130
|
-
'forceLoginMethod',
|
|
131
|
-
'forceLoginOrgUUID',
|
|
132
|
-
'hooks',
|
|
133
|
-
'includeCoAuthoredBy',
|
|
134
|
-
'installMethod',
|
|
135
|
-
'model',
|
|
136
|
-
'outputStyle',
|
|
137
|
-
'permissions',
|
|
138
|
-
'pluginGroups',
|
|
139
|
-
'pluginRepositoryEnabled',
|
|
140
|
-
'pluginsLocalConfig',
|
|
141
|
-
'proxy',
|
|
142
|
-
'skipAutoPermissionPrompt',
|
|
143
|
-
'statsig',
|
|
144
|
-
'statusLine',
|
|
145
|
-
'subagents',
|
|
146
|
-
'theme',
|
|
147
|
-
]);
|
|
114
|
+
// Schema-drift baseline for `~/.claude/settings.json`; top-level keys not in
|
|
115
|
+
// this set trigger a `nomad doctor` WARN. Defined in ./settings-keys.ts so the
|
|
116
|
+
// schema-derived half can be re-synced mechanically; re-exported here so
|
|
117
|
+
// existing `from './config.ts'` imports keep resolving.
|
|
118
|
+
export { KNOWN_SETTINGS_KEYS } from './settings-keys.ts';
|
|
148
119
|
|
|
149
120
|
/**
|
|
150
121
|
* Static half of the push allow-list. Entries with trailing `/` are prefix
|
package/src/nomad.help.ts
CHANGED
|
@@ -49,6 +49,8 @@ export const DEFAULT_HELP = [
|
|
|
49
49
|
cont('gitleaks, gitlinks).'),
|
|
50
50
|
row(' --check-shared', 'Preflight gitleaks scan of the session transcripts a'),
|
|
51
51
|
cont('`nomad push` would stage (a temp copy, never the live dir).'),
|
|
52
|
+
row(' --check-schema', 'Flag settings.json keys absent from the live published'),
|
|
53
|
+
cont('Claude Code settings schema (needs network; degrades offline).'),
|
|
52
54
|
row(' --resume-cmd <id>', 'Print `cd <abspath> && claude --resume <id>` for a session id'),
|
|
53
55
|
cont('from ~/.claude/projects/.'),
|
|
54
56
|
'',
|
package/src/nomad.ts
CHANGED
|
@@ -131,13 +131,16 @@ try {
|
|
|
131
131
|
// Sub-flags: `doctor --resume-cmd <session-id>` dispatches to the
|
|
132
132
|
// read-only sidecar that prints `cd <abspath> && claude --resume <id>`;
|
|
133
133
|
// `doctor --check-shared` (no positional) appends the gitleaks preflight
|
|
134
|
-
// scan of the transcripts a push would stage
|
|
135
|
-
//
|
|
136
|
-
//
|
|
134
|
+
// scan of the transcripts a push would stage; `doctor --check-schema`
|
|
135
|
+
// (no positional) appends the live settings-schema check. Bare `doctor`
|
|
136
|
+
// runs the plain read-only health check. Any other shape (unknown flag,
|
|
137
|
+
// extra positional, a scan flag with trailing args) is a usage error.
|
|
137
138
|
if (process.argv[3] === undefined) {
|
|
138
139
|
cmdDoctor();
|
|
139
140
|
} else if (process.argv[3] === '--check-shared' && process.argv.length === 4) {
|
|
140
141
|
cmdDoctor({ checkShared: true });
|
|
142
|
+
} else if (process.argv[3] === '--check-schema' && process.argv.length === 4) {
|
|
143
|
+
cmdDoctor({ checkSchema: true });
|
|
141
144
|
} else if (process.argv[3] === '--resume-cmd') {
|
|
142
145
|
const id = process.argv[4];
|
|
143
146
|
if (process.argv.length !== 5 || typeof id !== 'string' || id.length === 0) {
|
|
@@ -146,7 +149,9 @@ try {
|
|
|
146
149
|
}
|
|
147
150
|
resumeCmd(id);
|
|
148
151
|
} else {
|
|
149
|
-
console.error(
|
|
152
|
+
console.error(
|
|
153
|
+
'usage: nomad doctor [--check-shared | --check-schema | --resume-cmd <session-id>]',
|
|
154
|
+
);
|
|
150
155
|
process.exit(1);
|
|
151
156
|
}
|
|
152
157
|
break;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level keys recognized in `~/.claude/settings.json`, used by the
|
|
3
|
+
* `nomad doctor` schema-drift check. Split by provenance so the schema half
|
|
4
|
+
* can be re-synced mechanically.
|
|
5
|
+
*
|
|
6
|
+
* `SCHEMA_KEYS`: the documented properties from the official Claude Code
|
|
7
|
+
* settings JSON schema (https://json.schemastore.org/claude-code-settings.json).
|
|
8
|
+
* Regenerated by scripts/sync-settings-keys.mjs (run weekly by the
|
|
9
|
+
* settings-schema-drift workflow); do not hand-edit this array.
|
|
10
|
+
*
|
|
11
|
+
* `APP_ONLY_KEYS`: keys Claude Code writes to settings.json that the published
|
|
12
|
+
* schema has not caught up to yet (the running app runs ahead of the schema).
|
|
13
|
+
* These cannot be derived from the schema and are hand-maintained.
|
|
14
|
+
*/
|
|
15
|
+
export const SCHEMA_KEYS = [
|
|
16
|
+
'$schema',
|
|
17
|
+
'agent',
|
|
18
|
+
'allowedChannelPlugins',
|
|
19
|
+
'allowedHttpHookUrls',
|
|
20
|
+
'allowedMcpServers',
|
|
21
|
+
'allowManagedHooksOnly',
|
|
22
|
+
'allowManagedMcpServersOnly',
|
|
23
|
+
'allowManagedPermissionRulesOnly',
|
|
24
|
+
'alwaysThinkingEnabled',
|
|
25
|
+
'apiKeyHelper',
|
|
26
|
+
'attribution',
|
|
27
|
+
'autoMemoryDirectory',
|
|
28
|
+
'autoMemoryEnabled',
|
|
29
|
+
'autoMode',
|
|
30
|
+
'autoUpdatesChannel',
|
|
31
|
+
'availableModels',
|
|
32
|
+
'awsAuthRefresh',
|
|
33
|
+
'awsCredentialExport',
|
|
34
|
+
'blockedMarketplaces',
|
|
35
|
+
'channelsEnabled',
|
|
36
|
+
'claudeMdExcludes',
|
|
37
|
+
'cleanupPeriodDays',
|
|
38
|
+
'companyAnnouncements',
|
|
39
|
+
'defaultShell',
|
|
40
|
+
'deniedMcpServers',
|
|
41
|
+
'disableAllHooks',
|
|
42
|
+
'disableDeepLinkRegistration',
|
|
43
|
+
'disabledMcpjsonServers',
|
|
44
|
+
'disableSkillShellExecution',
|
|
45
|
+
'effortLevel',
|
|
46
|
+
'enableAllProjectMcpServers',
|
|
47
|
+
'enabledMcpjsonServers',
|
|
48
|
+
'enabledPlugins',
|
|
49
|
+
'env',
|
|
50
|
+
'extraKnownMarketplaces',
|
|
51
|
+
'fastMode',
|
|
52
|
+
'fastModePerSessionOptIn',
|
|
53
|
+
'feedbackSurveyRate',
|
|
54
|
+
'fileSuggestion',
|
|
55
|
+
'forceLoginMethod',
|
|
56
|
+
'forceLoginOrgUUID',
|
|
57
|
+
'forceRemoteSettingsRefresh',
|
|
58
|
+
'hooks',
|
|
59
|
+
'httpHookAllowedEnvVars',
|
|
60
|
+
'includeCoAuthoredBy',
|
|
61
|
+
'includeGitInstructions',
|
|
62
|
+
'language',
|
|
63
|
+
'minimumVersion',
|
|
64
|
+
'model',
|
|
65
|
+
'modelOverrides',
|
|
66
|
+
'otelHeadersHelper',
|
|
67
|
+
'outputStyle',
|
|
68
|
+
'parentSettingsBehavior',
|
|
69
|
+
'permissions',
|
|
70
|
+
'plansDirectory',
|
|
71
|
+
'pluginConfigs',
|
|
72
|
+
'pluginTrustMessage',
|
|
73
|
+
'prefersReducedMotion',
|
|
74
|
+
'prUrlTemplate',
|
|
75
|
+
'respectGitignore',
|
|
76
|
+
'sandbox',
|
|
77
|
+
'showClearContextOnPlanAccept',
|
|
78
|
+
'showThinkingSummaries',
|
|
79
|
+
'showTurnDuration',
|
|
80
|
+
'skillOverrides',
|
|
81
|
+
'skipDangerousModePermissionPrompt',
|
|
82
|
+
'skippedMarketplaces',
|
|
83
|
+
'skippedPlugins',
|
|
84
|
+
'skipWebFetchPreflight',
|
|
85
|
+
'spinnerTipsEnabled',
|
|
86
|
+
'spinnerTipsOverride',
|
|
87
|
+
'spinnerVerbs',
|
|
88
|
+
'statusLine',
|
|
89
|
+
'strictKnownMarketplaces',
|
|
90
|
+
'strictPluginOnlyCustomization',
|
|
91
|
+
'subagentStatusLine',
|
|
92
|
+
'teammateMode',
|
|
93
|
+
'terminalProgressBarEnabled',
|
|
94
|
+
'tui',
|
|
95
|
+
'useAutoModeDuringPlan',
|
|
96
|
+
'viewMode',
|
|
97
|
+
'voiceEnabled',
|
|
98
|
+
'worktree',
|
|
99
|
+
'wslInheritsWindowsSettings',
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
export const APP_ONLY_KEYS = [
|
|
103
|
+
'agentPushNotifEnabled',
|
|
104
|
+
'agents',
|
|
105
|
+
'apiKeyHelperTimeoutMs',
|
|
106
|
+
'awsLoginRefresh',
|
|
107
|
+
'awsRegion',
|
|
108
|
+
'awsRetryMode',
|
|
109
|
+
'disableNonEssentialModelCalls',
|
|
110
|
+
'enabledExperimentalFeatures',
|
|
111
|
+
'inputNeededNotifEnabled',
|
|
112
|
+
'installMethod',
|
|
113
|
+
'pluginGroups',
|
|
114
|
+
'pluginRepositoryEnabled',
|
|
115
|
+
'pluginsLocalConfig',
|
|
116
|
+
'proxy',
|
|
117
|
+
'skipAutoPermissionPrompt',
|
|
118
|
+
'statsig',
|
|
119
|
+
'subagents',
|
|
120
|
+
'theme',
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
/** Union of schema-documented and app-only settings keys; unknown top-level keys trigger a doctor WARN. */
|
|
124
|
+
export const KNOWN_SETTINGS_KEYS = new Set<string>([...SCHEMA_KEYS, ...APP_ONLY_KEYS]);
|