How to Sync Claude Code Config Across Multiple Macs with Syncthing
I use a MacBook Pro and a Mac Mini side by side. Both run Claude Code, but custom skills, agents, and memory files created on one machine don't exist on the other. Manually copying them every time is not an option.
I wanted to automatically sync the ~/.claude folder between the two machines while also tracking changes with Git.
What's inside ~/.claude
All Claude Code configuration lives in ~/.claude/.
| Path | Description | Sync needed |
|---|---|---|
CLAUDE.md | Global instructions | Yes |
settings.json | Settings (hooks, MCP, permissions) | Yes |
memory/ | Global memory | Yes |
skills/ | Custom skills | Yes |
agents/ | Custom agents | Yes |
commands/ | Custom slash commands | Yes |
projects/*/memory/ | Per-project memory | Yes |
*.jsonl | Session logs (~1.6GB) | No |
cache/, debug/, etc. | Cache, temp files | No |
settings.local.json | Machine-specific settings | No |
.credentials.json | Auth credentials | No |
In my case, the entire folder was about 2GB, but only ~165MB actually needed syncing. The rest was session logs and cache.
Why not sync sessions
Session logs (*.jsonl) and session data (UUID folders) are excluded from sync. The volume keeps growing, and I start and end dozens of sessions a day. There is no need to sync all of that. My workflow is already split: short-lived sessions run on the MacBook, long-running sessions run on the Mac Mini. And I certainly did not want my conversation history pushed to GitHub.
Comparing sync options
Dropbox
Cloud-based sync. Automatic and stable. But you'd need to move ~/.claude into the Dropbox folder and create a symlink. This breaks Git version control since the symlink gets committed instead of the actual files. Free plan is limited to 3 devices. Idles at 200-400MB RAM.
rsync
One-directional copy tool. You can automate it with cron, but bidirectional sync and conflict resolution need to be built from scratch. Essentially manual.
Syncthing
P2P real-time bidirectional sync. Open source (70k+ GitHub stars). No limits on devices, storage, or bandwidth. Idles at 30-50MB RAM. Syncs ~/.claude in place, so you can use Git in the same directory. No symlinks needed.
Why Syncthing: free, lightweight, syncs the original folder without symlinks, making it fully compatible with Git version control.
Setup
1. Install and start
On both machines:
brew install syncthing
brew services start syncthing
Access the WebUI at http://localhost:8384.
2. Connect the two machines
- On Machine A's WebUI, go to Actions > Show ID and copy the Device ID
- On Machine B's WebUI, click Add Remote Device and paste the ID
- Accept the connection request on Machine A
Adding from one side establishes a bidirectional connection.
3. Configure the shared folder
On Machine A, click Add Folder:
- Folder Label:
claude-config - Folder Path:
~/.claude - Folder ID:
claude-config
Save, then Edit > Sharing tab > check the other machine. Accept the share request on Machine B.
4. Set up .stignore
~/.claude/.stignore:
// Session logs
(?d)*.jsonl
// Session data (UUID folders)
(?d)projects/*/????????-????-????-????-????????????
(?d)projects/*/????????-????-????-????-????????????.jsonl
// Projects that don't need memory/skill sync
projects/-Users-sh
// Cache and temp (no trailing slash: ignores the folder entry itself too)
(?d)cache
(?d)image-cache
(?d)debug
(?d)file-history
(?d)paste-cache
(?d)session-env
(?d)shell-snapshots
(?d)sessions
(?d)telemetry
(?d)tasks
(?d)downloads
(?d)ide
(?d)backups
(?d)plans
// Machine-specific
settings.local.json
.credentials.json
// System
.DS_Store
// Git (root-anchored /: avoids clashing with the skills/*/.git submodule rule; no (?d) to protect the local repo)
/.git
(?d)skills/*/.git
// Build artifacts
(?d)**/node_modules
I occasionally run Claude Code from my home directory (~), which creates a project folder under projects/ based on the home path. For example, if your username is sh, it creates projects/-Users-sh. I use this for quick, throwaway sessions, so there's no need to sync memory or skills for it. I excluded the entire project path.
Note: Syncthing's.stignoredoes not support character ranges like[0-9a-f]. Use?wildcards for UUID patterns instead.
5. Add Git version control
Initialize Git in the same ~/.claude folder. The .gitignore is nearly identical to .stignore, with the addition of Syncthing metadata (.stfolder/, .stversions/).
cd ~/.claude
git init
gh repo create .claude --private --source=. --push
Skills that contain external git repos should be managed as submodules.
git submodule add https://github.com/garrytan/gstack.git skills/gstack
Gotchas
Never sync .credentials.json
I initially forgot to exclude .credentials.json. One machine's auth credentials overwrote the other's, and I got logged out. This file must be unique per machine. Exclude it from both .stignore and .gitignore.
.stignore glob syntax differs from .gitignore
.gitignore supports character ranges like [0-9a-f], but .stignore throws a parse error on them. Use ????????-????-????-????-???????????? for UUID folder patterns.
Skills may contain git repos
Some community skills are installed via git clone. Running git add on these triggers an "embedded git repo" warning. Register them as submodules, or exclude them in .gitignore.
Result
When I install a new skill or add a memory file on one machine, it shows up on the other within seconds. Git tracks the change history and backs everything up to GitHub.
Maintenance is minimal. Occasionally check the WebUI at localhost:8384. After macOS updates, verify Syncthing is running with brew services list.
Update 2026-05-02: Syncthing + git race conditions on the same file
After a month running this setup, one recurring problem showed up: editing files like .stignore or .gitignore would cause git pull to fail with "Your local changes would be overwritten by merge."
The mechanism is straightforward once you see it:
- I edit
.stignoreon Machine A. - Syncthing notices the file change and propagates it to Machine B in seconds (file-level, FSEvents-based).
- I commit on A and push.
- On Machine B I run
git pull. It errors. The working tree already matches the new commit's content (Syncthing got there first), but git's HEAD is still on the old commit, so git sees the new content as uncommitted local modifications.
In short, Syncthing and git are touching the same file at different layers and on different schedules. Race condition.
Fix: take sync-rule files out of Syncthing entirely
Add this to .stignore:
// Manage these via git only (avoid Syncthing/git race)
.gitignore
.stignore
Now .gitignore and .stignore are git-only. Syncthing leaves them alone. Updates flow through git push / git pull exclusively. The race is gone.
This works because the problematic files are exactly the sync-rule files themselves. Self-referential, but harmless: .stignore ignoring itself means each machine keeps its own copy of .stignore, but they stay identical because git is the single source of truth.
When NOT to apply this pattern
This is worth doing only for files where the Syncthing-vs-git race fires often. For most other files (skills, agents, scripts), occasional conflicts are rare and easy to clear:
cd ~/.claude
# Common case: local content already matches remote, just clear the diff
git checkout HEAD -- <file>
git pull
Or, if you genuinely want to keep your local edits:
git stash push <file>
git pull
git stash pop
One footgun while you're recovering
git reset --hard origin/master will wipe all uncommitted changes to git-tracked files. Anything in .gitignore survives. If you've been mid-edit across several files when things tangled, commit (or copy aside to /tmp) before resetting.
Update 2026-07-22: (?d) and trailing slashes, or why a folder won't leave sync
Two months in, I noticed one folder, image-cache, was stuck "out of sync," with Syncthing logging the same error every minute:
Failed to sync (path=image-cache error="syncing: delete dir: directory has been deleted on a remote device but contains ignored files (see ignore documentation for (?d) prefix)")
Cause 1: a folder deleted on the remote + local ignored files = can't delete
I had deleted the image-cache directory on one machine. Syncthing tried to propagate that deletion to the other, but the local image-cache still held ignored files. Syncthing refuses to delete a directory that contains ignored files — it assumes they might be precious local data. So it retries the delete every cycle and fails.
The error message names the fix itself: the (?d) prefix. It means "when the parent directory is deleted, it's OK to delete these ignored files too." Safe to add for a regenerable cache.
(?d)image-cache
Cause 2: a trailing slash only ignores the contents (the real trap)
There was a deeper issue. The original pattern was image-cache/, which expands to image-cache/** — meaning it ignores what's inside the folder but not the folder entry itself. So the empty folder shell kept getting tracked as part of the sync set.
I confirmed it: I dropped a file under image-cache, rescanned, and Syncthing's tracked file count (globalFiles) went from 183 → 184. The contents (the png files, the test file) were ignored, but the folder entry entered the sync set.
Fix: drop the trailing slash. image-cache (no slash) matches the folder entry itself, so the whole folder leaves sync. Recreating the same file and rescanning left the count unchanged this time.
// With a slash: contents ignored, but the empty folder still syncs
image-cache/
// Without a slash: the folder entry is ignored too, folder fully leaves sync
(?d)image-cache
Two caveats
1. .stignore is per-machine. Syncthing does not sync .stignore itself (it's device-local by design). So the change above has to be made on each machine separately for the folder to leave sync on both sides. Fix only one and the other keeps tracking the folder.
2. .git is the exception. I added (?d) to the cache folders but not to .git. (?d) authorizes deleting local files when a remote deletion propagates — put that on .git and Syncthing could wipe your local git repo. I also root-anchored it (/.git) so it doesn't overlap with the skills/*/.git submodule rule.
// Protect the local git repo: no (?d), root-anchored to stay separate from the submodule rule
/.git
(?d)skills/*/.git
In short: to fully exclude a folder from Syncthing, drop the trailing slash, and add (?d) to regenerable folders so a propagated remote deletion doesn't get stuck. Just don't put (?d) on something precious like git.