Skip to content
nomadv0.69.0

Commands

Every command is invoked as nomad <command>. Each section below names the command, shows its full invocation, and lists any flags in its own table.

nomad init [--repo <name>] [--snapshot] [--keep-actions]

Create a private GitHub repo via gh, wire it as origin, disable Actions, and scaffold shared/, hosts/, path-map.json, and a root .gitattributes (* -text) that stops Git from rewriting line endings between hosts (what would otherwise happen on a native Windows checkout with the common core.autocrlf=true default; nomad doctor warns when an older repo lacks the guard). Does not commit or push; run nomad push afterward to publish. Prompts for a repo name (default: claude-nomad-config). gh must be installed and authenticated; exits with FATAL otherwise. Refuses to clobber existing scaffold. Without --snapshot, an interactive init that finds an existing ~/.claude/ (a settings.json or any non-empty shared source) offers to seed the repo from it; declining keeps the empty scaffold, and a non-interactive shell skips the prompt and prints a --snapshot tip. See Quickstart for privacy details.

Flag Description
--repo <name> Non-interactive: use <name> as the private repo name without prompting. Useful in scripts.
--snapshot Overlay current host’s ~/.claude/ into shared/ and write ~/.claude/settings.json verbatim into hosts/<NOMAD_HOST>.json. Originals not modified. Same auto-disable behavior. An interactive init offers this automatically when it detects existing config.
--keep-actions Skip the Actions-disable step. Combinable with --snapshot and --repo. Use when an org policy already governs Actions, or you intentionally want CI on the private repo.

nomad pull [--dry-run] [--force-remote]

git pull --rebase --autostash, apply symlinks (real copies on native Windows), regenerate settings.json, remap session paths, and pull opted-in per-project extras. Errors out if scaffold missing. Non-destructive: unpushed local-only session transcripts are retained, and a repo-tracked extras file you have edited locally is kept (not overwritten) when it diverges from the incoming copy, with a warning to push and reconcile.

On native Windows, where shared config is a real copy rather than a symlink, pull first mirrors those copies into the repo, before the rebase, and reports what it took in a leading Symlinks section (one captured <local> -> <repo> row per name), so the copy is visible rather than silent. Without that step the rebase-then-overlay sequence would overwrite an edit you had not published yet. That mirror skips your Claude login and credential files, your per-host settings, your local history and stats cache, and any file that looks like a credential by name (a .env, a private key, a .netrc); see src/core/config.never-sync.ts for the exact lists. An ordinary directory of your own inside a shared name is carried, not skipped. A name whose shared/<name> counterpart is in the repo but leads nowhere, or cannot be read at all, is left alone too, and the pull warns naming it, so a local edit does not quietly stop being captured; the nomad diff and --dry-run previews say the same in read-only wording. A counterpart that leads nowhere asks you to remove the entry from the sync repo or restore what it points at, by hand; one that could not be read asks you to check its permissions there instead, because nothing established where it leads. If a skipped path is already sitting in the sync repo working tree, pull removes it when git does not track it (snapshotting it to the backup dir first, unless it is a symlink whose target is already gone and there is no content to save) and otherwise leaves it exactly as it found it, warning with the file name and the git command that clears it. Neither case fails the pull. The same pre-rebase step also removes a file you deleted from a shared directory from the repo, the same as deleting inside a symlinked directory already removes it on macOS or Linux, and the pull names that removal too, as a removed <repo> (gone from <local>) row in the same Symlinks section, right after any captured rows; the removal is left uncommitted (it publishes on your next push, through the same secret scan as everything else) and the repo copy is snapshotted to the backup dir first, gated on a per-host record of what this machine last had, so a repo file this machine has never synced is never touched. That record is also why the first pull after a host upgrades to this version is an exception: there is nothing to compare against yet, so a deletion made before that pull is restored once and has to be repeated. On macOS and Linux the symlink already makes a local edit (and a local deletion) an uncommitted change in the sync repo, so the step is a no-op there and both platforms behave the same way. It is also skipped under --dry-run, which writes nothing to ~/.claude/ or to your shared config (though it still runs the git pull --rebase that refreshes the sync repo, so the preview reflects the remote), and under --force-remote, but only when that flag actually recovers a repo stuck mid-rebase or mid-merge: that recovery resets the sync repo to origin/main, and re-staging local copies right after the reset would immediately undo it. On a repo that is not wedged, --force-remote no longer skips the mirror, so an unpublished edit is captured exactly as on a plain pull, and the command prints an info line reporting there was nothing to recover before continuing. When the mirror is skipped because recovery genuinely ran, the pull also prints a warning naming how many shared names it restored from the repo copy, and where their previous host copies were saved. If the mirror or removal step itself fails (an antivirus lock, or a path over the native Windows length limit), the pull warns and carries on instead of aborting, so you can still fetch; your unpublished edit or deletion stays pending on the host. A file you had just created inside a shared directory is the exception: the repo-to-local overlay later in the same pull removes it from ~/.claude/, after snapshotting it to the backup dir, so recover it from there and pull again. Separately, if a file you created inside a shared directory has the same name as one the incoming update adds, the pull stops before applying anything and, underneath git’s own untracked-file error, prints the file under ~/.claude/ to move or rename plus the two ways to finish; nothing is lost either way, since your file is untouched, the update simply has not landed yet, and the copy nomad had made in the sync repo is cleaned up for you.

Flag Description
--dry-run Network-aware preview: acquire lock + git pull --rebase, print planned changes (symlink moves, settings.json diff, transcript overwrites, an Extras section listing every <logical>/<dirname> a wet pull would copy including extras with no local copy yet, a count of retained local-only sessions, and any extras-divergence warning). On native Windows the same tree also shows every shared-config capture the pre-rebase mirror would perform and every removal the same step would make in the repo, so the preview matches the wet run in both directions. Writes nothing to ~/.claude/, but the git pull --rebase above updates the sync repo (~/claude-nomad/) first so the preview reflects the remote.
--force-remote Recover from a wedged sync repo. Two recovery paths depending on state: (1) stuck mid-rebase or mid-merge: abort the in-progress operation, park stranded commits on nomad/stranded-<ts>, reset to origin/main, and re-pull; refuses if stranded or dirty tracked changes touch synced config (shared/, hosts/, path-map.json). (2) unmerged index with no active rebase or merge: clear the stuck index via git reset --mixed HEAD (preserves working-tree edits), surface any orphaned autostash entry with a hint, and re-pull; no abort, no park step. On a repo that is not wedged, prints an info line reporting there is nothing to recover and continues as a normal pull (exit status success); when the check for a stuck index cannot run at all (git missing, or the index lock still held when the check times out), it reports that it could not determine whether the repo is wedged instead of claiming the repo is clean, and continues the same way. On native Windows, when recovery genuinely runs via path (1), the pre-pull shared-config mirror is skipped because the reset to origin/main would otherwise be undone immediately after; the pull warns, naming how many shared names were restored from the repo copy and the backup directory holding their previous host copies. Cannot combine with --dry-run (it performs mutations incompatible with preview mode).

nomad diff

Offline, lockless twin of pull --dry-run. No network, no lock. Works against the current local repo state. The settings.json diff filters gsd-owned hook entries from both sides before comparing, so GSD’s per-session hook self-heal does not show up as a phantom hooks change; the preview reflects what a real pull would write. Also renders the same Extras section as pull --dry-run, listing every <logical>/<dirname> a pull would copy, including extras with no local copy yet. On native Windows it shows the same shared-config capture and removal rows pull --dry-run shows, reading the host-local record those rows are gated on without ever writing to it, so nomad diff stays fully read-only.

nomad push [--dry-run] [--full-scan] [--redact-all] [--allow <rule>] [--allow-all]

Export local sessions and opted-in per-project extras to logical names, commit (chore: sync from <NOMAD_HOST>), push. Steady-state pushes scan only the transcripts that changed since the last successful push (incremental); a cold start, a gitleaks version change, a gitleaks config change, or --full-scan forces a full rescan of all transcripts.

On native Windows, before any of that, push also carries your local edits back into the sync repo for names the repo already shares, the same as nomad pull does before it fetches. A directory the repo does not carry yet stays on this machine until you run nomad adopt <name>, the same as on macOS, Linux, and WSL2: publishing a directory to every other host is something you ask for, not a side effect of the next push. A name you have already shared is unaffected and keeps publishing your edits on every push.

If a name you have already shared ends up with a broken pointer in the sync repo instead, for example because a machine that shared it no longer has the original, push skips writing through it exactly as it did before, but now also prints a warning naming the entry so your local edit does not silently stop reaching the repo. Push does not repair the broken pointer for you: remove it from the sync repo, or restore what it pointed at, by hand. An entry push could not read at all is skipped and named the same way, but the warning points at its permissions in the sync repo rather than at a broken pointer, because nothing checked where that entry leads. Neither warning fails the push.

