Copies your live ~/.claude/ configuration into this repo, applying anonymization rules to strip personal data before committing.
cp scripts/anonymization.example.yaml scripts/anonymization.yamlThat is the whole setup if you have uv.
sync.py carries PEP 723 inline metadata
and a #!/usr/bin/env -S uv run --script shebang, so running it as
./scripts/sync.py builds its own environment on first run and reuses it
afterwards. There is no dependency step to forget and nothing installed into a
system Python.
scripts/requirements.txt is still maintained for this case:
pip install -r scripts/requirements.txt
python3 scripts/sync.py --dry-run # invoke through python3, bypassing the shebangIf that pip install is refused with externally-managed-environment, your
Python is PEP 668-managed — Homebrew's is, and so are most distro packages.
Install into the user site instead, which leaves the managed installation
untouched:
python3 -m pip install --user --break-system-packages -r scripts/requirements.txtDo not skip the step and hope. sync.py imports yaml at module scope, so a
missing PyYAML is an immediate ModuleNotFoundError and the sync does not run
at all — which also disables the sync step inside /cleanup, where nobody is
watching the output. That failure mode is the reason the uv path is preferred:
it has no step to skip.
The dependency is therefore declared twice — in requirements.txt and in
sync.py's inline block. Keep them in step. There is exactly one, which is what
makes that affordable.
Edit scripts/anonymization.yaml with your real data — app names, URLs, domains, people. The file is gitignored and never committed.
# Preview what would happen (no files written)
./scripts/sync.py --dry-run
# Run the sync
./scripts/sync.py
# Audit existing repo files for personal data leaks
./scripts/sync.py --audit-only
# Use a different source directory
./scripts/sync.py --source /path/to/claude-configThese read python scripts/sync.py until 2026-08-17. That form was already
broken on the machine this repo is synced from — there is no bare python on
it, only python3 — and it bypasses the shebang, which is now what selects the
interpreter.
uv run --with pytest --with PyYAML pytest scripts/ -qpytest is deliberately absent from requirements.txt: that file is the
runtime dependency list for people running the sync, and adding a test-only
package would make every such person install it.
Everything under a synced root is generated. commands/, agents/,
skills/, hooks/ and claude-scripts/ are an image of ~/.claude/; the live
file is the only input. Editing the repo's copy feels like it works — the file
changes, the tests pass, the diff reads right — and the next sync reverts it,
because nothing ever read it.
The sync refuses to do that silently. Before writing anything it compares each
destination against both the content it is about to write and the content
committed at HEAD, and stops with exit 2 if a file matches neither, which is
what a hand edit looks like. Dirtiness alone is not the signal: a real sync
leaves every destination dirty until you review and commit, and re-running must
stay free. --allow-dirty overrides it, for the one honest case — deliberately
throwing those edits away.
So: make the change in ~/.claude/, then re-run. The owner-maintained files
(README.md, docs/, hooks/README.md, claude-scripts/README.md,
root-level files) are the exception — they are edited here and never synced.
Some live files are mostly publishable with a section that is not. A region
between SYNC-PRIVATE:BEGIN and SYNC-PRIVATE:END is dropped from the
published copy, markers included:
Public sentence that stays.
<!-- SYNC-PRIVATE:BEGIN -->
A paragraph that never reaches the repo.
<!-- SYNC-PRIVATE:END -->A pair that opens and closes on one line cuts just that span, so a single clause can be removed from the middle of a sentence without reflowing the paragraph. The tokens work in any comment syntax — a line only has to contain one — and an HTML-comment wrapper is consumed along with the marker.
Two properties worth knowing. The markers live in the live source, never in the repo, which is the same rule as above: a marker added to the repo's copy does nothing at all. And unbalanced markers are fatal — an unclosed BEGIN would truncate a file invisibly, a stray END would publish everything above it, so the sync validates every source up front and aborts before writing anything. Every run reports how many regions it removed and from which files, because a deletion nobody is told about is indistinguishable from a file that was never marked.
Use skip instead when the whole file is private; markers are for a private
section inside a public file.
One consequence: a live file that documents this mechanism cannot spell the
marker pair out contiguously, because the sync would read it as a real marker.
The failure is loud and safe — an unbalanced pair aborts the run before
anything is written — but it is why ~/.claude/commands/sync-setup.md refers to
them obliquely and this file, which is owner-maintained and never synced, can
show them in full.
- Reads
anonymization.yamlfor replacement rules - Copies files from
~/.claude/matching thefile_mappatterns - Strips
SYNC-PRIVATEregions, before any replacement runs — so a private paragraph cannot be laundered into something publishable by a rule that happens to match it - Applies exact string replacements (longest first, to avoid partial matches)
- Applies regex patterns for catch-all rules (paths, emails)
- Regenerates the
docs/workflow-guide.htmlDATA arrays (commands, agents, skills, hooks) from live config viagenerate_workflow_guide.py, preserving the hand-written French and English descriptions and flagging genuinely new entries - Prunes orphaned repo files under synced roots (
commands/,agents/,skills/,hooks/,claude-scripts/) whose live source has disappeared - Runs an audit: greps all output files (
.md,.html,.yml,.yaml,.sh,.json) for patterns that should not survive. Gitignored paths are skipped — they can never be pushed — and so are the owner-maintainedREADME.mdandLICENSE, whose real name and links are deliberate. Without those two exclusions the gate is red on a clean tree, which makes it useless - Prints a summary and
git diff --stat— you review and commit manually
The anonymization.yaml has four sections:
replacements: Exact string replacements. Add your real app names, URLs, domains, and people here. Longer strings are applied first automatically.patterns: Regex patterns for catch-all rules (e.g., home directory paths).audit_patterns: Patterns to grep for after sync — anything matching is a potential leak.skip: Directories/files in~/.claude/to ignore entirely. Also the place to protect an owner-maintained repo file that happens to sit under a synced root (hooks/README.md), since afile_mapglob would otherwise make it a sync destination and overwrite it.file_map: What to copy and where to put it.
The example config uses "Option B" — public apps (already on GitHub) keep their real names, while private apps get descriptive placeholders like my-budget-app. This makes the repo more readable for public apps while protecting private projects.