The first time I tried to share a Claude Code skill with the team, I stopped a stow command halfway through, because it was about to replace my entire ~/.claude/ directory — sessions, credentials, in-flight projects — with a symlink into a git repo.
The setup looked harmless. We had a repo, agents-shared, where teammates were dropping their slash commands and skills; everyone was reinventing slightly different versions of the same thing. The obvious move: make the repo the source of truth and symlink it into everyone's ~/.claude/.
The catch is that ~/.claude/ is not a config folder you can symlink the way you'd symlink a .vimrc. It's a mixed-state directory where team-shared assets sit next to machine-local state, and most sharing strategies treat the whole folder as one thing.
The folder that isn't a config folder
After a couple of weeks of using Claude Code, ls -la ~/.claude looks like this:
backups/
cache/
claude_desktop_config.json
debug/
history.jsonl
ide/
mcp-needs-auth-cache.json
plans/
plugins/
policy-limits.json
projects/
session-env/
sessions/
settings.json
shell-snapshots/
skills/
statsig/
telemetry/
todos/
CLAUDE.md
commands/
agents/
Only four entries — skills/, commands/, agents/, and CLAUDE.md — are things a team might want to share. Everything else is local state: open sessions, conversation history, MCP auth tokens, settings overrides, in-flight plans.
The naïve strategies fail in different ways. Symlink the whole folder and Claude Code writes its sessions and credentials into the repo. Symlink only the four subfolders and you're closer, but run stow with the wrong package layout and it will replace ~/.claude/ with a single directory symlink, quietly breaking the tool's ability to write its own state. Copy files in and out with a Make target and the copies drift the first time someone edits the local one and forgets to copy back.
~/.claude/ is two folders pretending to be one: a small team-shared surface and a much larger machine-local surface that must never travel.
Three constraints that make stow safe here
Once you frame it that way, the implementation is GNU stow with three non-obvious constraints:
- The package layout mirrors
~/.claude/but only contains the four shared entries:
The deny-by-defaultconfigs/claude/.claude/ ├── .gitignore # deny-by-default allowlist ├── CLAUDE.md ├── agents/ ├── commands/ └── skills/.gitignoreis the safety net: anything new Claude Code writes here stays untracked unless explicitly re-included, so sessions can't leak into the repo. - Stow runs with
--no-folding. By default stow does "tree folding": if a target directory doesn't exist, it creates one symlink for the whole folder — the exact failure I nearly hit.--no-foldingcreates one symlink per leaf, so~/.claude/stays a real directory Claude Code can write into. - Conflicts are resolved per item. If
~/.claude/skills/already exists with personal skills, that one folder moves to~/.claude/skills.backup-<timestamp>and stow takes its place.~/.claude/itself is never touched.
The whole sync command — install stow, locate the repo, git pull --ff-only, inventory the four tracked items, per-item migration and backup, dry-run, then stow for real — is a single slash command checked into the repo. The first run asks for confirmation at every destructive step. Later runs are git pull plus stow --restow, no prompts.
Testing it against a fake $HOME
I stopped that first stow command halfway for a reason that applies to any tool like this: you can't test it against the live folder without risking the folder you're testing against. The workaround is a fake $HOME:
mkdir -p /tmp/fake-home/.claude
echo '{"theme":"dark"}' > /tmp/fake-home/.claude/settings.json
echo '{"creds":"fake"}' > /tmp/fake-home/.claude/.credentials.json
cd ~/Code/agents-shared
stow --no-folding --target=/tmp/fake-home --dir=configs \
--verbose=2 --simulate claude
The dry run, against a sandbox you can rm -rf, shows which symlinks would be created and — more importantly — which files would not be touched. If settings.json or .credentials.json appear anywhere in the output, the config is wrong and the real run would have damaged your real folder.
The dry run also caught a bug I wouldn't have spotted: a stale empty directory in the repo (configs/claude/skills/, left over from an earlier layout) was making stow plan MKDIR: skills in the target root. Against the real home, that would have created a mystery ~/skills/ folder in my home directory. Deleting the empty repo folder fixed it before it happened.
One bootstrap constraint shows up late if nobody warns you: the sync command lives at configs/claude/.claude/commands/sync-agents.md, and the only way it appears as /sync-agents in Claude Code is to run itself. So the first run is a manual cp into ~/.claude/commands/; from the second run on, the symlink takes over. Any tool that manages its own deployment has this step, and it usually goes wrong the first time someone forgets it.
What I still don't have a clean answer for
Multi-tool sharing. The same prompt might be useful as a Claude Code slash command, a Codex skill, an OpenCode command, and a Copilot prompt — four folder conventions, four frontmatter formats. I tried a tool-agnostic prompts/ folder with symlinks fanning out into each tool's package and rolled it back the same day: the indirection cost more than it bought, and the prompts are still settling. I'll revisit if the same prompt drifts between tools and the divergence hurts.
Personal skills that aren't ready to share. I have a weekly-insights skill iterating in ~/.claude/skills/; the moment I run /sync-agents, that folder gets backed up and replaced with a symlink to the repo, where my skill doesn't live. The current answer — develop in the repo on a branch, symlink it manually for testing, PR when ready — works but is clumsy. A skills.local/ overlay merged into the symlinked folder is the likely future version; I haven't built it because I want to see what breaks first.
The mistake to avoid is treating ~/.claude/ — or any AI tool's config directory — as one thing you can put under version control. It's four shared entries surrounded by state the tool owns and writes constantly. A deny-by-default allowlist on the package side, --no-folding on the stow side, per-item backups, and a fake $HOME dry run cover the rest.