Flag Description
--dry-run Run pre-push safety checks (gitleaks probe, rebase, remap preview, gitlink scan, allow-list) and a read-only gitleaks leak preview over a throwaway temp copy of the sessions, extras, and non-gsd user skills this host would stage. Exits with code 5 if a leak is found, or if the staged tree cannot be scanned cleanly (the scan ran but produced no parseable report); both fail closed with the same leak-blocked code a real push uses, so a scripted $? == 5 pre-flight buckets them the same way. Only a scan that could not run at all (gitleaks or git missing) exits 1. Writes nothing to ~/.claude/ and commits/pushes nothing, but the rebase above updates the sync repo (~/claude-nomad/) first.
--full-scan Ignore the per-host push manifest and rescan all transcripts, then rewrite the manifest on success. Use after a gitleaks upgrade, after editing a gitleaks config file, or when in doubt. Composes freely with --dry-run and all resolution modes.
--redact-all Redact all findings non-interactively (backup written first) without a TTY. Does not auto-Allow findings. After redaction re-stages and re-scans; aborts with the session-aware FATAL if any finding survives. Mutually exclusive with --allow*; cannot combine with --dry-run. See Recovery flows.
--allow <rule> Append the fingerprint of every finding whose gitleaks rule id matches <rule> to .gitleaksignore, re-stage, and re-scan. Proceeds only when no finding survives. Never skips scanning. No TTY required. Mutually exclusive with --redact-all and --allow-all; cannot combine with --dry-run. See Recovery flows.
--allow-all Append the fingerprint of every current finding to .gitleaksignore, re-stage, and re-scan. Proceeds only when no finding survives. Never skips scanning. No TTY required. Mutually exclusive with --redact-all and --allow; cannot combine with --dry-run. See Recovery flows.

nomad sync [--dry-run] [--verbose|--all|-v]

The recommended everyday command: pulls first, then pushes, under a single lock, so you never have to reason about which one to run first. The pull half is the same retain-merge overlay nomad pull runs (local-only work is kept, a diverged extras file is kept local with a warning), so it is always safe to run first; the push half then reconciles everything local, including any local-only sessions and diverged extras files the pull half just retained, to the remote.

On native Windows the pull half also mirrors your shared-config copies into the repo before it fetches, and removes a file you deleted from a shared directory from the repo the same way (see pull below), so pulling first cannot overwrite an edit, or resurrect a deletion, you have not published yet. Under --dry-run that mirror and removal pass is skipped along with every other write.

Output is compact by default, matching nomad doctor: a run prints its sync on host=<HOST> header, then a single Sync summary composed from the run’s outcome, not the full status tree. Pass --verbose (or --all / -v) to also print the full merged status tree (on native Windows a leading Symlinks section naming what the pre-fetch mirror captured and what it removed, then Settings, Global config, Sessions, Extras, and Leak scan, as applicable) before the summary, the same tree every nomad sync run used to print unconditionally.

A pull-half failure (for example a wedged repo) stops the run immediately; no push is attempted. Run nomad pull --force-remote to recover, then re-run nomad sync (sync itself has no --force-remote flag; that recovery stays on the low-level pull command). A push-half failure after a successful pull reports pull: applied, push: failed (<reason>) and exits non-zero; there is no rollback, since the pull half already retained everything and made local state strictly better than before. A run where neither half changed anything prints a single compact already in sync line. If the sync repo holds commits that never reached the remote (for example a push interrupted mid-run), the run does not claim to be in sync; the Sync summary adds a sync repo has unpushed commits note instead. A run where the pull half retained diverged extras or local-only sessions and the push half then reconciled them still exits 0, with the push row’s own parenthetical naming how many items were reconciled (this is treated as resolved work, not a standing problem). If nomad push’s secret scan finds something mid-sync, the same interactive Redact/Allow/Drop/Skip menu you would see from a plain nomad push opens; recovery behaves identically either way.

--dry-run previews both halves: the pull preview renders first, then a one-line note that the push preview below is computed against pre-pull state (a real sync runs the push half after the pull half has already applied, so its staging set can differ slightly), then the push preview. Both previews are the same ones nomad pull --dry-run and nomad push --dry-run render, so the pull preview also surfaces the wedged-repo check and the diverged-extras warnings.

What this means for you: a dry run never touches ~/.claude/ and never commits or pushes anything, but it is not a completely offline, zero-effect command. Like nomad pull --dry-run and nomad push --dry-run, it contacts the remote and brings your sync repo (~/claude-nomad/) up to date with it first. That is deliberate: a preview computed before fetching would describe the state you are about to leave rather than the changes a real sync would apply.

nomad push and nomad pull remain available as lower-level commands for cases sync does not cover, such as --force-remote wedge recovery or the non-interactive leak-resolution flags (--redact-all, --allow, --allow-all, --full-scan).

Flag Description
--dry-run Stack the pull preview then the push preview; acquires the lock. Writes nothing to ~/.claude/ and commits/pushes nothing, but both halves fetch and rebase the sync repo first so each preview reflects the remote (same contract as pull --dry-run and push --dry-run).
--verbose, --all, -v Print the full merged status tree before the Sync summary; default output is the summary alone.

nomad drop-session <id>

