Sync Claude Code configuration across machines (ccs): collect, apply, and bootstrap from a GitHub payload repo
Project description
dazzle-claude-config
Git-backed sync for your Claude Code configuration (skills, commands, agents, hooks, and CLAUDE.md) across every machine you work on, with credential guards on the way in and backups on every write.
The Problem
Your Claude Code setup is earned. Skills you refined over months, commands that encode how you actually work, a CLAUDE.md tuned by a hundred small corrections. All of it lives in ~/.claude on one machine.
Then you get a laptop. Or a work box. Or you reinstall. So you copy the folder by hand -- and now the two drift apart, silently, because there is nothing to tell you which one is newer. Worse, ~/.claude is not only config: it also holds .credentials.json, OAuth state, plugin caches, and session databases. Copy it wholesale into a git repo to "back it up" and you have published an API key. There is no history either, so when a config change makes Claude behave oddly, there is nothing to diff and nothing to roll back to.
ccs treats your configuration as a git repository -- a payload -- and moves files between that repo's checkout and your live ~/.claude. collect copies a live config in, refusing credentials rather than trusting you to notice them. apply copies config back out, backing up every file it overwrites. Git does what git is good at: history, review, and resolving the conflict when two machines change the same skill. Because a payload is just a repo, it can be yours, someone else's, or a fork of theirs.
[!NOTE] Alpha software (v0.2.1) -- working, in daily use, surfaces not frozen. It manages my own configs across machines. The core loop (
collect/apply/status/diff, deny-list + credential scanning, backups, staged removals) are functional; the Phase 2 set (templated settings rendering, declarative plugin installs, and one-commandccs bootstraponboarding) is still a WIP, and command surfaces may change between versions until it does. Please file issues for anything rough. And, as with any sync tool, keep an independent copy of anything irreplaceable.
Part of the DazzleML Claude toolchain: session-logger records, claude-session-backup preserves, ccs distributes.
Quick Start
pip install dazzle-claude-config # installs `ccs` (alias: dazzle-claude-config)
Python 3.10+, stdlib only, git is the sole external dependency.
Try a config without committing to one. dazzle-claude-code-config is a public collection (skills, commands, and agents for structured design analysis, postmortems, human test checklists, and multi-agent consultation) and it is a working payload repo you can point ccs at right now:
git clone https://github.com/DazzleML/dazzle-claude-code-config ~/claude/dazzle-config
ccs status --checkout-dir ~/claude/dazzle-config # what differs? nothing is touched
ccs diff --checkout-dir ~/claude/dazzle-config # which files, exactly?
ccs apply --only dotclaude/skills --checkout-dir ~/claude/dazzle-config # a slice...
ccs apply --checkout-dir ~/claude/dazzle-config # ...or the lot
--only takes a prefix of the path inside the repo (the left-hand paths ccs diff prints) which is why it reads dotclaude/skills rather than skills. A prefix matching nothing warns rather than silently doing nothing.
apply merges into your live config rather than replacing it, backs up anything it overwrites, and treats CLAUDE.md as seed-if-absent (so your own memory file is never clobbered). Fork it if you want to build on it; it is designed to be forked.
Already have your own config repo:
git clone <your-payload-repo> ~/claude/my-config
ccs status --checkout-dir ~/claude/my-config
ccs apply --checkout-dir ~/claude/my-config
Starting from the config you already have on this machine -- see Turning your current config into a payload for the four commands that package ~/.claude into a repo.
Then, day to day:
ccs status # three-way drift report (live vs checkout vs remote); exit 1 = drift
ccs diff # per-file details
ccs collect # live config INTO the checkout (credentials refused + reported)
ccs apply # checkout config INTO the live tree (originals backed up first)
Usage
ccs status answers "am I in sync?" across all three legs -- your live config vs the checkout, the checkout vs its remote, and any uncommitted work in the checkout:
checkout ~/claude/dazzle-claude-code-config
on main, in sync with origin/main
compared 83 files across 11 entries of config
~/.claude
~/claude
protected (1 file kept out of sync on purpose -- matches a deny rule, so ccs will not copy it in either direction)
bin/gpg-loopback.sh
status: clean -- your live config and the checkout match; nothing to collect, nothing to apply
Output is colorized on a TTY (and plain when piped, or with --no-color / NO_COLOR).
collect and apply support --dry-run. apply supports --only <prefix> and --sync-removals (staged to the backup dir -- nothing is ever deleted in place). Options work before or after the verb.
Where things live
ccs moves files between exactly three locations. The distinction that matters: your live config is what Claude Code reads; the checkout is a git clone that Claude Code never looks at.
| Default location | Who reads it | How to point elsewhere | |
|---|---|---|---|
| Live config | ~/.claude |
Claude Code, every session | CLAUDE_CONFIG_DIR env, or --claude-dir |
| User territory | ~/claude |
you -- notes, backups, and where checkouts land | --user-claude |
| Payload checkout | ~/claude/dazzle-claude-code-config |
ccs and git -- not Claude Code | CCS_CHECKOUT_DIR env, or --checkout-dir |
The payload checkout is an ordinary git clone of a config payload repo: a repository whose contents are skills, commands, agents, hooks, and a CLAUDE.md. Editing files there changes nothing until you run ccs apply. That indirection is the whole point. It gives your config a place to be versioned, reviewed, conflict-resolved, and shared, without a half-finished edit reaching a live session.
flowchart LR
subgraph laptop["laptop"]
L1["~/.claude<br/>live config"]
C1["payload checkout<br/>(git clone)"]
end
subgraph gh["GitHub"]
R["payload repo"]
end
subgraph desktop["desktop"]
C2["payload checkout"]
L2["~/.claude"]
end
L1 -- "ccs collect" --> C1
C1 -- "ccs apply" --> L1
C1 -- "git push" --> R
R -- "git pull" --> C2
C2 -- "ccs apply" --> L2
ccs owns the vertical hops (live ↔ checkout, guarded and backed up). Git owns the horizontal ones (checkout ↔ GitHub ↔ your other machines). No merge logic lives in ccs -- a conflict between two machines is an ordinary git conflict you resolve in the checkout with your normal tools.
Turning your current config into a payload
You already have a ~/.claude full of work. To package it:
gh repo create my-claude-config --private # or make the repo in the web UI
git clone <that repo> ~/claude/my-config
cd ~/claude/my-config
mkdir skills commands agents # the surfaces you want tracked
touch CLAUDE.md # only if you want your memory synced
ccs collect --checkout-dir ~/claude/my-config # copies your live config in, guarded
git add -A && git commit -m "my config" && git push
The empty directories are how you say what to track: collect fills in each surface you created and ignores the ones you didn't. Nothing is copied blind -- the deny-list and credential scan run on the way in, and anything refused is reported rather than silently skipped, so .credentials.json, settings.local.json, plugin state, and any file containing a credential-shaped token stay out of the repo. Read the collect output before you commit; it is the last cheap moment to notice a surprise.
Run ccs collect from then on whenever you change your live config, and commit. There is no manifest to write unless you want one -- the implicit layout above is enough for most people.
Whose config is in the checkout?
Any repo that holds config. That is the interesting part:
- Your own (the main use case) -- a private repo you push from one machine and pull on the next. Your config follows you.
- Someone else's, read-only -- point a checkout at a public collection such as dazzle-claude-code-config and
ccs applyto try the "Dazzle" skills collection.--only <prefix>takes a slice instead of everything. - A fork of someone else's -- start from a collection you like, then
ccs collectyour own changes on top and push to your fork. Theirs becomes yours, and you can still pull their updates.
Nothing stops you keeping several checkouts side by side and pointing ccs at whichever you want, as they are just directories:
ccs diff --checkout-dir ~/claude/someones-collection # what would change?
ccs apply --checkout-dir ~/claude/someones-collection --only skills/ # borrow just their skills
ccs status --checkout-dir ~/claude/my-config # back to yours
apply copies into your live tree rather than swapping it, so borrowing from a second collection merges on top of what you have (originals backed up first, as always). Named profiles with a single active set -- ccs use <name> -- are a tracked idea, not a current feature; today, --only plus your own payload as the source of truth is the way to stay in control.
Set the one you use daily and stop typing the flag:
export CCS_CHECKOUT_DIR=~/claude/my-config # POSIX (add to your shell profile)
setx CCS_CHECKOUT_DIR C:\src\my-config # Windows (applies to new shells)
Precedence: --checkout-dir > CCS_CHECKOUT_DIR > the default above.
A payload repo needs no special structure. If it has a ccs-manifest.json, that file declares exactly what syncs where. If it does not, ccs treats a repo that looks like a ~/.claude directory (root-level CLAUDE.md, skills/, commands/, agents/...) as one -- so a repo someone made by pushing their config folder as-is just works.
How it works
- Manifest-driven allowlist: the payload repo's
ccs-manifest.jsondeclares what syncs where (territories, per-entry strategy:copy,seed-if-absent,render,plugins). Nothing outside the manifest ever moves. - Secrets are structurally fenced: a hard deny-list (
.credentials.json,.claude.json,settings.local.json, databases, plugin caches...) refuses files even if listed, and collect scans content for credential shapes (sk-ant-,ghp_, AWS keys, private-key headers...) -- refusals are reported, never silent. - Git is the merge arena: multi-machine conflicts are ordinary git conflicts in the checkout;
applyrefuses while conflicts are unresolved.ccscontains no merge logic and exposes no branch operations. - The home repo is untouchable: if your home directory is itself a git repository (e.g. managed by claude-session-backup),
ccsstructurally refuses to operate on it. - Nothing is destroyed: every overwrite is preceded by a timestamped backup under
~/claude/backups/ccs/; removals are staged there, never deleted in place. - Index verification: files copied into the checkout are checked against git's ignore/exclude mechanism, so a machine-level exclude can never silently drop config from the payload.
Roadmap
render (templated settings with per-OS/per-machine overlays), declarative plugin install, and ccs bootstrap <payload-url> (one-command machine onboarding) land in Phase 2 -- see the pinned Roadmap issue.
Contributing
Contributions welcome! Please open an issue or submit a pull request.
See CONTRIBUTING.md for:
- Development setup (
pip install -e ".[dev]") - Running the test suite and human test checklists (
tests/checklists/) - Version management with
sync-versions.py - Pull request checklist
Like the project?
Related Projects
- dazzle-claude-code-config - A public collection of Claude Code skills, commands, agents, and settings -- a ready-made payload repo to point ccs at, or fork as your own
- claude-session-logger - Real-time per-session tool/conversation logging; its session naming and state-file conventions are part of the config surface ccs syncs
- claude-session-backup - Backs up and restores Claude Code session history; the preservation half of the toolchain ccs distributes into
License
dazzle-claude-config, Copyright (C) 2026 Dustin Darcy
Licensed under the GNU General Public License v3.0 (GPL-3.0) -- see LICENSE
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dazzle_claude_config-0.2.1.tar.gz.
File metadata
- Download URL: dazzle_claude_config-0.2.1.tar.gz
- Upload date:
- Size: 51.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c495a7411340a6e0bd5b17b248002a1ee7e40dad52b8e46afe8883b69ad5231e
|
|
| MD5 |
5399a63578f9b0146abe181adeff63e1
|
|
| BLAKE2b-256 |
9ea39369ef10b223e44a4d9f532c1256dd75c180bda36025aaf4cecf09521663
|
Provenance
The following attestation bundles were made for dazzle_claude_config-0.2.1.tar.gz:
Publisher:
release.yml on DazzleML/dazzle-claude-config
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dazzle_claude_config-0.2.1.tar.gz -
Subject digest:
c495a7411340a6e0bd5b17b248002a1ee7e40dad52b8e46afe8883b69ad5231e - Sigstore transparency entry: 2243063381
- Sigstore integration time:
-
Permalink:
DazzleML/dazzle-claude-config@3b3f3e6b093376ae0764c0a630406d996e7897c2 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/DazzleML
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3b3f3e6b093376ae0764c0a630406d996e7897c2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file dazzle_claude_config-0.2.1-py3-none-any.whl.
File metadata
- Download URL: dazzle_claude_config-0.2.1-py3-none-any.whl
- Upload date:
- Size: 41.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fb69b83ae6ee3650fe39e19021a1bd0c366a17a73f052650f51ee7b7c206eb2d
|
|
| MD5 |
d8228e3a3493acd3eb3d1dec777787f7
|
|
| BLAKE2b-256 |
e27d929c913a36a17034a4a0f2272cc077bb2a798f8491f0ed047ea5f76fe674
|
Provenance
The following attestation bundles were made for dazzle_claude_config-0.2.1-py3-none-any.whl:
Publisher:
release.yml on DazzleML/dazzle-claude-config
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dazzle_claude_config-0.2.1-py3-none-any.whl -
Subject digest:
fb69b83ae6ee3650fe39e19021a1bd0c366a17a73f052650f51ee7b7c206eb2d - Sigstore transparency entry: 2243063828
- Sigstore integration time:
-
Permalink:
DazzleML/dazzle-claude-config@3b3f3e6b093376ae0764c0a630406d996e7897c2 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/DazzleML
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3b3f3e6b093376ae0764c0a630406d996e7897c2 -
Trigger Event:
release
-
Statement type: