Skip to content
AstroPaper
Go back

Designing a Config Sync Strategy for Claude Code

Edit page

The Problem

Claude Code stores user-level configuration under ~/.claude/ — settings, status line scripts, custom skills, rules, and more. If you work across multiple machines or just want your config version-controlled, the natural instinct is to manage it through a dotfiles repo.

For most tools (tmux, vim, zsh), the standard approach is simple: keep the source of truth in your dotfiles repo and symlink it into place. But Claude Code and symlinks don’t mix well.

Claude Code’s Configuration Scopes

Claude Code has four configuration scopes, each with its own location and purpose:

ScopeLocationShared?Who it affects
ManagedSystem-level (deployed by IT)YesAll users on the machine
User~/.claude/NoYou, across all projects
Project.claude/ in repoYesAll collaborators
Local.claude/settings.local.jsonNoYou, in this repo only

Precedence flows from highest to lowest: managed > local > project > user. Array settings merge across scopes rather than replace, so permissions defined at multiple levels combine.

The user-level scope is the one we want in our dotfiles — it includes:

The obvious dotfiles approach — ln -sf ~/dotfiles/claude ~/.claude — runs into multiple known issues:

The only place symlinks are officially supported is .claude/rules/, which explicitly documents symlink resolution and circular symlink detection.

How the Community Copes

Without reliable symlink support, people have landed on several workarounds:

  1. Copy-based install scripts — most common. Keep source of truth in dotfiles, cp to ~/.claude/. Simple but manual.
  2. Chezmoi — templates and applies configs without symlinks. Growing in popularity for AI tool configs.
  3. Dedicated config repos — projects like claude-config centralize config with init scripts.
  4. Per-project copy-paste — not elegant, but works. Many people just copy configs into each new project.

The Design: A Self-Installing Dotfiles Skill

Instead of fighting symlinks or maintaining external scripts, we can use Claude Code’s own skill system to solve the problem. Here’s the architecture:

graph TD
    A["dotfiles repo"] --> B["claude/<br/>(user-level source of truth)"]
    A --> C[".claude/<br/>(project-level config)"]
    C --> D[".claude/skills/install-claude-config/"]
    B --> E["settings.json"]
    B --> F["statusline.sh"]
    B --> G["skills/blog/SKILL.md"]
    D -->|"/install-claude-config"| H["~/.claude/<br/>(deployed config)"]

Directory Layout

The dotfiles repo has two separate Claude Code directories:

dotfiles/
├── .claude/                          # Project-level (for this repo only)
│   └── skills/
│       └── install-claude-config/    # The deployment skill
│           └── SKILL.md
├── claude/                           # User-level source of truth
│   ├── settings.json
│   ├── statusline.sh
│   └── skills/
│       └── blog/
│           └── SKILL.md
├── tmux/
└── install.sh

The Merge Skill

The /install-claude-config skill handles deployment with strict safety rules:

Additive only — it never changes or deletes anything already in ~/.claude/:

ScenarioAction
File/key doesn’t exist in targetADD — copy it
File/key exists with identical content⏭️ SKIP — nothing to do
File/key exists with different content🛑 CONFLICT — stop and report

Dry-run first — the skill follows a four-step procedure:

  1. Discover — scan claude/ and compare each file against ~/.claude/
  2. Plan — present exactly what will happen (ADD/SKIP/CONFLICT) for every file
  3. Execute — only after explicit user approval
  4. Report — summarize what was done

For JSON files like settings.json, the merge is key-by-key and recursive. New keys are added, identical keys are skipped, and conflicting keys halt the process. For all other files, it’s whole-file comparison: copy if new, skip if identical, conflict if different.

Example Run

> /install-claude-config

## Merge Plan

| File                  | Action   | Details                              |
|-----------------------|----------|--------------------------------------|
| settings.json         | SKIP     | statusLine key already identical     |
| statusline.sh         | SKIP     | Already identical                    |
| skills/blog/SKILL.md  | COPY     | Target does not exist                |

Do you want to proceed with this plan?

> yes

## Report
| File                  | Action   | Result |
|-----------------------|----------|--------|
| settings.json         | SKIP     | Already identical |
| statusline.sh         | SKIP     | Already identical |
| skills/blog/SKILL.md  | COPY     | Done |

Why This Approach Works

Trade-offs and Limitations

Future Possibilities

As Claude Code matures, some of these pain points may resolve naturally:

Until then, the additive merge skill provides a pragmatic, safe solution that works within Claude Code’s current constraints.

Footnotes

  1. An automated approach using Claude Code hooks (e.g., on SessionStart) is possible but adds complexity and risk. The manual invocation is a deliberate design choice favoring safety over convenience.


Edit page
Share this post on:

Previous Post
Markdown Standards and the GitHub Convergence Problem
Next Post
OpenAI Codex: From Code Model to AI Agent Ecosystem