Surgically unstage every shared/projects/*/<id>.jsonl and the sibling shared/projects/*/<id>/ subagent directory from the staged tree of ~/claude-nomad/. Idempotent; the local ~/.claude/projects/<encoded>/<id>.jsonl and <id>/ tree are preserved. See Recovery flows.

nomad adopt <name> [--dry-run]

Back up, then move a pre-existing ~/.claude/<name> directory into shared/<name>, recreate the symlink so this host keeps working, and stage the result for push. <name> must already be listed in SHARED_LINKS or in the sharedDirs field of path-map.json; adopt is a mover, not a config editor, so it never writes path-map.json itself. A credential-shaped <name> is refused outright, before the membership check and before --dry-run takes effect: .env, id_rsa, credentials, *.pem, *.key and similar shapes stop the command with an error and exit code 1, since adopting one would move a secret into the sync repo. On native Windows adopt recreates the name as a real copy instead of a symlink (the win32 copy-sync modality). There, a name whose shared/<name> counterpart already exists and resolves to something real is reported as already adopted and skipped (with a nomad pull hint to refresh the local copy), where macOS, Linux, and WSL2 would refuse with a would-clobber error. If that counterpart is there but does not lead to anything usable, either because it points at content another host shared that never reached this one, or because it could not be read at all, adopt now stops with an error naming the entry and exits 1, rather than the older behavior of reporting it already adopted and pointing you at a nomad pull that could not have fixed that state either. If the entry does not resolve, remove it from the sync repo or restore what it points at, then run nomad adopt <name> again. If it could not be read, check its permissions in the sync repo before you rerun the command. On macOS, Linux, and WSL2, the would-clobber refusal now says which of those three it found, so it never claims there is content in the way when all it saw was a pointer leading nowhere, or a path it could not read. The same state is also called out, on every platform, when your local ~/.claude/<name> is already a symlink into the sync repo: adopt used to report a plain already-adopted success there too, and now prints a warning saying the link is broken, or, when the sync repo entry could not be read at all, that it could not tell whether the link works. A symlink with no shared/<name> in the repo behind it at all is called out the same way, since a link the sync repo has no counterpart for is not adopted either: remove ~/.claude/<name> and run adopt again, or run nomad pull if another machine already shares the name. It still exits 0 in all three cases, since it is reporting rather than writing and nothing is silently lost. The warning goes to standard error with the same warning marker nomad push and nomad doctor use for that state, so all three surfaces are greppable the same way. If that copy cannot be written, because another program has the path open or its permissions block it, adopt stops with an error naming the path and exits 1. The content itself is not lost: it is already in shared/<name>, and staged unless the same error also reports that staging failed. Run nomad pull to recreate the local copy before your next nomad push, because a push copies the local name back over shared/<name> first, so publishing while the local copy is missing is what would undo the adopt. If the error says a partial copy is still at the path, hold off on nomad sync too, since it pushes in the same run: pull on its own first, and check it does not warn about that name again, because a pull that still cannot read the path warns and carries on rather than stopping.

Three earlier steps can fail the same way. The first is the snapshot adopt takes before it moves anything, which fails when the backup cache cannot be written. Adopt stops there and exits 1: nothing has been removed from ~/.claude/ and nothing has been written to the sync repo. A partial snapshot may be left behind in the backup cache, which is harmless, and nomad clean --backups prunes it.

If the copy INTO shared/<name> fails, nothing has been removed from ~/.claude/: adopt clears whatever partial copy reached the repo and asks you to run the command again once the path is readable. Should it report a partial shared/<name> it could not clear, remove that one yourself before re-running. On macOS, Linux, and WSL2 adopt would otherwise turn the re-run away with the would-clobber error. On native Windows removing it matters more, because there a name whose shared/<name> exists is reported as already adopted, so the re-run would claim success over a half-copied fragment, and the nomad pull that reply suggests would copy that fragment over the local directory this failure left whole.

If the copy succeeds but the original cannot be removed, the answer depends on the platform. On native Windows a real local copy sitting beside a populated shared/<name> is exactly what an adopted name looks like, so adopt warns, refreshes the local copy from the repo, and finishes normally. On macOS, Linux, and WSL2 the same leftover is a real directory where the symlink belongs, so adopt stops with an error and exits 1, having staged shared/<name> anyway: run nomad pull, which backs that directory up and replaces it with the symlink.

Before touching anything, adopt checks the whole ~/.claude/<name> tree against two separate lists, both in src/core/config.never-sync.ts. The first is a list of exact names, ALWAYS_NEVER_SYNC, and it is narrower than you might expect: it holds only the credential and per-host settings files (.claude.json, .credentials.json, settings.local.json, history.jsonl, stats-cache.json). Your own folders named plans, tasks, cache, sessions or todos inside the directory you are adopting are carried into the sync repo like anything else, because you asked for that directory to be shared. The second is a list of credential filename shapes, SECRET_FILE_PATTERNS, which catches .env and .env.local, id_rsa, credentials, .netrc, .npmrc, and anything ending in .pem, .key, .p12 or .pfx. Neither list looks inside a file, so a directory of your own that happens to be spelled exactly like a credential name is refused too: that is a name collision, not a secret it found. Both kinds are exactly what the sync repo refuses to publish, so moving them into shared/<name> would only defer the failure to your next nomad push.

