Dotclaude: A Claude Code CLI Config Sync Across Computers

A sync system that mirrors an AI coding assistant's skills, hooks, and settings across machines via a private Git repo, gating anything that auto-executes behind explicit human review.

Claude CodeBashGitGitHub
ai-skillsautomationdeveloper-experiencegreenfield
Lab Build·2026-08-09

The Problem

I run an AI coding assistant across two machines, each with its own local configuration: custom skills (reusable workflows), hooks (shell scripts that fire automatically on session start, session stop, and tool use), and permission settings. Left alone, the two machines drift — a skill improved on one goes stale on the other, and there’s no record of what changed or when. The harder problem is that some of this configuration is not inert content: hooks and settings execute code automatically the moment a session starts. Any sync mechanism that blindly mirrors one machine onto another is also a mechanism for silently pushing unreviewed, auto-executing code onto a second device.

The Solution

A version-controlled sync system built around a private Git repository, with a single design principle: configuration is only as trustworthy as its blast radius. Inert content — skill instructions the assistant reads and follows — syncs and applies automatically. Anything that runs without being asked — hooks and top-level settings — is diffed and surfaced, but never auto-applied; a human has to read the diff and explicitly approve it. A companion check runs once a day and tells the user, in plain language, when the other machine has updates waiting, without ever running them itself.

Outcome & Impact

Sixteen skills and six auto-executing hooks now stay consistent across two machines with zero manual copying, while nothing that runs automatically has ever been applied without a human reading the diff first. The daily check surfaces drift the same day it happens instead of it being discovered weeks later mid-task. The design was stress-tested directly: a real security review of the setup flagged that an earlier version of this sync auto-applied hook and settings changes unreviewed, and the risk-tiered split described above was the fix — validating that the split wasn’t theoretical caution but closed a finding that would otherwise have let a bad upstream commit execute unreviewed on a second machine.

My Role & Contributions

Aspect Detail
Role Sole designer and implementer
Team size 1
Timeline Built and iterated over several sessions; actively maintained
Scope Sync architecture, risk-tiering design, shell scripting, hook design, conflict recovery
Key decisions The auto-apply/require-review split; excluding personal memory and machine-local permissions from sync entirely; making the daily update check a passive notice rather than an actionable prompt

Technical Overview

   Machine A (~/.claude)                Machine B (~/.claude)
   ┌──────────────────────┐                   ┌─────────────────────┐
   │ skills/              │                   │ skills/.            │
   │ hooks/*.sh           │                   │ hooks/*.sh          │
   │ settings.json        │                   │ settings.json       │
   │ settings.local.json. │    (push-only)    │ settings.local.json │
   └─────────┬────────────┘                   └─────────┬───────────┘
             │ push (rsync + git commit/push)           │ push
             ▼                                          ▼
        ┌───────────────────────────────────────────────────┐
        │            private Git repository                 │
        │   skills/   hooks/   settings.json   sync.log     │
        └───────────────────────────────────────────────────┘
             │ pull                                    │ pull
             ▼                                         ▼
   skills/ → auto-applied                    skills/ → auto-applied
   hooks/, settings.json → diffed, held for human review

The core is a single idempotent shell script with three modes. Push mirrors the local skills directory, hook scripts, and settings files into the repo, commits with a timestamped diffstat summary, and pushes — deterministic enough to run unattended on a weekly schedule. Pull fetches the remote, auto-applies skill changes immediately since they’re just instructions the assistant reads, but for hooks and settings.json it only prints a diff — it deliberately stops short of writing anything. Apply-hooks is a separate, explicitly-named third mode that performs that write, meant to be run by a human after reading the diff pull produced.

A SessionStart hook checks once a day whether the remote has moved ahead of the local clone and, if so, surfaces a single fixed notice telling the user to run the update command themselves — it never triggers the update. Per-machine permission settings are excluded from pull entirely, since applying another machine’s local permission grants would silently change what commands can run unattended on this one.

Challenges & Key Decisions

Drawing the line between “content” and “code”

The central design question was where to put the human-in-the-loop gate. Treating everything as equally safe to auto-sync is simplest but means a compromised or just-plain-wrong upstream commit runs unreviewed the instant the other machine syncs. Treating everything as equally risky and requiring manual review for all of it defeats the purpose of automating the sync at all. The resolution was to tier by what actually executes: skills are read by the assistant as instructions and can misbehave only within the assistant’s own guardrails; hooks and settings run as real shell code on session start, stop, and every tool call, with no such guardrail. That distinction, not a blanket policy, is what determines what auto-applies.

Recovering from repo divergence without losing the safety gate

The sync log itself, written by every run, occasionally caused the two machines’ local commit histories to diverge — one machine would fail a pull, append an error line to the log, and that log-only commit would then conflict with the other machine’s legitimate push. The fix had to resolve the divergence without silently accepting incoming hook or settings changes as a side effect: diagnosing the conflict as confined to the log file, then merging with a strategy scoped only to that file, kept the human-review requirement for actual logic changes intact instead of overwriting it as a byproduct of conflict resolution.

Building, then removing, a convenience wrapper

A one-command wrapper was added to fold the two-step pull-then-apply flow into a single invocation with an inline confirmation prompt, to reduce the friction of the manual step. It was later removed in favor of keeping the split explicit at the shell level — worth noting as a case where reducing friction and preserving a deliberate pause were in tension, and the deliberate pause won.

Lessons Learned

  • Not all configuration carries the same risk, and sync policy should reflect that. Treating “config” as one undifferentiated bucket either over-restricts safe content or under-restricts dangerous content; splitting by what actually executes gets both right at once.
  • A convenience feature that erodes a deliberate safety pause isn’t actually convenient. The one-command wrapper was useful engineering but wrong for this system, and removing it was the correct call once that tension surfaced.
  • Automated logging can itself become a source of merge conflicts. A sync tool’s own audit trail needs the same conflict-resistance thinking as the content it’s syncing, or the safety net becomes the thing that breaks.