For three years I ran my entire engineering life on one Windows laptop. One ~/.claude/ directory. One .agents/ directory full of carefully tuned skills. One .codex/auth.json keeping ChatGPT plugged into image generation. One folder of Telethon session files for two Telegram accounts.
Then I added a Mac. Then I started thinking about a Hetzner VPS for laptop-free work. Then Anthropic shipped Claude Code Web and the mobile Cloud Environment.
Suddenly the question was: how do I make this exact AI workspace appear on every device, automatically, without copy-pasting JSON or re-installing skills by hand?
What follows is the architecture I landed on after a long working session. It isn't pretty. It works. It's also a useful frame for thinking about what should sync, what must not, and what's genuinely unsolved.
What's actually in ~/.claude/
If you've used Claude Code for more than a week, your home directory has a .claude/ folder doing a lot of jobs. Not all of it is yours, and not all of it should travel.
~/.claude/
├── commands/ ← global slash commands (you write these)
├── skills/ ← global skills (you write or curate these)
├── CLAUDE.md ← global instructions to the model
├── settings.json ← model, theme, MCP servers, hooks
├── settings.local.json← per-machine secrets (do NOT sync)
├── keybindings.json ← your chord shortcuts
├── plugins/ ← marketplace plugins (huge, OS-specific)
├── projects/ ← per-project session history (huge, ephemeral)
├── cache/, sessions/, history.jsonl, ide/, downloads/, ...
The mental model: the top four are content you authored. The plugins directory is package-managed (you reinstall, you don't sync). Everything else - projects, cache, sessions, IDE plumbing - is ephemeral state that lives on one machine and dies with it. Synchronizing any of it is either pointless or actively harmful.
I learned this the obvious way: my first instinct was "just put the whole thing in iCloud / Google Drive". This is a trap. Two clients writing to a session SQLite at once will corrupt it. A cache file written on Mac and read on Windows hits encoding bugs. Plugin binaries are platform-specific and several gigabytes. Anthropic itself doesn't sync ~/.claude/, on purpose - they leave it to you.
What you must not sync (and why)
Before any architecture, the ban list:
plugins/- gigabytes of compiled binaries, bun caches, native bindings. Reinstall viaclaude plugin install <name>on each machine.projects/,sessions/,history.jsonl- per-machine session state. If you sync this, two machines will fight over locks and you will corrupt your own conversation history.settings.local.json- local secrets (API keys you've pasted in). The whole point of.local.jsonin Anthropic's settings model is "don't share this".~/.codex/auth.json- if you also use the Codex CLI for ChatGPT-backed image generation, this file holds OAuth tokens that auto-refresh every ~7 days. Two machines refreshing at once will kill each other's tokens. Log in separately on each machine.- Anything binary that any process writes to - Telethon
*.sessionSQLite files, browser cookies, etc. These are not designed for shared writers.
That leaves a small, clean set: commands, skills, CLAUDE.md, settings.json, keybindings.json. This is what we sync.
The two-repo pattern
I ended up with two private GitHub repos:
| Repo | Mounts to | Holds |
|---|---|---|
claude-config | ~/.claude/ (file-by-file) | commands/, CLAUDE.md, settings.json, keybindings.json |
claude-skills | ~/.claude/skills/<name> (per-skill symlinks) | Each subdirectory is one skill (with its own SKILL.md, scripts, references) |
Why two repos? Because the granularity is different. Settings and the global CLAUDE.md change on every machine. You want one file = one symlink to git. Skills are folders, and you want to add a new skill on any machine and have it ship to all of them as a unit. Per-skill symlinks also play nicely with plugin-installed skills that want to coexist in ~/.claude/skills/ without being committed.
The pattern on each side:
Windows (source machine):
~/.claude/commands ─── junction ───▶ ~/claude-config/commands git
~/.claude/CLAUDE.md ─── symlink ───▶ ~/claude-config/CLAUDE.md ↓
~/.claude/settings.json ─ symlink ──▶ ~/claude-config/settings.json ↓
push
macOS / Linux / VPS:
git pull
~/.claude/commands ◀── symlink ──── ~/claude-config/commands ↓
~/.claude/CLAUDE.md ◀── symlink ──── ~/claude-config/CLAUDE.md
...
Edit any file on any machine - you're editing the repo file directly through the symlink. Commit, push. The other machines pull (manually or on a schedule) and the new content is live.
I also documented one decision sharply: HTTPS remote, not SSH. Why? Because gh CLI's credential helper handles authentication transparently on every modern OS, and SSH key distribution across Mac / Windows / VPS / mobile sandbox is a chore I refused to repeat four times. If you already have a passwordless SSH agent across all your devices, fine - keep SSH. Most people don't.
Cross-platform symlink theatre
On macOS and Linux, ln -s does the job and nothing more needs saying.
Windows has two distinctions that matter. Directory junctions (mklink /J) work for any user with no admin and no Developer Mode - use these for commands/. File symlinks require Developer Mode on (Settings → Privacy & Security → For Developers), which most engineering accounts already have - use these for CLAUDE.md, settings.json, keybindings.json. The PowerShell bootstrap checks for Dev Mode and prompts.
The subtler gotcha was personal. My pre-existing ~/.claude/skills/ had 17 directories that were themselves symlinks into ~/.agents/skills/ - a separate dotfiles tree I'd accumulated as a single-machine artifact. The naive bootstrap could go three ways. (A) follow the symlinks and copy resolved contents into the new repo, losing the .agents/ link layer. (B) skip them, leaving the new machines without those skills. (C) ignore the problem.
The right answer was (A) plus an explicit decision to retire .agents/ entirely. There was no upstream - I'd just been authoring there because I'd never crossed machine boundaries before. Once I admitted that, the script resolved every symlink at copy time and emitted plain directories into claude-skills/. The .agents/ tree stayed on Windows untouched but no longer canonical, and after a verification window it moved to .agents.bak.
The generic lesson when migrating any single-machine workflow to multi-machine: find the symlinks pointing into your other personal trees and decide whether those trees should become repos, get folded in, or get retired. Almost always retired or folded in.
Auto-pull cadence
Manual git pull on every machine every day is a recipe for forgetting. I used:
- macOS:
launchd. Per-repo plist at~/Library/LaunchAgents/com.<you>.<repo>-pull.plist, runsgit -C <repo-path> pull --ff-only --quietevery 30 minutes. Logs to~/Library/Logs/. - Linux / VPS: systemd user timer + oneshot service. Same idea,
systemctl --user enable --now <repo>-pull.timer. - Windows: Task Scheduler. Same
git pull --ff-only --quieton a schedule.
30 minutes is the sweet spot. It's short enough that you don't have to think about it (you commit on machine A, walk to machine B, it's there). It's long enough that GitHub doesn't see your IPs as a polling pest. The plist is a single file, generated by a 60-line install script that takes the repo path and the interval as arguments.
There's one cosmetic finding worth flagging. My initial bootstrap reported "38 skills synced" but the GitHub repo had 36. The PowerShell script's counter incremented per loop iteration, but git add -A silently skipped two empty directories that were placeholders for slash commands defined elsewhere. Empty directories are git's quiet enemy. Now I know.
Multi-account secrets, generalised
A second sync problem hides in the same neighborhood: secrets that aren't config, but which need to be available to every machine.
In my case it was four Telegram accounts, each with its own API ID and hash, used by Python scripts that read client chats for sales analysis. The scripts originally hardcoded two accounts. With four (and growing) the right pattern is discover from environment, not hardcode:
TG1_LABEL=mihailorama
TG1_API_ID=...
TG1_API_HASH=...
TG1_SESSION=/abs/path/to/tg_session_mihailorama
TG2_LABEL=mihailorama2
...
A small tg_accounts.py module reads .env.tg, finds every TG<N>_* block, and yields a label-keyed dict. Adding a fifth account is a four-line append to the env file. Zero code change.
The same envelope applies to: multiple OpenAI keys, several Vercel projects, parallel Cloudflare tokens, anything where the count grows.
The env file itself is gitignored - it never travels through git. It moves between machines via SCP, password manager, or hand entry on the rare occasion. The point isn't to sync secrets. The point is to make the code that consumes secrets indifferent to how many of them you have.
The unsolved part: mobile and Cloud Environment
This is where the architecture stops being elegant.
Anthropic now offers Claude Code Web at claude.ai/code and a mobile experience that runs in a Cloud Environment (a remote sandbox provisioned per session). When you open Claude Code on your phone, it isn't reading ~/.claude/ from your phone - there's no .claude/ on your phone. It's reading from a fresh ephemeral sandbox.
The good news: that sandbox has git. So in principle the same bootstrap script that works on macOS works there:
git clone https://github.com/<you>/claude-config.git ~/claude-config
git clone https://github.com/<you>/claude-skills.git ~/code/claude-skills
bash <bootstrap-unix.sh>
bash ~/code/claude-skills/bootstrap.sh
The bad news: every Cloud Environment session is fresh. You'd have to re-run the bootstrap every time. Two ways to handle that:
- A session-init hook. If Anthropic exposes a "run this on session start" mechanism (they hint at this in their docs), point it at a single
cloud-env-bootstrap.shin your repo. Then every new mobile session is configured in ~10 seconds. - A persistent home volume. If Cloud Environment grows persistent storage (Anthropic has signalled this is on the roadmap), do the bootstrap once and let the volume retain symlinks across sessions.
I'd love both. Today, neither exists in a public, documented form. The pragmatic move is to write the cloud-env bootstrap now, keep it in the same tools/claude-config-sync/ directory, and turn it on the moment Anthropic exposes the hook. If you're an Anthropic engineer reading this - the API surface I'd want is exactly what chezmoi does: a one-line "apply these dotfiles" call at session-init.
The OAuth piece is harder. ~/.codex/auth.json cannot be safely synced. In a Cloud Environment, the only clean answer is device-flow OAuth - open a URL on your phone, authenticate, paste the resulting token back. This works for ChatGPT-backed image generation today (npx @openai/codex login), and the same pattern will work for any browser-OAuth provider. For API-key providers, an env var injected via the cloud sandbox's secret store does the job.
"Why not just use chezmoi?"
Fair question. There are mature dotfile managers - chezmoi, yadm, stow, dotbot - and any of them could in principle host these files. Why two thin custom repos plus shell scripts?
chezmoi (~30K stars, Go) is the most powerful: templates, OS-conditional content, password-manager integration, encrypted secrets. If you're going to manage dotfiles for everything (shell, editor, system) it's the right answer. For ~/.claude/ alone it's overkill - you're paying template-engine cognitive cost to manage four files and one directory of skills, all of which are platform-neutral text already.
yadm (~5K stars, bash) is "git for your home directory with extras". Cleaner than chezmoi if you don't need templating. Same overkill issue at this scope, plus it wants to own the entire $HOME as a git work-tree which conflicts with most peoples' existing arrangements.
stow (GNU, decades old) is the closest to what we built: symlink farms from a source directory into a target. The reason I didn't use it: stow assumes one source dir → one target tree. Our two-repo split (config files vs per-skill folders) maps awkwardly. Two separate stow invocations would work, but at that point the bootstrap script is doing more work than the tool saves.
The custom approach pays off in two specific places: the Cloud Environment bootstrap (a portable bash script that an ephemeral sandbox can curl-pipe-bash on session start, no preinstalled tooling required) and the per-skill symlink granularity (so plugin-managed skills can coexist in ~/.claude/skills/ next to your synced ones without colliding). If your dotfiles needs are wider than this, use chezmoi. If they're exactly this, the 4-script setup is smaller surface area to maintain.
What I'd build next
If I had a clean weekend:
bootstrap-cloud-env.sh- DONE. Sits in the sametools/claude-config-sync/directory. Runs inside any ephemeral sandbox, clones both repos, symlinks everything in under 10 seconds, idempotent on re-invoke. What it can't do is trigger itself on a fresh Cloud Environment session - that requires Anthropic to expose a session-init hook.- A
manifest.jsonin the dotfiles repo listing what should be symlinked where, so the per-OS bootstrap scripts collapse into one driver + one declarative file. - A skills-pack publisher so the curated skills can ship to others (and back to me) via a public marketplace, not just my private repo.
I'll publish the bootstrap scripts as a public template repo soon, with the entire architecture documented end-to-end. If you want a notification when it's out, leave a comment, ping me on Telegram, or watch github.com/Mihailorama.
TL;DR
- One
~/.claude/per machine is fine. Don't sync the whole thing. - Do sync
commands/,skills/,CLAUDE.md,settings.json,keybindings.jsonvia two private GitHub repos. - Use directory junctions on Windows (no admin), real symlinks on Mac/Linux. Turn on Developer Mode on Windows for file symlinks.
- Auto-pull every 30 minutes via launchd / systemd / Task Scheduler. Done.
- Don't sync
.codex/auth.json,settings.local.json, plugins, sessions, or projects. - Multi-account secrets: env-driven discovery, gitignored
.env.<thing>, code that doesn't care how many. - Cloud Environment / mobile is the open frontier. The bootstrap pattern works there. The trigger doesn't exist yet.
The full pattern, with PowerShell + bash bootstrap scripts, lives in my CMO operations repo under tools/claude-config-sync/. I'll publish a clean public version once it's stable across one more month of daily use.