The two lists answer two different questions, and adopt uses both. Is <name> itself, the directory you are pointing adopt at, safe to share at all? That is checked against the full set in src/core/config.never-sync.ts, so nomad adopt sessions or nomad adopt cache is still refused as a NAME, unchanged by any of this. Is the CONTENT inside a directory you have already chosen to share safe to carry? That is the narrower check above, and it is what changed: a sessions/ or plans/ folder inside your own my-tools/ now adopts along with everything else.

If the content check finds anything, adopt stops before the backup and before anything is copied or moved, so nothing on your machine or in the repo has changed; the error lists every offending path relative to ~/.claude/<name>/, says which of the two lists caught it, and exits 1. --dry-run answers exactly the same way, with the same exit code, rather than previewing a move it would refuse. To clear it, move those paths out of ~/.claude/<name>/ and run nomad adopt <name> again. Renaming works too for a name collision, since the spelling is the whole of the match and the error quotes the name to rename away from. It does not work for a credential filename shape, where the extension or the whole filename is what matched, so a new name in the same shape is refused identically.

Flag Description
--dry-run Preview the planned backup, move, and git add without touching the filesystem or the git index.

nomad eject [--dry-run]

Replace every managed ~/.claude/ symlink with a real dereferenced copy so your setup keeps working after you delete the ~/claude-nomad/ checkout and uninstall the CLI. The set of managed names is the union of SHARED_LINKS and validated sharedDirs entries that nomad pull manages (the authoritative list is allSharedLinks in src/core/config.ts), widened with anything an older version of nomad already linked under a looser rule. That widening is deliberate: eject materializes what this host already has, so a name sync now refuses is still dereferenced into a real copy rather than left as a symlink into a checkout you are about to delete. Names that are already real files or directories are reported as skipped and left unchanged; absent names are also skipped. A managed name that is a symlink pointing outside the sync repo’s shared/ directory is skipped as not nomad-managed and left untouched, so eject only materializes links it owns. A dangling symlink (the target is missing) causes the whole command to abort before any copy is written, with a hint to run nomad pull first to restore the missing target. After all copies succeed, eject prints a checklist of the manual steps remaining: uninstall the CLI, remove NOMAD_HOST and NOMAD_REPO from your shell rc, and optionally delete the local sync checkout and backup cache. eject never writes to the sync repo, never invokes git, and never touches ~/.claude/projects/ (session transcripts are already real files). On native Windows there is usually nothing to materialize: under the win32 copy-sync modality the managed names are already real copies, so each is reported as already a real copy (win32 copy-sync) and only the manual checklist remains.

Flag Description
--dry-run List what would be materialized without touching the filesystem.

nomad capture-settings [--host] [--dry-run] [--yes]

Promote local-only ~/.claude/settings.json keys into the shared repo so they survive the next nomad pull. Use this when an external tool (such as Claude Code or GSD) added new keys to your live settings file that are not yet in shared/settings.base.json or your host override. After writing the destination file, capture-settings calls regenerateSettings so the local settings.json immediately matches the updated repo state. Idempotent: when no local-only keys remain the command exits cleanly with a message and writes nothing.

Because the default destination (shared/settings.base.json) syncs to every host, the command shows the destination and the keys it will promote and asks for confirmation before writing. Pass --yes to skip the prompt; in a non-interactive shell the prompt cannot be answered, so the command refuses to write unless --yes is given.

Credential- and secret-bearing keys (apiKeyHelper, awsAuthRefresh, awsCredentialExport, otelHeadersHelper, and env) are never promoted, so a secret placed in live settings cannot ride into the shared repo.

Flag Description
--host Write into hosts/<NOMAD_HOST>.json instead of shared/settings.base.json. Use for host-specific values (absolute paths, machine-local model preferences).
--dry-run Show the destination file and keys that would be written without changing anything.
--yes, -y Skip the confirmation prompt. Required when running without an interactive terminal.

nomad redact <session-id> [--rule <id>] [--dry-run]

Rewrite the secret span across a session’s local source transcripts (the main transcript plus any subagent transcripts under <session-id>/), backed up to ~/.cache/claude-nomad/backup/. Refuses to touch a session that was modified recently (potential active session). Safe to re-run. See Recovery flows.

Flag Description
--rule <id> Limit redaction to findings of one gitleaks rule id only.
--dry-run Show what nomad redact would change without writing anything.

