FAQ
I edited ~/.claude/settings.json and my change vanished after a pull. Why?
Section titled “I edited ~/.claude/settings.json and my change vanished after a pull. Why?”Because that file is generated, not synced. Every nomad pull rebuilds it by merging
shared/settings.base.json with hosts/<NOMAD_HOST>.json from your sync repo, so anything you
hand-edit into ~/.claude/settings.json on a synced host is overwritten by the next pull.
Make the edit in the sync repo instead:
- Want it on every host? Edit
shared/settings.base.json. - Want it on this host only? Edit
hosts/<NOMAD_HOST>.json.
Then nomad push from the host you edited on (or just commit and push the repo) and nomad pull
everywhere else. Truly host-local settings that should never sync can also live in
~/.claude/settings.local.json, which claude-nomad never touches.
How do I set a per-host setting, like a different model or an env var?
Section titled “How do I set a per-host setting, like a different model or an env var?”Put it in hosts/<NOMAD_HOST>.json in your sync repo. On every pull, that file is deep-merged on
top of shared/settings.base.json, with these rules:
- Scalars override:
"model": "opus"in the host file beats the base value. - Objects merge recursively: you can override one key inside
envwithout redeclaring the rest. - Arrays replace wholesale: a host-file array swaps out the base array, it does not append.
nullis a valid override: use it to explicitly blank out a base value on one host.
Why isn’t my session showing up on the other host?
Section titled “Why isn’t my session showing up on the other host?”Almost always: the project is not in path-map.json. Only projects listed there sync; everything
else is left alone in both directions, and push/pull fold those into a single info row like
ℹ︎ 4 not in path-map. Run nomad doctor to list the unmapped projects by name, then add the
project to path-map.json with its absolute path on each host (see
How it works for the format), push, and pull on the other host.
nomad doctor lists “unmapped local projects” I don’t recognize. Are they broken?
Section titled “nomad doctor lists “unmapped local projects” I don’t recognize. Are they broken?”No, they are normal. In Claude Code, a “project” is just any directory you have launched
claude from. The first time you run a session in a folder, Claude Code creates a matching
directory under ~/.claude/projects/ and stores that session’s transcripts (the .jsonl files)
there. So the unmapped list is simply every working directory you have ever started a session in
that is not listed in your path-map.json, often throwaway ones like your home folder, a repo’s
subdirectory, or a parent directory you happened to cd into once.
They are real (each holds actual session transcripts), not corruption and not a sync error. claude-nomad deliberately leaves unmapped projects alone in both directions: they are never pushed and never pulled, so they only ever exist on the machine that created them.
You do not have to do anything. Two options if you want to tidy up:
- Want a project’s sessions to follow you across hosts? Add it to
path-map.json(see Why isn’t my session showing up on the other host?). - It is throwaway? Delete its folder under
~/.claude/projects/. That removes the local transcripts for that directory only; nothing synced is touched, and your mapped projects are unaffected.
What never leaves my machine?
Section titled “What never leaves my machine?”Credentials and ephemeral state are excluded by a hard-coded block list: OAuth tokens and MCP
state (.claude.json, .credentials.json), your prompt history (history.jsonl), per-host
overrides (settings.local.json), and runtime dirs like todos/, shell-snapshots/, caches,
and telemetry. The authoritative list is the NEVER_SYNC set in src/config.ts, and the
sensitive subset stays blocked even inside an opted-in extras directory. On top of that, only an
explicit allow-list of paths can be pushed at all, and everything that is pushed gets scanned by
gitleaks first. See Security for the full trust model.
Push says gitleaks found a secret. Now what?
Section titled “Push says gitleaks found a secret. Now what?”Three exits, depending on what the finding is:
- Real secret in a transcript you want to keep:
nomad redact <session-id>(or answerRedactin the interactive menu) rewrites the secret in place, locally, then push again. - False positive:
nomad push --allow <rule>ornomad allow <fingerprint>records it in.gitleaksignoreso it stops blocking. - Session you do not need to sync:
nomad drop-session <id>unstages it from the sync repo while leaving the local file intact forclaude --resume.
The full decision tree, including non-interactive CI paths, is in Recovery flows.
What is the difference between nomad diff and nomad pull –dry-run?
Section titled “What is the difference between nomad diff and nomad pull –dry-run?”Both preview what a pull would change, but nomad diff is offline and lockless: it does not
take the sync lock and does not contact the remote, so it shows what a pull would do against the
repo state you already have checked out. nomad pull --dry-run does the network round-trip
first, so it shows what the next real pull would actually apply. Use diff for a quick local
look, --dry-run for the authoritative preview.
One thing neither preview writes: your skills/ directory. Skills are copy-synced only on a
real (non-dry-run) pull, so --dry-run never touches ~/.claude/skills. The preview still
reports every other planned change (symlink moves, settings.json diff, transcript overwrites).
Backups are piling up in ~/.cache/claude-nomad. Is that a problem?
Section titled “Backups are piling up in ~/.cache/claude-nomad. Is that a problem?”Every pull and push snapshots what it is about to overwrite into
~/.cache/claude-nomad/backup/<timestamp>/, and nothing deletes those automatically. nomad doctor warns once the pile passes 20 directories or 200 MB. Prune with:
$ nomad clean --backups # delete backups older than 14 days$ nomad clean --backups --keep 5 # or: keep only the 5 newest$ nomad clean --backups --dry-run # preview either mode firstEvery pull fails with “Pulling is not possible because you have unmerged files”
Section titled “Every pull fails with “Pulling is not possible because you have unmerged files””error: Pulling is not possible because you have unmerged files.fatal: Exiting because of an unresolved conflict.✗ git pull --rebase failedThere are three distinct states that produce this error. Check which one you are in before running any recovery command.
State 1: stuck mid-rebase or mid-merge
Section titled “State 1: stuck mid-rebase or mid-merge”A previous pull’s rebase (or merge) hit a conflict and was never resolved. The sync repo has an
in-progress operation that git is waiting on: .git/rebase-merge/, .git/rebase-apply/, or
.git/MERGE_HEAD is present inside ~/claude-nomad/.
You can confirm this with:
$ ls ~/claude-nomad/.git/rebase-merge 2>/dev/null && echo "mid-rebase" || \ ls ~/claude-nomad/.git/MERGE_HEAD 2>/dev/null && echo "mid-merge"Automated recovery (recommended): nomad pull --force-remote automates the sequence below.
It aborts the in-progress rebase or merge, safety-diffs stranded commits and dirty tracked
changes against origin/main, parks stranded commits on a nomad/stranded-<ts> branch, resets
hard to origin/main, and re-pulls. If any stranded or dirty tracked changes touch synced config
(shared/, hosts/, path-map.json), it refuses and lists the at-risk paths so nothing config-related
is silently discarded. The parking branch stays in the repo as a recoverable ref.
Manual fallback (use if --force-remote refuses due to synced-config changes):
$ cd ~/claude-nomad$ git rebase --abort # or: git merge --abort, if it was a merge
# Safety check before discarding local state: what would be thrown away?$ git log --oneline origin/main..HEAD # stranded local commits, if any$ git diff origin/main --stat # uncommitted divergence
# If nothing above touches config you care about (shared/, hosts/, path-map.json):$ git reset --hard origin/main$ nomad pullIf the safety check shows local-only commits that DO touch synced config, cherry-pick or copy
those changes out before the reset --hard; they exist nowhere else. When in doubt, the repo is
plain git, so anything discarded is still in git reflog until git prunes it.
State 2: unmerged index with no active operation
Section titled “State 2: unmerged index with no active operation”The rebase or merge was torn down (the marker files are gone) but the git index still holds
unmerged entries from the conflict (stage-2 and stage-3 versions of the same file). There is
nothing to abort. Running git rebase --abort will say “No rebase in progress” and do nothing.
This is the sibling state to State 1 and is just as stuck.
You can confirm this with:
$ cd ~/claude-nomad$ git diff --diff-filter=U --name-only # non-empty = unmerged index entries presentIf that lists files and the State 1 marker check above is empty, you are in State 2.
An orphaned autostash may also be present. The pull that conflicted saved your working-tree
changes to the git stash before rebasing (via --autostash). When the rebase was interrupted
without completing, that stash entry was never automatically restored. Check:
$ git stash list # look for a line containing "autostash"Automated recovery (recommended): nomad pull --force-remote handles this state too. It
clears the stuck index via git reset --mixed HEAD (preserving your working-tree edits), reports
any orphaned autostash entry with a hint so you can decide what to do with it, then re-pulls.
Unlike State 1, there is nothing to abort and no stranded commits to park, so recovery is simpler.
Clearing the index does not remove conflict markers that were already written into your files. If
any of the conflicted files still carry <<<<<<< / ======= / >>>>>>> after the reset,
--force-remote stops there rather than continuing: a pull at that point would copy the markers
into your live ~/.claude/ config. The index is already repaired when it stops, so the repo is no
longer wedged. Clean up the files it names (keep the content you want), then re-run nomad pull.
Local edits to files that were not part of the conflict do not trigger this stop.
Manual runbook:
$ cd ~/claude-nomad$ git reset --mixed HEAD # clear the stuck index; your working-tree edits are preserved
# Review working-tree files for leftover conflict markers (<<<<<<<, =======, >>>>>>>):$ git diff --name-only # files with unstaged changes likely still carry markers
# If git stash list shows an autostash entry, decide what to do with it:$ git stash pop # restore the autostashed changes (may re-conflict; review first)$ git stash drop # discard the autostash if you do not need those changes
$ nomad pullUse git reset --mixed HEAD here, not git reset --hard. The --mixed form clears only the
index, leaving your working-tree files as-is, so any work in progress is not discarded. The
--hard form would throw away working-tree edits too.
State 3: conflicted autostash pop
Section titled “State 3: conflicted autostash pop”git pull --rebase --autostash can exit 0 (success) even though it failed to fully apply
your changes. This happens when the autostash step at the very end of the pull, the one that
restores the local edits --autostash set aside before rebasing, hits a conflict while
reapplying them. The pull reports success, but the index is left unmerged, stash@{0}: autostash
is retained, and conflict markers are written into the affected file. HEAD is still on your
branch, so git rebase --abort fails with “no rebase in progress”: there is nothing to abort.
nomad now catches this itself. Both nomad pull and nomad push re-check for this exact state
right after the pull step and stop with exit code 4 before applying or pushing anything that
carries conflict markers, so you will see nomad’s own message rather than a silent success.
This looks identical to State 2 under the unmerged-index check above (git diff --diff-filter=U --name-only is non-empty in both), but the two need different recoveries and should not be
conflated. State 2’s git reset --mixed HEAD is wrong here: it clears the index but leaves the
conflict markers behind as ordinary unstaged modifications, which is the state this guard exists
to prevent.
The reliable signal is nomad’s own message. The guard only fires immediately after a pull
step, so State 3 is what you are in when nomad pull or nomad push just stopped with exit
code 4 and said the autostash pop conflicted. If you are looking at a wedged repo without having
just seen that message, treat it as State 2 and use the non-destructive --mixed recovery above.
If you are still unsure, inspect the stash content before discarding anything (the git show
step below is read-only and does not re-trigger the conflict). In State 3 nothing is lost as
long as the stash entry still exists: the pre-conflict content of every stashed file is
retained there until you drop it. An autostash can hold more than one file, so list its contents
and restore all of them before dropping it.
Manual runbook:
$ cd ~/claude-nomad$ git stash list # confirm stash@{0}: autostash is present$ git stash show --name-only stash@{0} # list EVERY file the stash holds$ git show 'stash@{0}:<path>' # view the pre-conflict content; safe, does not re-pop
$ git reset --hard HEAD # discard the marked-up working tree$ git checkout 'stash@{0}' -- <path> # restore one file; repeat for every path listed above$ git stash drop # ONLY after every stashed path is restored
$ nomad pull # or: nomad pushgit reset --hard is safe to use here specifically because the pre-conflict content is retained
in the stash entry from the step above; it is the exact opposite of the State 2 recovery, where
the --hard form is called out as unsafe because there is no such retained copy. That safety
ends the moment you run git stash drop, which is why the drop is the last step.
A hook that worked before nomad now fails with “Cannot find module”
Section titled “A hook that worked before nomad now fails with “Cannot find module””SessionStart:startup hook errorError: Cannot find module '../some-tool/lib/helper.cjs'Require stack:- /home/you/claude-nomad/shared/my-tool/check-update.jsThe giveaway is the require stack: the failing script shows up under your sync repo
(~/claude-nomad/shared/...) instead of ~/.claude/....
Here is what happens. On a synced host, a directory added via sharedDirs (see
Shared support dirs) is a symlink
into the sync repo. When Node runs a script from it, it resolves symlinks first, so the script
“believes” it lives in ~/claude-nomad/shared/my-tool/. If the tool loads another file by a path
relative to its own location (say require('../tool-runtime/helper.cjs'), expecting to find
~/.claude/tool-runtime/ next door), the lookup happens inside the sync repo, where that directory
does not exist. The hook crashes with MODULE_NOT_FOUND.
For Node hooks the fix is one flag, --preserve-symlinks-main, which tells Node to keep the
symlinked path so relative lookups resolve back under ~/.claude/:
"command": "node --preserve-symlinks-main \"$HOME/.claude/my-tool/check-update.js\""Make that edit in shared/settings.base.json in your sync repo, not in ~/.claude/settings.json
(see the first question for why), then nomad sync as usual.
Any tool that stores hook scripts in a sharedDirs-symlinked directory and references other
~/.claude/ paths relative to its own file location can hit this, often right after the tool
updates itself. You do not have to spot it yourself: nomad doctor warns about hook commands with
this shape and prints the same fix hint. Native Windows hosts cannot hit this at all: shared
directories are real copies there (no symlinks), so Node never resolves a hook script into the
sync repo.
Is nomad update different from npm update -g claude-nomad?
Section titled “Is nomad update different from npm update -g claude-nomad?”No. nomad update runs the npm self-update for you; it is a convenience wrapper, nothing more.
Use whichever you prefer.
How do I stop using nomad without breaking my setup?
Section titled “How do I stop using nomad without breaking my setup?”Run nomad eject. It replaces every managed ~/.claude/ symlink with a real copy of its target, so
your config keeps working after the sync repo is gone. It only touches nomad-managed symlinks: real
files and directories are left alone, and it aborts safely (with a nomad pull hint) if it finds a
dangling symlink rather than guessing. On native Windows the managed names are already real copies
(the win32 copy-sync modality), so eject has nothing to materialize and goes straight to the
checklist. After it runs it prints a short manual-remainder checklist:
uninstall the CLI, drop the NOMAD_HOST / NOMAD_REPO env vars, and optionally delete
the sync repo. Preview first with nomad eject --dry-run. See the
offboard a machine recipe for the full
sequence.
I have local changes to push and remote changes to pull. What order do I run them in?
Section titled “I have local changes to push and remote changes to pull. What order do I run them in?”Run nomad sync and let it handle the order for you.
$ nomad sync # pull first (keeps your local work), then push everything back upUnder the hood, sync runs the pull half first and the push half second, under one lock. Pulling
first is safe because a pull keeps rather than deletes your work: unpushed session transcripts are
retained, and a project file synced as an extra (like .planning/) that changed on both sides is
kept local with a warning (anything else a pull overwrites is backed up first). The push half then
reconciles everything you have, including whatever the pull half just kept, back to the sync repo.
If the pull half fails, sync stops before pushing; if the push half fails after a successful
pull, it says so (pull: applied, push: failed) and nothing you had is lost. See the
command reference for the full behavior.
If you drive the lower-level commands yourself instead, push first, then pull:
$ nomad diff # optional: preview what a pull would apply, without locking anything$ nomad push # your local changes win and land in the sync repo$ nomad pull # apply the merged repo state back to ~/.claude/Why this manual order works:
-
nomad pushalready does the pull’s git half for you. Before touching anything, push rebases your sync repo on the remote, so commits from other hosts are integrated first. Then your local state (sessions, extras, hooks) is copied over the repo tree and pushed. When the same file changed on both sides, your local copy wins (last-write-wins is the designed model). Conflicts that git cannot rebase cleanly stop the push at the rebase step so you can resolve them by hand. -
nomad pullthen applies the merged result locally: it regeneratessettings.jsonfrom the base and host files, refreshes the symlinks, and copies down anything other hosts pushed that you did not have yet.
Pulling first is no longer lossy, but it leaves more to clean up. A pull keeps your diverging local files instead of overwriting them (your local edit wins on conflict, and unpushed session transcripts are retained), so nothing is thrown away. What you get instead is a divergence warning that keeps firing until you push to reconcile:
local folder .planning/ in repo claude-nomad differs from the synced copy in 3 files; the next pull step will keep your local copy (push to reconcile; your current files are backed up to ~/.cache/claude-nomad/backup/<timestamp>/extras/<encoded-project-path>/)Pushing first skips that back-and-forth: your local changes land in the repo, the divergence
clears, and the next pull is a clean fast-forward. Every pull still snapshots what it touches into
~/.cache/claude-nomad/backup/<timestamp>/ as a safety net, so you can always recover an older copy
if you want one. Inside the backup, extras live under extras/<encoded-project-path>/ (the
project’s absolute path with slashes turned into dashes):
$ ls -t ~/.cache/claude-nomad/backup/ | head -1 # newest backup, named <timestamp>$ cp ~/.cache/claude-nomad/backup/<timestamp>/extras/-home-you-code-myproject/.planning/ROADMAP.md \ ~/code/myproject/.planning/ROADMAP.mdOne nuance: because push is last-write-wins, if the same file genuinely changed on two hosts,
the host that pushes last clobbers the other’s version in the sync repo (the older copy survives
only in git history). If you suspect a real both-sides edit on something you care about, run
nomad diff first and reconcile by hand before pushing.
gsd-owned directories: hooks/agents not synced
Section titled “gsd-owned directories: hooks/agents not synced”hooks/ and agents/ under ~/.claude/ are owned and installed by @opengsd/gsd-core (the GSD
tool) per host via npm i -g @opengsd/gsd-core. Because every host runs npm install
independently, each host always gets a self-consistent set of hook scripts for its own gsd version.
Syncing these directories was pure churn: two hosts on different gsd versions would overwrite each
other’s versioned scripts on every push/pull cycle.
As of this version, nomad drops hooks/ and agents/ from the sync set entirely. If you upgrade
from an older version that did sync them, the repo trees at shared/hooks/ and shared/agents/
stay in place as inert history. You do not need to delete them from the repo. Nomad will not touch
them on push or pull.
Skills are handled differently: your own skills (any ~/.claude/skills/ entry that does not
start with gsd-) still sync, but as a filtered copy rather than a symlink. The gsd-* skills are
excluded from both push and pull; gsd reinstalls them per host via npm.
I upgraded nomad and now nomad doctor shows a migration hint for hooks or agents
Section titled “I upgraded nomad and now nomad doctor shows a migration hint for hooks or agents”After upgrading to a version that drops hooks and agents from the sync set, any host that
previously synced them will still have a ~/.claude/hooks symlink pointing at the old
shared/hooks/ tree. That symlink is harmless: gsd’s per-host install creates the real directory
it needs regardless of what is at that path.
nomad doctor detects the leftover symlink and emits a migration hint:
⚠︎ ~/.claude/hooks is a symlink left over from the pre-upgrade sync era gsd (@opengsd/gsd-core) now owns this directory and installs it per host via npm. Remove the symlink and let gsd reinstall a real directory on its next run: rm ~/.claude/hooksTo resolve it, run the command shown in the hint:
$ rm ~/.claude/hooksGsd reinstalls a real ~/.claude/hooks/ directory the next time it runs (on session start, or
when you run any gsd command). The same applies to ~/.claude/agents. After removing both, run
nomad doctor again to confirm the hints are gone.
The hint is informational only (a WARN, not a FAIL) and does not affect pull or push.
nomad doctor says settings.base.json will self-clean gsd hook entries on my next push
Section titled “nomad doctor says settings.base.json will self-clean gsd hook entries on my next push”This is separate from the leftover-symlink hint above. GSD registers its hook commands as entries
inside ~/.claude/settings.json and re-applies them on every session start, so those entries are
managed per host, not synced. Nomad filters gsd-owned hook entries (whose script basename starts
with gsd-) out of the generated settings on pull, which is why the old recurring “hooks diverged”
drift warning is gone.
If your committed shared/settings.base.json was written by an older nomad that still carried those
entries, nomad doctor shows a one-time info line (not a warning, and it never changes the exit
code) reading:
gsd now owns hook entries per-host; shared/settings.base.json self-cleans on your next 'nomad push'You do not need to do anything special: the next nomad push rewrites the base to drop the gsd
entries (backed up first, idempotent, never on pull or --dry-run), and the note disappears once
the base is clean. A hook you wrote yourself (basename not starting with gsd-) is untouched and
still syncs via nomad capture-settings.
Can I pin a gsd version to keep hook scripts byte-identical across hosts?
Section titled “Can I pin a gsd version to keep hook scripts byte-identical across hosts?”Yes, though it is optional and fragile as a primary strategy.
If all your hosts run the exact same @opengsd/gsd-core version, the gsd-* files gsd installs
are byte-identical on every machine. This means even if a sync path accidentally carried them, the
result would be a no-op. Pinning one version is useful as an extra layer of defense while you
transition, or in a team setting where you want to coordinate gsd upgrades.
To pin:
$ npm i -g @opengsd/gsd-core@<version> # run on every hostWhy this is not the primary fix: the moment you update gsd on one host, that host’s gsd-*
files diverge from the others. The defense only holds for as long as every host stays on the same
version, which is hard to guarantee over time. The structural fix (dropping hooks/ and agents/
from the sync set) is the reliable solution; version pinning is an optional complement, not a
replacement.