Quickstart
Requirements
Section titled “Requirements”- Node.js 22.22.1 or newer (24 LTS recommended; the npm
enginesfield declares the 22.22.1 floor and surfaces a warning on older runtimes; npm only blocks the install whenengine-strict=trueis configured) - Git
gitleaks(required fornomad push, which exits with an error if it is not on PATH;nomad doctoralso checks it against the pinned 8.30.x and warns when it is absent or mismatched)gh(GitHub CLI), required bynomad initto create and wire the private sync repo. Whenghis missing or unauthenticated,nomad initexits with a FATAL and shows install /gh auth loginguidance. On hosts where the private repo is already set up (all subsequent hosts),ghis only needed bynomad doctor’s Actions-drift check and auto-disable; pull and push work without it.
Optional:
- curl or wget, the HTTP fetcher behind
the version-staleness check (
nomad doctorlatest-release line) andnomad doctor --check-schema. curl is tried first and wget is the fallback, so either one works. The checks soft-skip (no error, no exit-code change) when neither is present, so the rest of the CLI works without it;nomad doctorshows a single “HTTP fetcher” row that names the binary in use (for exampleHTTP fetcher: curl 8.5.0) when either is installed, and warns only when both are absent.
First host (once, ever)
Section titled “First host (once, ever)”# 1. Install the CLI.$ npm i -g claude-nomad
# 2. Create your private sync repo and scaffold it. nomad init uses gh to# create the repo, wire origin, and disable Actions, then scaffolds locally.$ nomad init # prompts for a repo name (default: claude-nomad-config)$ nomad init --repo my-config # non-interactive: use this name, no prompt
# 3. Add a stable host label to ~/.zshrc or ~/.bashrc, then reload.export NOMAD_HOST=<your-host-label>
# 4. Publish the scaffold to your private repo.$ nomad pushThen the everyday loop on any host:
$ nomad doctor # confirm setup$ nomad sync # pull config, then publish local changes, in one stepnomad sync always pulls first and then pushes, so there is no ordering to remember. The
lower-level nomad pull and nomad push remain available when you need their extra flags; see
the command reference.
Each additional host
Section titled “Each additional host”$ npm i -g claude-nomad$ gh repo clone <your-username>/<your-repo-name> ~/claude-nomad # default: claude-nomad-configexport NOMAD_HOST=<your-host-label> # add to ~/.zshrc or ~/.bashrc$ nomad pullWindows
Section titled “Windows”claude-nomad runs natively on Windows (PowerShell or cmd), and WSL2 works too. The everyday loop is the same either way.
Two things come from native Windows specifically, both in the list below: a .gitleaksignore allow
entry may not travel to a macOS, Linux, or WSL2 host, and deep session paths can hit the native
Windows 260-character path limit.
The native Windows steps are the same as First host and Each additional host above, with a couple of PowerShell-specific swaps:
# 1. Install the CLI.> npm i -g claude-nomad
# 2. Create your private sync repo and scaffold it.> nomad init
# 3. Add a stable host label. PowerShell has no ~/.bashrc equivalent, so set it# as a persistent user environment variable instead, then restart your# terminal so the new value is picked up.> [System.Environment]::SetEnvironmentVariable('NOMAD_HOST', '<your-host-label>', 'User')# Using cmd instead of PowerShell? The equivalent one-liner is:# setx NOMAD_HOST <your-host-label>
# 4. Publish the scaffold to your private repo.> nomad pushA few native Windows specifics worth knowing. WSL2 behaves like Linux, so the copy-sync and
path-length items below do not apply to it; the .gitleaksignore one can still reach it, from the
other side:
- Installing gh:
winget install GitHub.cli(orscoop install gh), thengh auth login. Needed beforenomad initon the first host; later hosts only clone with it. - Installing gitleaks:
winget install gitleaks.gitleaks(orscoop install gitleaksif you use Scoop).nomad doctorprints the same hint whenever gitleaks is missing from PATH. - Shared config is copied, not symlinked. On macOS and Linux, files like
CLAUDE.mdand your skills live in the sync repo and are symlinked into~/.claude/, so there is one source of truth on disk. Creating a symlink on native Windows needs Developer Mode or admin rights, so there these are real copies instead, whether or not you have Developer Mode enabled. WSL2 is unaffected and behaves like Linux. What this means for you: nothing extra. On native Windows bothnomad pullandnomad syncmirror your local copies into the repo before they fetch, so an unpublished edit is captured rather than reverted, and a realnomad pullnow prints aSymlinksline for each name it captured, so the copy is visible instead of silent. If one of those names cannot be read at all, because another program is holding it open or its permissions changed, the mirror leaves it out and warns, naming the file and the reason, instead of a silent gap in what was captured.nomad pushprints the same warning. Thenomad diffand--dry-runpreviews print a read-only version of it, saying nothing was captured for the file and nothing was written, because they were never going to write to the repo; either way they report what they could not read rather than dropping it from the plan. A shared name is also skipped when its copy in the sync repo is there but points at content that is not, usually because the machine that shared it no longer has the original, and when that copy cannot be read at all. Pull, push, and the previews all warn and name the entry, and none of them repairs it for you: remove a copy that points nowhere from the sync repo, or restore what it points at, by hand; for one that could not be read, check its permissions in the sync repo instead. A later step in the same pull reads that file too, and it no longer stops there: it warns as well, naming the file, saying whether the file is gone or may be part-updated, and pointing at the backup copy when it made one, then skips that one name and finishes the rest of the pull. Close whatever is holding the file, or fix its permissions, and runnomad pullagain to pick it up. A file you delete from a shared directory is handled the same way: it is removed from the sync repo by the next pull, exactly as deleting inside a symlinked directory already removes it on macOS or Linux, and that pull names the removal in the sameSymlinkssection, on its own row right after any files it captured. The removal is left uncommitted, so it publishes on your next push and passes the same secret scan as everything else, and the file is snapshotted to the backup dir first. Nomad only removes a file it has a record of having given this machine, so a repo file this machine has never synced is never touched. That record is also why the first pull after you upgrade to this version is an exception: there is nothing to compare against yet, so a deletion made before that pull comes back once, and deleting it again sticks. If a file you created has the same name as one another machine just created, the pull stops before it overlays your copy: the only thing it removes is the temporary copy it had just made inside the sync repo, and it tells you which file under~/.claude/to move or rename, with the two ways to finish. Your file stays exactly as you left it and the update waits for the next pull.nomad pull --force-remoterecovers a wedged sync repo. When the repo is stuck mid-rebase or mid-merge, recovery parks any local commits on a branch of their own, resets the repo to match the shared repo, and on native Windows that reset also replaces your shared config with the repo’s copy; the pull warns naming how many shared names it restored from the repo copy, with your previous copies snapshotted to the backup dir first. One guard sits in front of that reset, and it covers the sync repo rather than this machine: when the repo’s own tracked copy of the shared config differs from the shared repo, the pull refuses before it resets anything and lists those repo paths instead. That refusal comes before the commit parking described above, so at that point nothing has been saved for you: put your own commits on a branch, and copy any uncommitted work out, before you follow the manual steps in the FAQ, because those end in a hard reset. The refusal clears only once the repo matches the shared repo again; moving or committing the files does not clear it on its own. Before you retry, note two things. Whichever operation was stuck, the rebase or the merge, has already been unwound by the time it refuses, so running the same command again is now just an ordinary pull, and the list can also name paths where the shared repo is simply ahead of yours, which are nothing of yours at risk. The guard cannot help with an edit that exists only on this machine, which is what the warning above is for. A different stuck state, an unfinished index with nothing to abort, recovers by clearing the index without touching your working files, so on native Windows your shared config is left exactly as it was, and the usual pre-pull copy into the sync repo still runs, so an edit you have not pushed yet is still captured. If a file from that old conflict still carries conflict markers, the pull stops there instead, so the markers never reach your live config. Copying instead of symlinking is the same approach claude-nomad’sskills/sync already uses on every platform. - The copy-in never carries your Claude secrets or session history. The same mirror that
captures your Windows edits into the sync repo refuses to copy your Claude login and credential
files, your per-host settings, or your local history and stats cache, by name and by filename
shape (a
.env, a private key, a.netrc), whether that part is a directory along the way or the file name itself; seesrc/core/config.never-sync.tsfor the exact set. Session transcripts under~/.claude/projects/are a separate mechanism entirely (path-remapped, not mirrored by this gate) and are never carried by it either way. If something on the credential list somehow lands in the sync repo working tree anyway, such as a file edited directly in the repo rather than through~/.claude/, what happens next depends on whether Git already tracks it. If it does not, the nextnomad pulldeletes it, snapshotting it to the backup dir first, and prints a warning naming the file. The warning names the backup location only when there was something to copy: a symlink whose target is already gone is deleted without one, because there is no content to save. If it does, pull leaves the file untouched and prints a warning naming the file and the exact command to run to finish clearing it yourself. An ordinary folder of your own inside one of your shared names (sessions,tasks,plans,cache, and the like) mirrors along with everything else; it is not on this list, even though Claude Code itself uses those same names under~/.claude/for its own runtime state. - A
.gitleaksignoreallow entry may not travel across hosts. gitleaks fingerprints each finding using the file path exactly as it saw it: backslashes on native Windows, forward slashes on macOS/Linux/WSL2. If you allow a finding withnomad push --allow(ornomad allow) on native Windows, the identical finding can reappear as “new” the first time it is scanned from a macOS, Linux, or WSL2 host, and the same happens in reverse. This is a known gitleaks limitation, not a claude-nomad bug; just allow it again from the other host. - Deep session paths and the native Windows path-length limit.
nomad doctorchecksgit config core.longpathsand the WindowsLongPathsEnabledregistry value, and warns if either is off. Turning both on avoids problems with the classic 260-character path limit, which a deeply nested project’s encoded session path can otherwise exceed. - Line endings stay put. A fresh
nomad initwrites a.gitattributeswith* -text, so Git never converts line endings between hosts. If you are joining a sync repo created before this file existed, add that one line from any host (or watch for thenomad doctorwarning that nudges you), otherwise a native Windows checkout with the commoncore.autocrlf=trueGit default would rewrite every text file’s line endings, and every host would then see the whole tree as permanently changed.
Privacy by default
Section titled “Privacy by default”Your private sync repo must stay private. Session transcripts contain the full text of your
conversations. nomad init disables Actions on the new repo as soon as it is created, via the
GitHub API call gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=false. What
this means for you: the repo nomad init creates ships no workflows of its own, so this is a
precaution, not a fix for a known problem. It guarantees that no CI (which could echo transcript
content into build logs) can ever run against your private data repo, even if a workflow file is
added later; you do not need to remember to do it.
Pass --keep-actions to skip the disable step (for example, when your org already enforces an
Actions policy).
Per-project Claude config: in-repo vs sidecar
Section titled “Per-project Claude config: in-repo vs sidecar”Some projects keep their Claude config (a .claude/ directory with hooks/, agents/,
commands/, a project settings.json) committed to the project’s own git repo. Others keep it
as a local sidecar, listed in .gitignore so it is never committed. nomad’s per-project extras
are for the sidecar case only.
- Committed to the project repo: do nothing in nomad. Cloning the repo on another machine
already brings
.claude/, and the repo is the source of truth. Adding it to nomad’sextraswould create a second, competing copy, and because a pull mirrors the synced copy over your working tree (last write wins) it can overwrite or revert the committed version. - Git-ignored sidecar: add
.claudeto that project’s entry in theextrasfield ofpath-map.jsonso nomad carries it across your machines. On push nomad strips host-local and ephemeral state (session transcripts,settings.local.json, caches), syncing only config.
The same rule applies to .planning/ and a project-level CLAUDE.md: sync them through extras only
when the project git-ignores them. See How it works for the exact
fields and the filtering boundary.
Setup: first host in detail
Section titled “Setup: first host in detail”nomad init creates the private repo via gh, wires it as origin, disables Actions, scaffolds
the directory layout. You then run nomad push to publish. The gh CLI must be installed and
authenticated before you run it.
# Install the CLI.$ npm i -g claude-nomad
# Create the private sync repo and scaffold it. You will be prompted for a# repo name (default: claude-nomad-config). Pass --repo to skip the prompt.$ nomad init# or non-interactively:$ nomad init --repo my-config
# If ~/.claude/ is already populated on this host, capture it as the starting# point instead of an empty scaffold. Stages shared/ and writes# hosts/<NOMAD_HOST>.json from your current ~/.claude/settings.json.# Does NOT touch the originals.$ nomad init --snapshotnomad init refuses to clobber existing scaffold artifacts, so re-running on a populated repo is
a safe no-op (it errors out naming the offender). nomad pull against an unscaffolded repo fails
fast with FATAL: repo not initialized; run 'nomad init' to scaffold instead of silently leaving
a half-state.
Add a stable host label to your shell rc, then reload it:
export NOMAD_HOST=<your-host-label> # add to ~/.zshrc or ~/.bashrcNOMAD_HOST overrides os.hostname(), which returns noisy values like WINDOWS-I5NT6OH on WSL
or <name>.local on macOS. Pick a clean label per machine (e.g., wsl-laptop, macbook,
homelab-nuc). nomad doctor reports the resolved host so you can confirm.
Edit path-map.json to add your logical projects (see How it works), then:
$ nomad doctor # read-only state check; reports host, repo state, and any check # needing action (compact by default; -v shows all passing checks), # each marked checkmark (pass) / cross (fail) / warning (warn)$ nomad doctor --check-shared # read-only gitleaks preflight over the session transcripts a push # would stage$ nomad diff # preview what nomad pull would change on this host; no lock, # no network, no mutation$ nomad push # send current state to the private remote$ nomad pull # apply on another host (or this one after a remote update)nomad pull --dry-run is the network-aware twin of nomad diff: it acquires the lock and runs
git pull so you see what the next real pull would do given the latest remote, then exits without
mutating.
If the destination host already has populated ~/.claude/{CLAUDE.md, agents/, ...}, the first
nomad pull will refuse to overwrite real files. See Usage for the safe migration
flow.
Setup: each additional host in detail
Section titled “Setup: each additional host in detail”# Install the CLI.$ npm i -g claude-nomad
# Clone your private data repo (<your-repo-name> defaults to claude-nomad-config).$ gh repo clone <your-username>/<your-repo-name> ~/claude-nomad# or with plain git:$ git clone git@github.com:<your-username>/<your-repo-name>.git ~/claude-nomad
# Add to ~/.zshrc or ~/.bashrc, then reload.export NOMAD_HOST=<your-host-label>
$ nomad pull # apply config to ~/.claude/npm i -g claude-nomad puts a nomad binary on your PATH. What this means for you: there is no
compile step, no extra transpiler to install, and nothing is fetched from the network the first
time you run nomad, so the first run works offline. (The Node version floor and the
engine-strict caveat are in the Requirements section above.)