nomad allow <fingerprint>...

Append one or more gitleaks fingerprints to <REPO_HOME>/.gitleaksignore without going through a push cycle. Use this to pre-record confirmed false positives so the next nomad push does not prompt for them. Fingerprints come from a previous nomad push finding report or a nomad doctor --check-shared scan; the format is file:rule:line (the opaque string gitleaks emits, shown in the scan output).

Idempotent: a fingerprint already present in .gitleaksignore is silently skipped. All inputs are validated up front: a single invalid fingerprint (empty, containing a newline, or over 512 characters) aborts the whole command with exit 1 and writes nothing. No flags are accepted.

See Recovery flows for the non-interactive push allow paths (nomad push --allow <rule> and nomad push --allow-all), which record fingerprints AND re-scan in a single step.

nomad clean --backups [--older-than <dur>] [--keep <N>] [--dry-run]

Delete old backup snapshots under ~/.cache/claude-nomad/backup/. The --backups flag is required. By default (no retention flag) removes snapshots older than 14 days. A snapshot that holds nothing is removed in every mode, whatever its age and whatever --keep says, since there is no content in it to protect. Always preview with --dry-run first. See Recovery flows.

Flag Description
--backups Required: confirm backup pruning is the intended target.
--older-than <dur> Delete snapshots older than this age (e.g. 7d, 24h). Default when omitted: 14 days.
--keep <N> Keep the N newest snapshots that hold something and delete the rest. Cannot be combined with --older-than.
--dry-run List the snapshots that would be removed without deleting.

nomad update

Update the nomad CLI binary from npm (npm update -g claude-nomad). Does NOT pull your sync data; run nomad pull separately for that. Finishes with a link to the release notes for the version you ended up on. See Usage.

nomad doctor [--resume-cmd <id>] [--check-shared] [--check-schema] [--check-remote] [--verbose|--all|-v]

Read-only health check. Each line carries a status glyph ( pass, fail, ⚠︎ warn); any sets process.exitCode = 1 (⚠︎ does not). Output ends with a Summary section that repeats every warning and failure and closes with a one-line verdict (✓ healthy, or warning/failure counts), so the last line always answers “am I healthy?”. By default the report is compact: only the version line, the Environment repo-state line, any section carrying a warning or failure (passing rows removed), and the Summary are shown. Add --verbose (alias --all, -v) to print the full per-check tree, including everything that passed. The exit code is identical in both modes. Includes a release-version staleness check (an info line says when the latest version could not be determined, so a skipped check is not mistaken for “current”), a Hook targets check that fails (, exit 1) when settings.json references a hook command whose script under ~/.claude/ is missing on this host, a wedged-repo check that fails (, exit 1) in two cases: the sync repo is stuck mid-rebase or mid-merge from a previous failed pull, OR the git index has unmerged entries with no active rebase or merge (the sibling state where the operation was torn down but the index was left stuck); both FAIL lines carry a nomad pull --force-remote recovery hint. A separate ⚠︎ warn fires when an orphaned autostash entry is found in git stash list (a stash entry left by a --autostash rebase that was interrupted before completion); the warn is non-blocking and points at the git stash pop or git stash drop runbook. Other ⚠︎-only checks: gitleaks version drift; on a private GitHub repo, re-enabled Actions; optional-dependency presence (gh and the curl-or-wget HTTP fetcher); a backups-cache size/count nudge toward nomad clean --backups; an ESM/CommonJS hook-scope mismatch; a Node-engine floor check; a hook command that runs a Node script under a synced (symlinked) directory without --preserve-symlinks-main; a denylisted path git already tracks under shared/ (a git ls-files walk filtered through the same never-sync check the push gate and the pull-side backstop use, naming the path, the segment that matched, and the git rm --cached that stops tracking it while leaving the file on disk; a committed path is invisible to that backstop, which reads git status and so reports changes rather than contents); and, when NOMAD_HOST is unset on a repo that already configures other hosts, a hostname-derived host key that matches neither a hosts/<NOMAD_HOST>.json override nor a path-map entry (the silent-misalignment nudge: per-host settings and session sync key off this label, so set NOMAD_HOST to the label this host should use when the warning fires; a single-host or fresh repo stays silent). The Environment section prints an informational sync-modality row (symlink (posix) or copy-sync). On native Windows that row also names when a local edit reaches the repo (the next pull or push, since the host-side and repo-side files are distinct there) and is kept in the default compact view; the posix row stays verbose-only. On native Windows, the per-name shared-link row (the same one covering CLAUDE.md, commands/, rules/, and any sharedDirs entries) also byte-compares the real copy against its shared/ counterpart and warns (⚠︎, exit code untouched) with the diverging files listed when it has drifted, instead of reporting it healthy on presence alone; a matching copy still reads . Paths the mirror will never sync (now the narrower credential and per-host-settings floor) are thrown out of that comparison rather than reported as drift, since no command could reconcile them; when any were excluded, the passing row carries a dim (N never-synced path(s) not compared) note under --verbose. On native Windows, a real local copy the sync repo does not carry (never published, since nomad push no longer creates a repo counterpart on its own) gets its own info row naming nomad adopt <name>; it never fails the check and, like every other informational Links row, it is stripped from the default compact view and shown under --verbose. On native Windows, when a real local copy sits beside a shared/<name> counterpart that leads nowhere, doctor now warns and names the broken repo pointer, instead of the older behavior of reporting the name as never published and pointing you at nomad adopt <name>, a command that now refuses that exact state. On every platform, when your local entry is missing entirely, or your local symlink into the sync repo is itself broken, and the repo’s own shared/<name> pointer also leads nowhere, doctor now warns that shared/<name> does not resolve and that there is nothing to restore from either side. This replaces two older lines that no longer fit that state: one saying the name was simply never shared, and one saying the dangling local symlink was stale and safe to remove, neither of which named the real problem, that the repo’s own copy is unusable too. When the repo’s entry cannot be read at all, rather than pointing nowhere, doctor now says exactly that and points you at its permissions, where it used to report the name as never shared, on a quiet informational line the default view hides. On macOS, Linux, and WSL2, when a real file or directory sits at ~/.claude/<name> where a link into the sync repo belongs, the failing row only tells you to run nomad adopt <name> when that command could actually help. When the sync repo entry is unusable it names the entry to clear up first, since adopt would refuse. When the sync repo entry already holds content, adopt refuses for a different and deliberate reason, that it will not choose between two copies of the name that have drifted apart on different machines, so the row says so and names both ways out: compare the two, then either remove the local copy and run nomad pull, or remove the repo entry and run nomad adopt <name>. Either direction discards one copy, which is why nothing does it for you. Native Windows reports the same situation through its own copy-model rows, which list the files that differ. All of these are failing rows, so they do set the exit code; the warning rows above leave it untouched, so a script that only checks the exit code should still read those lines. A CRLF-guard check on every platform warns when the sync repo has no .gitattributes * -text line (the wording names whether core.autocrlf is actively converting, explicitly false on this host, or unset). On native Windows two further warn-only rows check long-path support (git config core.longpaths and the OS LongPathsEnabled registry value), since deep encoded session paths under ~/.claude/projects/ can exceed the legacy 260-character MAX_PATH; the gitleaks-missing install hint also switches to winget/scoop there. The Path map section lists both the projects mapped for this host and any local project directories with no path-map entry (what nomad push counts as “unmapped”; they are left alone in both directions).

Flag Description
--resume-cmd <id> Print a host-local cd ... && claude --resume <id> line for a session. See Usage.
--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. Skips with a ⚠︎ when gitleaks is not on PATH. Also runs two separate, WARN-only advisories (⚠︎, exit code untouched) over content already committed to the sync repo: memory/*.md files and shared/skills/** files, both pointing at the push-recovery Redact step; a latent memory or skill secret never fails this check. See Recovery flows.
--check-schema Read-only: fetches the live Claude Code settings schema and lists any ~/.claude/settings.json key absent from it. Non-fatal and offline-tolerant: skips with a ⚠︎ when neither curl nor wget is available or the schema is unreachable.
--check-remote Read-only: verifies origin/main has shared/ and a valid path-map.json. Reads the locally-cached remote-tracking ref (no network required when the ref is already cached); skips with a ⚠︎ when the ref is unavailable or git is not on PATH. Non-fatal in all cases.
--verbose, --all, -v Print the full per-check tree, including passing checks. Without it, doctor shows only checks that need action plus the Summary verdict. --check-shared / --check-schema / --check-remote sections always render in full when their flag is set, in either mode.

The version-check emits a warning when the local install is behind the latest upstream release, and a pass line when current. It silently skips on network failures.

The Hook targets check reads the live ~/.claude/settings.json hooks block and fails (, exit 1) when a hook command points at a script under ~/.claude/ that is missing on this host (the freshly-configured-host symptom that motivated syncing hooks/). It deliberately skips any command it cannot resolve to a ~/.claude/ path (bare binaries like jq, unresolved env vars), so it never false-fails on a command that does not reference a local script.

The preserve-symlinks check (⚠︎-only) catches a hook that would crash on every session start: when a hook command runs a Node script that lives under one of the directories claude-nomad symlinks into ~/.claude/, Node resolves the script into the sync repo, and any require('../...') of a ~/.claude/ neighbor breaks with MODULE_NOT_FOUND (see the FAQ for a real-world walkthrough). The warning line names the fix: add --preserve-symlinks-main to the hook command in shared/settings.base.json. It is deliberately conservative: only clear node <script-under-symlinked-dir> shapes are flagged, and a bounded, never-executed peek at the script’s first 64 KB suppresses the warning when the script’s relative requires all resolve (or it has none), so self-contained hooks stay silent.

Two further warning-only drift checks run in nomad doctor. The gitleaks version-drift line 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 as a pass), and is silent when gitleaks is not on PATH. The Actions-drift line (carrying a gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false remediation hint) fires when origin is a private GitHub repo that is gh-authed with Actions re-enabled, complementing the auto-disable that runs on nomad init (see Quickstart); it is silent on every prerequisite miss (non-GitHub origin, gh unauthed, public repo, or Actions already off).

The settings merge-drift check (⚠︎-only, never exit 1) runs in the Settings section of nomad doctor immediately after the host-overrides row. It recomputes the same deepMerge(shared/settings.base.json, hosts/<NOMAD_HOST>.json) that nomad pull would write, then deep-compares the result against ~/.claude/settings.json. A ⚠︎ warning fires when merged keys are missing from the live file, the signature of an external writer (for example a Claude Code onboarding flow) silently clobbering settings.json and dropping managed keys; the fix is nomad pull. A second warning fires when a key is present on both sides but its value diverged: this is genuinely ambiguous (the repo or your local file could be the newer one), so the hint points at nomad diff to inspect, and notes that nomad pull would overwrite local with the repo while editing the base/host file keeps the local value. The comparison normalizes node launcher paths first, so a hook that differs only by a bare node versus an absolute /.../bin/node (host-specific churn an installer writes) does not register as drift. A separate info line lists local-only keys absent from the merge as promotion candidates for shared/settings.base.json or hosts/<NOMAD_HOST>.json, since those are typically transient state written between pulls (for example notification toggles), not an error; when this host has no hosts/<NOMAD_HOST>.json at all, that info line is withheld because the host-overrides row above it already flags the same keys as a failure. A ⚠︎ warning also fires when hosts/<NOMAD_HOST>.json exists but does not parse, since nomad pull would stop on that file. The check reports key names only and never leaks values. It skips with a ℹ︎ when settings.json is absent or when shared/settings.base.json is absent or unparseable; a malformed settings.json is skipped silently, since doctor’s settings load already fails (, exit 1) on the same file.

Also in the Settings section, a one-time info line (never a warning, never exit 1) appears while the committed shared/settings.base.json still holds gsd-owned hook entries (commands whose script basename starts with gsd-). GSD manages those entries per host, so nomad filters them out of the generated settings.json on pull and rewrites the committed base to drop them on the next real nomad push (backed up first, idempotent, never on pull or --dry-run). The note resolves itself once the base is clean. See GSD-aware sync for the full picture.

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.

Every nomad subcommand exits with one of a small set of codes, so a script or cron wrapper can branch on $? without parsing stderr text.

Code Name Meaning
0 Success Completed successfully.
1 Generic failure Unclassified failure; the default for any error not covered below.
2 Usage Bad argv: an unknown subcommand, an unknown flag, or a malformed flag value.
4 Conflict The sync repo is wedged (e.g. an unresolved rebase) and needs manual git resolution.
5 Leak blocked gitleaks confirmed a secret in the staged tree and the push was aborted.
130 Interrupted You pressed Ctrl+C at an interactive prompt, so nomad stopped without finishing.

A run skipped because another nomad process already holds the lock also exits 0: this is an intentional no-op skip, not a failure, so a backgrounded shell-rc or cron invocation never raises a false alarm from a concurrent run. Value 3 is reserved for future use.

When nomad hits an unexpected bug it prints a short “this looks like a bug” banner (with a link to the issue tracker) instead of a raw stack trace, and writes a bounded, redacted report to ~/.cache/claude-nomad/crash/. The exit code contract is unchanged: an unexpected crash exits 1, a documented failure keeps its own code. A prompt you cancel yourself is not a crash: it exits 130 and writes no report.

  • Local only, never uploaded. The report is written owner-readable-only under your cache dir and nothing is transmitted anywhere; you choose whether to attach it to an issue.
  • Two-layer redaction. A structural scrub (home directory to ~, hostname to a placeholder) always runs, followed by a best-effort gitleaks secret scan, the same redaction nomad uses for session transcripts.
  • Fail-safe without gitleaks. If gitleaks is absent, the structural scrub still applies and the report is still written, with a note that the secret scan did not run, so review it before sharing publicly.
  • Bounded contents. Only the nomad version, the command you ran (bounded, including any flag values), the error name and message, a trimmed stack, the platform, the Node.js version, and a timestamp. No environment dump, no file contents.
  • Self-pruning. The crash directory keeps only the most recent reports and prunes older ones automatically. There is intentionally no nomad clean flag for it.