Project 34: Configuration Sync - Cross-Machine Settings

Project 34: Configuration Sync - Cross-Machine Settings

Comprehensive Learning Guide Synchronize Claude Code settings, hooks, skills, and preferences across multiple development machines


Table of Contents

  1. Learning Objectives
  2. Deep Theoretical Foundation
  3. Complete Project Specification
  4. Real World Outcome
  5. Solution Architecture
  6. Phased Implementation Guide
  7. Testing Strategy
  8. Common Pitfalls & Debugging
  9. Extensions & Challenges
  10. Resources
  11. Self-Assessment Checklist

Learning Objectives

By completing this project, you will master:

  • Configuration Hierarchy Understanding: Learn how Claude Code’s settings cascade from enterprise to user to project levels, and how to manage each layer effectively.

  • Dotfile Management Patterns: Apply proven patterns from tools like chezmoi, GNU Stow, and yadm to manage Claude Code configuration files across machines.

  • Sync Algorithms: Implement conflict detection, resolution strategies, and change propagation for distributed configuration management.

  • Secure Secret Management: Handle API keys, tokens, and sensitive settings using encryption (git-crypt, age) without exposing them in version control.

  • Change Detection: Build systems that detect configuration drift between machines using hashing and comparison algorithms.

  • Git-Based Synchronization: Use Git as a sync backend with proper branch strategies, merge handling, and atomic updates.


Deep Theoretical Foundation

Claude Code Configuration Hierarchy

Claude Code uses a layered configuration system where settings cascade from general to specific:

Configuration Precedence (highest to lowest):
=============================================

1. ENTERPRISE CONFIG (Highest Priority)
   Location: Set by organization (MDM, group policy)
   Scope: Organization-wide policies
   Override: Cannot be overridden by user
   Example: Approved MCP servers, security policies

2. PROJECT CONFIG
   Location: ./.claude/settings.json, ./CLAUDE.md
   Scope: Single project/repository
   Override: Can override user settings (except enterprise)
   Example: Project-specific hooks, coding conventions

3. USER CONFIG
   Location: ~/.claude/settings.json, ~/.claude/CLAUDE.md
   Scope: All projects on this machine
   Override: Base settings, overridable by project
   Example: Personal preferences, default output style

4. DEFAULT CONFIG (Lowest Priority)
   Location: Built into Claude Code
   Scope: Fallback values
   Override: Always overridable
   Example: Default model, token limits

                    +-------------------+
                    | Enterprise Config |  <-- Cannot override
                    +--------+----------+
                             |
                             v
                    +-------------------+
                    | Project Config    |  <-- ./CLAUDE.md
                    +--------+----------+
                             |
                             v
                    +-------------------+
                    | User Config       |  <-- ~/.claude/
                    +--------+----------+
                             |
                             v
                    +-------------------+
                    | Default Config    |  <-- Built-in
                    +-------------------+

What Lives Where:

~/.claude/                          # USER CONFIGURATION
+-- CLAUDE.md                       # Global instructions (SYNC)
+-- settings.json                   # User preferences (SYNC - encrypted)
+-- hooks/                          # User-defined hooks (SYNC)
|   +-- pre-tool-validator.ts
|   +-- post-session-logger.ts
+-- skills/                         # User-defined skills (SYNC)
|   +-- my-commit-skill.md
|   +-- code-review.md
+-- styles/                         # Output styles (SYNC)
|   +-- concise.md
|   +-- detailed.md
+-- cache/                          # Temporary cache (IGNORE)
|   +-- session-cache.json
+-- sessions/                       # Session history (IGNORE)
|   +-- session-abc123.json
+-- mcp/                            # MCP server configs (SYNC - partial)
    +-- servers.json

./.claude/                          # PROJECT CONFIGURATION
+-- settings.json                   # Project overrides
+-- CLAUDE.md                       # Project instructions (usually in git)

Reference: “Pragmatic Programmer” by Hunt & Thomas, Chapter 3 discusses dotfiles and configuration management principles.


Sync Strategies and Tradeoffs

Different sync strategies have different tradeoffs:

Strategy Comparison:
===================

PUSH/PULL (Git-based)
=====================
Pros:
- Version history
- Branching for experiments
- Works offline
- Mature tooling

Cons:
- Manual sync required
- Merge conflicts possible
- Learning curve for git

Workflow:
Machine A: git add -A && git commit && git push
Machine B: git pull && apply

-----------------------------------------------------

BIDIRECTIONAL SYNC (Dropbox/Syncthing-style)
=============================================
Pros:
- Automatic
- Real-time updates
- No manual intervention

Cons:
- Conflict resolution complex
- Requires always-on service
- Network dependency

Workflow:
Changes automatically propagate when connected

-----------------------------------------------------

LAST-WRITE-WINS
===============
Pros:
- Simple to implement
- No conflicts
- Predictable

Cons:
- Data loss risk
- No merge capability
- Timestamp dependency

Workflow:
Most recent change overwrites all copies

-----------------------------------------------------

RECOMMENDED APPROACH: Push/Pull + Selective Bidirectional
=========================================================
- Use git for version history and intentional sync
- Use timestamps for conflict detection
- Manual resolution with diff preview
- Selective sync (some files always auto-merge)

Reference: “Designing Data-Intensive Applications” by Martin Kleppmann, Chapter 5 covers replication, sync, and conflict resolution in distributed systems.


Secret Management in Sync Systems

Never sync secrets in plaintext. Use encryption:

Secret Management Options:
=========================

1. GIT-CRYPT
   =========
   - Transparent encryption for git repos
   - GPG-based key management
   - Automatic encrypt/decrypt on push/pull

   Setup:
   $ git-crypt init
   $ git-crypt add-gpg-user YOUR_GPG_ID

   .gitattributes:
   settings.json filter=git-crypt diff=git-crypt
   *.secret filter=git-crypt diff=git-crypt

   Result: Files encrypted in repo, decrypted locally

2. AGE ENCRYPTION
   ===============
   - Modern, simple encryption
   - No GPG complexity
   - Key file or passphrase

   Encrypt:
   $ age -r age1... settings.json > settings.json.age

   Decrypt:
   $ age -d -i key.txt settings.json.age > settings.json

3. ENVIRONMENT VARIABLES
   ======================
   - Secrets in .env (not synced)
   - References in config (synced)

   settings.json (synced):
   { "apiKeyRef": "$OPENAI_API_KEY" }

   .env (not synced, per-machine):
   OPENAI_API_KEY=sk-...

4. SECRET MANAGER INTEGRATION
   ==========================
   - 1Password CLI
   - HashiCorp Vault
   - AWS Secrets Manager

   Reference in config:
   { "apiKey": "op://vault/item/field" }

Reference: “Practical Security” by Roman Zabicki, Chapter 7 covers secret management patterns.


Change Detection and Drift

Detect when configurations diverge:

Change Detection Flow:
=====================

             Machine A                    Machine B
           +-----------+                +-----------+
           |  Config   |                |  Config   |
           +-----------+                +-----------+
                |                            |
                v                            v
           +-----------+                +-----------+
           |   Hash    |                |   Hash    |
           | SHA-256   |                | SHA-256   |
           +-----------+                +-----------+
                |                            |
                +------------+---------------+
                             |
                             v
                    +----------------+
                    |    Compare     |
                    |    Hashes      |
                    +----------------+
                             |
              +--------------+--------------+
              |                             |
              v                             v
        +-----------+                +-----------+
        |   Match   |                | Different |
        |   (sync)  |                |  (drift)  |
        +-----------+                +-----------+
                                           |
                                           v
                                   +----------------+
                                   | Conflict       |
                                   | Resolution     |
                                   | - Show diff    |
                                   | - User choice  |
                                   | - Auto-merge   |
                                   +----------------+


Drift Detection Manifest:
========================
{
  "lastSync": "2024-12-22T10:30:00Z",
  "machine": "laptop-a",
  "files": {
    "CLAUDE.md": {
      "hash": "sha256:a1b2c3...",
      "modified": "2024-12-22T09:15:00Z"
    },
    "settings.json": {
      "hash": "sha256:d4e5f6...",
      "modified": "2024-12-22T08:00:00Z"
    },
    "hooks/pre-tool.ts": {
      "hash": "sha256:g7h8i9...",
      "modified": "2024-12-21T16:45:00Z"
    }
  }
}

Complete Project Specification

Functional Requirements

Core Features (Must Have):

Feature Description Priority
Push configuration Upload local config to remote P0
Pull configuration Download remote config to local P0
Conflict detection Identify files changed on both sides P0
Diff preview Show changes before applying P0
Secret encryption Encrypt sensitive files P0
Ignore patterns Exclude cache, sessions, etc. P0
Status command Show sync state and drift P1
History command Show recent sync operations P1
Rollback command Restore previous configuration P2
Multi-machine tracking Track sync state per machine P2

CLI Commands:

# Push local configuration to remote
claude-sync push [--force] [--message "description"]

# Pull remote configuration to local
claude-sync pull [--force] [--dry-run]

# Show sync status
claude-sync status

# Show differences between local and remote
claude-sync diff

# Initialize sync repository
claude-sync init [--backend git|s3|custom]

# Configure sync settings
claude-sync config [key] [value]

# Show sync history
claude-sync log [--limit 10]

# Rollback to previous state
claude-sync rollback [commit-or-timestamp]

Real World Outcome

You’ll have a sync tool:

Example Usage:

# On Machine A - save current config
$ claude-sync push
Syncing Claude Code configuration...

Scanning ~/.claude/ for changes...

Changes to push:
+---------------------------------------+----------+--------+
| File                                  | Size     | Status |
+---------------------------------------+----------+--------+
| ~/.claude/CLAUDE.md                   | 2.3KB    | modified |
| ~/.claude/settings.json               | 1.1KB    | encrypted |
| ~/.claude/hooks/pre-tool-validate.ts  | 856B     | new    |
| ~/.claude/hooks/post-session-log.ts   | 1.2KB    | unchanged |
| ~/.claude/skills/commit-helper.md     | 2.1KB    | modified |
| ~/.claude/styles/concise.md           | 512B     | unchanged |
+---------------------------------------+----------+--------+

Pushing to remote...
Commit: abc1234 "Sync from machine-a at 2024-12-22 14:30"

Configuration synced to remote


# On Machine B - pull config
$ claude-sync pull
Fetching Claude Code configuration...

Changes detected:
+---------------------------------------+----------+--------+
| File                                  | Size     | Action |
+---------------------------------------+----------+--------+
| ~/.claude/CLAUDE.md                   | 2.3KB    | update |
| ~/.claude/hooks/pre-tool-validate.ts  | 856B     | create |
| ~/.claude/skills/commit-helper.md     | 2.1KB    | update |
+---------------------------------------+----------+--------+

Apply changes? [y/n/diff]: diff

--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -10,6 +10,10 @@
 ## Preferences
+
+### New Section Added
+- Always use TypeScript
+- Prefer functional patterns

Apply changes? [y/n]: y

Applying changes...
Decrypting settings.json...

Configuration applied
Your Claude Code is now in sync with machine-a

Conflict Resolution:

$ claude-sync pull
Fetching Claude Code configuration...

CONFLICT detected in ~/.claude/CLAUDE.md

Both local and remote have changes since last sync.

Local changes (machine-b, 2024-12-22 15:00):
  + Added Python coding guidelines

Remote changes (machine-a, 2024-12-22 14:30):
  + Added TypeScript preferences

How do you want to resolve?
  [l] Keep local version
  [r] Use remote version
  [m] Merge both changes (opens editor)
  [d] Show full diff

Choice: m

Opening merge editor...
[After merge completes]

Merged configuration saved.
Push this merged version? [y/n]: y

Pushed merged configuration to remote

Solution Architecture

System Architecture Diagram

+------------------------------------------------------------------+
|                     CLAUDE-SYNC SYSTEM                            |
+------------------------------------------------------------------+
|                                                                   |
|  +------------------+     +------------------+     +-----------+  |
|  |   CLI Interface  |     |   Sync Engine    |     |  Storage  |  |
|  |------------------|     |------------------|     |-----------|  |
|  | - push/pull      |---->| - Change detect  |---->| - Git repo|  |
|  | - status/diff    |     | - Conflict res   |     | - S3 (opt)|  |
|  | - config         |<----| - Encryption     |<----| - Custom  |  |
|  +------------------+     +------------------+     +-----------+  |
|           |                       |                     |         |
|           v                       v                     v         |
|  +------------------+     +------------------+     +-----------+  |
|  |   File Scanner   |     |  Crypto Layer   |     |  Transport |  |
|  |------------------|     |------------------|     |-----------|  |
|  | - Hash files     |     | - age encrypt   |     | - git ops  |  |
|  | - Ignore rules   |     | - decrypt       |     | - http     |  |
|  | - Manifest       |     | - key mgmt      |     | - file cp  |  |
|  +------------------+     +------------------+     +-----------+  |
|                                                                   |
+------------------------------------------------------------------+
                              |
                              v
                    +-------------------+
                    |  ~/.claude/       |
                    |  Configuration    |
                    +-------------------+

Data Flow Diagram

PUSH FLOW:
=========

~/.claude/           Sync Engine              Remote Storage
===========          ===========              ==============

  Files
    |
    v
+----------+
| Scanner  |-------> Manifest
+----------+         (hashes)
    |                   |
    v                   v
+----------+     +-----------+
| Filter   |     | Compare   |-----> Changed files list
| (ignore) |     | with last |
+----------+     +-----------+
    |                   |
    v                   v
+----------+     +-----------+
| Encrypt  |     | Resolve   |-----> Conflict resolution
| secrets  |     | conflicts |
+----------+     +-----------+
    |                   |
    +--------+----------+
             |
             v
      +-----------+
      | Commit &  |-----> git commit && git push
      | Push      |       OR S3 upload
      +-----------+
             |
             v
      +-----------+
      | Update    |-----> Local manifest
      | manifest  |
      +-----------+


PULL FLOW:
=========

Remote Storage       Sync Engine              ~/.claude/
==============       ===========              ===========

  Remote
  manifest
    |
    v
+----------+
| Fetch    |-------> Remote files list
| metadata |
+----------+
    |
    v
+----------+     +-----------+
| Compare  |<----| Local     |
| manifests|     | manifest  |
+----------+     +-----------+
    |
    v
+----------+
| Download |-------> Changed files
| changes  |
+----------+
    |
    v
+----------+     +-----------+
| Decrypt  |---->| Apply     |-----> Updated ~/.claude/
| secrets  |     | changes   |
+----------+     +-----------+
    |
    v
+----------+
| Update   |-------> Local manifest
| manifest |
+----------+

Module Breakdown

src/
+-- index.ts              # CLI entry point
+-- cli/
|   +-- commands/
|   |   +-- push.ts       # Push command handler
|   |   +-- pull.ts       # Pull command handler
|   |   +-- status.ts     # Status command handler
|   |   +-- diff.ts       # Diff command handler
|   |   +-- config.ts     # Config command handler
|   |   +-- init.ts       # Init command handler
|   |   +-- rollback.ts   # Rollback command handler
|   +-- prompts.ts        # Interactive prompts
|   +-- output.ts         # Formatted output
|
+-- core/
|   +-- scanner.ts        # File scanning and hashing
|   +-- manifest.ts       # Manifest management
|   +-- diff.ts           # Diff generation
|   +-- conflict.ts       # Conflict detection/resolution
|   +-- merge.ts          # File merging logic
|
+-- storage/
|   +-- backend.ts        # Storage backend interface
|   +-- git-backend.ts    # Git-based storage
|   +-- s3-backend.ts     # S3-based storage (optional)
|   +-- local-backend.ts  # Local folder (testing)
|
+-- crypto/
|   +-- encryption.ts     # Encryption/decryption
|   +-- keys.ts           # Key management
|   +-- age.ts            # age encryption wrapper
|
+-- config/
|   +-- settings.ts       # Sync settings
|   +-- ignore.ts         # Ignore patterns
|   +-- paths.ts          # Path constants
|
+-- types/
    +-- index.ts          # TypeScript types
    +-- manifest.ts       # Manifest types
    +-- config.ts         # Config types

Phased Implementation Guide

Phase 1: Scanner and Manifest (Day 1)

Goal: Scan ~/.claude/ and generate file manifest with hashes.

Milestone: Running scanner outputs JSON manifest of all sync-able files.

Tasks:

  1. Project Setup
    mkdir claude-sync && cd claude-sync
    npm init -y
    npm install commander chalk ora glob crypto
    npm install -D typescript @types/node vitest
    npx tsc --init
    
  2. Create Scanner (src/core/scanner.ts)
    import { createHash } from 'crypto';
    import { readFileSync, statSync } from 'fs';
    import { glob } from 'glob';
    
    export interface FileEntry {
      path: string;
      hash: string;
      size: number;
      modified: string;
    }
    
    export async function scanDirectory(dir: string): Promise<FileEntry[]> {
      const ignorePatterns = ['cache/**', 'sessions/**', '*.log'];
      const files = await glob('**/*', {
        cwd: dir,
        ignore: ignorePatterns,
        nodir: true
      });
    
      return files.map(file => {
        const fullPath = `${dir}/${file}`;
        const content = readFileSync(fullPath);
        const stats = statSync(fullPath);
    
        return {
          path: file,
          hash: createHash('sha256').update(content).digest('hex'),
          size: stats.size,
          modified: stats.mtime.toISOString()
        };
      });
    }
    
  3. Create Manifest Manager (src/core/manifest.ts)
    • Load/save manifest JSON
    • Compare two manifests
    • Identify added/modified/deleted files
  4. Test Scanner
    npx tsx src/core/scanner.ts
    

Success Criteria: Scanner correctly identifies files and generates hashes.


Phase 2: Git Backend (Day 2)

Goal: Push and pull files to/from a git repository.

Milestone: Files sync between test directories via git.

Tasks:

  1. Create Git Backend (src/storage/git-backend.ts)
    import { execSync } from 'child_process';
    
    export class GitBackend {
      constructor(private repoPath: string) {}
    
      async init(): Promise<void> {
        execSync('git init', { cwd: this.repoPath });
      }
    
      async push(files: FileEntry[], message: string): Promise<string> {
        // Stage files
        for (const file of files) {
          execSync(`git add "${file.path}"`, { cwd: this.repoPath });
        }
    
        // Commit
        execSync(`git commit -m "${message}"`, { cwd: this.repoPath });
    
        // Push
        execSync('git push origin main', { cwd: this.repoPath });
    
        // Return commit hash
        return execSync('git rev-parse HEAD', { cwd: this.repoPath })
          .toString()
          .trim();
      }
    
      async pull(): Promise<void> {
        execSync('git pull origin main', { cwd: this.repoPath });
      }
    }
    
  2. Create Storage Interface (src/storage/backend.ts)
    • Define common interface for all backends
    • Allow swapping git for S3 later
  3. Implement Push Command (src/cli/commands/push.ts)
    • Scan files
    • Generate manifest
    • Copy to git repo
    • Commit and push
  4. Implement Pull Command (src/cli/commands/pull.ts)
    • Pull from git
    • Compare manifests
    • Copy changed files to ~/.claude/

Success Criteria: Push/pull works between two directories.


Phase 3: Encryption (Day 3)

Goal: Encrypt sensitive files before syncing.

Milestone: settings.json is encrypted in git but readable locally.

Tasks:

  1. Create Crypto Module (src/crypto/encryption.ts)
    import { execSync } from 'child_process';
    
    export function encryptFile(inputPath: string, outputPath: string, publicKey: string): void {
      execSync(`age -r ${publicKey} -o "${outputPath}" "${inputPath}"`);
    }
    
    export function decryptFile(inputPath: string, outputPath: string, keyPath: string): void {
      execSync(`age -d -i "${keyPath}" -o "${outputPath}" "${inputPath}"`);
    }
    
  2. Define Encryption Rules
    // .claude-sync/config.json
    {
      "encrypt": [
        "settings.json",
        "*.secret",
        "mcp/servers.json"
      ],
      "publicKey": "age1...",
      "keyFile": "~/.claude-sync/key.txt"
    }
    
  3. Integrate with Push/Pull
    • On push: encrypt matching files before commit
    • On pull: decrypt after download
  4. Key Setup Command (src/cli/commands/init.ts)
    • Generate age key pair
    • Store private key locally (never sync)
    • Store public key in config (sync)

Success Criteria: Encrypted files in git, decrypted locally.


Phase 4: Conflict Resolution (Day 4)

Goal: Detect and resolve conflicts when both sides change.

Milestone: User can choose how to resolve conflicts interactively.

Tasks:

  1. Create Conflict Detector (src/core/conflict.ts)
    export interface Conflict {
      path: string;
      localHash: string;
      remoteHash: string;
      baseHash: string;  // Last synced version
      localModified: string;
      remoteModified: string;
    }
    
    export function detectConflicts(
      local: Manifest,
      remote: Manifest,
      base: Manifest
    ): Conflict[] {
      const conflicts: Conflict[] = [];
    
      for (const [path, localEntry] of Object.entries(local.files)) {
        const remoteEntry = remote.files[path];
        const baseEntry = base.files[path];
    
        if (remoteEntry && baseEntry) {
          // Both changed since base - conflict!
          if (localEntry.hash !== baseEntry.hash &&
              remoteEntry.hash !== baseEntry.hash &&
              localEntry.hash !== remoteEntry.hash) {
            conflicts.push({
              path,
              localHash: localEntry.hash,
              remoteHash: remoteEntry.hash,
              baseHash: baseEntry.hash,
              localModified: localEntry.modified,
              remoteModified: remoteEntry.modified
            });
          }
        }
      }
    
      return conflicts;
    }
    
  2. Create Resolution Prompts (src/cli/prompts.ts)
    • Show conflict details
    • Options: keep local, use remote, merge, show diff
    • Open merge editor if requested
  3. Implement Diff Display (src/core/diff.ts)
    • Use diff library for side-by-side comparison
    • Color-coded output
  4. Update Pull Command
    • Check for conflicts before applying
    • Prompt for resolution
    • Apply resolutions

Success Criteria: Conflicts detected and resolved interactively.


Phase 5: CLI Polish (Day 5)

Goal: Complete CLI with status, history, and rollback.

Milestone: Production-ready sync tool.

Tasks:

  1. Status Command (src/cli/commands/status.ts)
    • Show local changes since last sync
    • Show remote changes (requires fetch)
    • Show sync state (in sync, ahead, behind, diverged)
  2. History Command (src/cli/commands/log.ts)
    • Show recent sync operations
    • Include machine name, timestamp, files changed
  3. Rollback Command (src/cli/commands/rollback.ts)
    • List available restore points
    • Restore specific version
    • Confirm before applying
  4. Polish
    • Progress spinners during operations
    • Colored, formatted output
    • Helpful error messages
    • –help with examples
  5. Create bin Script
    {
      "bin": {
        "claude-sync": "./dist/index.js"
      }
    }
    

Success Criteria: All commands work smoothly with good UX.


Testing Strategy

Unit Tests: Scanner

// tests/core/scanner.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdirSync, writeFileSync, rmSync } from 'fs';
import { scanDirectory } from '../../src/core/scanner';

describe('scanner', () => {
  const testDir = '/tmp/claude-sync-test';

  beforeEach(() => {
    mkdirSync(testDir, { recursive: true });
  });

  afterEach(() => {
    rmSync(testDir, { recursive: true });
  });

  it('scans files and generates hashes', async () => {
    writeFileSync(`${testDir}/file1.txt`, 'content1');
    writeFileSync(`${testDir}/file2.txt`, 'content2');

    const entries = await scanDirectory(testDir);

    expect(entries).toHaveLength(2);
    expect(entries[0].hash).toBeDefined();
    expect(entries[0].hash).toHaveLength(64); // SHA-256 hex
  });

  it('ignores specified patterns', async () => {
    mkdirSync(`${testDir}/cache`, { recursive: true });
    writeFileSync(`${testDir}/file.txt`, 'content');
    writeFileSync(`${testDir}/cache/temp.txt`, 'temp');

    const entries = await scanDirectory(testDir);

    expect(entries).toHaveLength(1);
    expect(entries[0].path).toBe('file.txt');
  });

  it('detects file modifications via hash change', async () => {
    writeFileSync(`${testDir}/file.txt`, 'original');
    const before = await scanDirectory(testDir);

    writeFileSync(`${testDir}/file.txt`, 'modified');
    const after = await scanDirectory(testDir);

    expect(before[0].hash).not.toBe(after[0].hash);
  });
});

Integration Tests: Sync Flow

// tests/integration/sync.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { SyncEngine } from '../../src/core/sync-engine';

describe('sync flow', () => {
  let engine: SyncEngine;
  let localDir: string;
  let remoteDir: string;

  beforeEach(() => {
    localDir = '/tmp/claude-sync-local';
    remoteDir = '/tmp/claude-sync-remote';
    engine = new SyncEngine(localDir, remoteDir);
  });

  it('pushes new files to remote', async () => {
    writeFileSync(`${localDir}/new-file.txt`, 'content');

    const result = await engine.push();

    expect(result.pushed).toContain('new-file.txt');
    expect(existsSync(`${remoteDir}/new-file.txt`)).toBe(true);
  });

  it('pulls new files from remote', async () => {
    writeFileSync(`${remoteDir}/remote-file.txt`, 'remote content');

    const result = await engine.pull();

    expect(result.pulled).toContain('remote-file.txt');
    expect(existsSync(`${localDir}/remote-file.txt`)).toBe(true);
  });

  it('detects conflicts', async () => {
    // Setup base state
    writeFileSync(`${localDir}/shared.txt`, 'base');
    await engine.push();
    await engine.pull();

    // Modify both sides
    writeFileSync(`${localDir}/shared.txt`, 'local change');
    writeFileSync(`${remoteDir}/shared.txt`, 'remote change');

    const result = await engine.pull({ dryRun: true });

    expect(result.conflicts).toContain('shared.txt');
  });
});

Common Pitfalls & Debugging

Pitfall 1: Ignoring Machine-Specific Files

Symptom: Sync breaks on different machines due to absolute paths.

Bad:

// settings.json contains absolute paths
{
  "pythonPath": "/Users/alice/anaconda/bin/python"
}

Good:

// Use environment variables or relative paths
{
  "pythonPath": "${PYTHON_PATH:-python}"
}

Fix: Add machine-specific patterns to ignore list or use path variables.


Pitfall 2: Encrypting Key File

Symptom: Can’t decrypt after sync because key was encrypted.

Bad:

// .claude-sync/config.json
{
  "encrypt": ["*.json"]  // Encrypts the key reference!
}

Good:

// Be explicit about what to encrypt
{
  "encrypt": ["settings.json", "mcp/servers.json"],
  "neverEncrypt": ["config.json", "manifest.json"]
}

Pitfall 3: Git Conflicts in Binary Files

Symptom: Git can’t merge binary or encrypted files.

Bad:

git pull  # Fails with merge conflict in encrypted file

Good:

# Configure git to use "ours" or "theirs" for encrypted files
# .gitattributes
*.age merge=binary

Or handle at application level by always preferring one side and prompting user.


Pitfall 4: Lost Changes on Force Push

Symptom: Remote changes lost after force push.

Bad:

claude-sync push --force  # Overwrites remote without checking

Good:

# Always fetch and check first
claude-sync push
# Shows: "Remote has changes not in local. Pull first or use --force"

Pitfall 5: Timestamp Skew

Symptom: Sync decisions wrong due to clock differences between machines.

Debug:

# Check system time
date

# Check file timestamps
stat ~/.claude/CLAUDE.md

Fix: Use git commit timestamps (server-side) rather than file modification times for ordering decisions.


The Interview Questions They’ll Ask

Prepare to answer these:

  1. “How would you handle a sync conflict where both machines modified the same hook?”
    • Detect via three-way comparison (local, remote, base)
    • Show diff to user
    • Options: keep local, use remote, merge manually
    • For code files, attempt automatic merge with git merge-file
  2. “What’s your strategy for storing secrets that need to sync?”
    • Never store plaintext secrets in sync repo
    • Use age encryption with public/private key pair
    • Private key stays local, never synced
    • Public key in config for adding new machines
    • Environment variables for truly machine-specific secrets
  3. “How do you detect configuration drift?”
    • Hash all synced files on each machine
    • Store manifest with last-sync state
    • Compare current hashes to manifest
    • Detect: local changes, remote changes, conflicts
    • Optional: periodic background check
  4. “What happens if sync fails partway through?”
    • Atomic operations where possible
    • Transaction log of pending changes
    • Rollback capability to last good state
    • Lock file to prevent concurrent syncs
    • Clear error messages about what failed
  5. “How would you roll back a bad configuration sync?”
    • Git history provides all versions
    • List available restore points (commits)
    • Preview what would change
    • Apply restore with confirmation
    • Update manifest to prevent immediate re-sync

Hints in Layers

Hint 1: Use Git The simplest approach is a private git repo for your dotfiles. ~/.claude/ becomes managed by git or symlinked via stow.

Hint 2: Hash for Changes Hash file contents to detect changes without transferring entire files. SHA-256 is fast and collision-resistant.

Hint 3: Encryption Layer Use age or git-crypt to encrypt sensitive files before committing. Both integrate well with git workflows.

Hint 4: Ignore Patterns Some files should never sync: cached data, machine-specific paths, temporary files, session history.


Books That Will Help

Topic Book Chapter
Dotfile management “Pragmatic Programmer” by Hunt & Thomas Ch. 3: Basic Tools
Sync algorithms “Designing Data-Intensive Applications” by Kleppmann Ch. 5: Replication
Secret management “Practical Security” by Roman Zabicki Ch. 7: Secrets
Git internals “Pro Git” by Scott Chacon Ch. 10: Git Internals
Change detection “Algorithms” by Sedgewick Ch. 5: Hashing

Extensions & Challenges

Extension 1: Real-Time Sync

Use file system watchers to sync immediately on change:

import { watch } from 'fs';

watch(claudeDir, { recursive: true }, (event, filename) => {
  if (!isIgnored(filename)) {
    debounce(() => syncFile(filename), 1000);
  }
});

Extension 2: Multi-Backend Support

Add S3, Dropbox, or custom server backends:

interface StorageBackend {
  push(files: FileEntry[]): Promise<void>;
  pull(): Promise<FileEntry[]>;
  getManifest(): Promise<Manifest>;
}

class S3Backend implements StorageBackend { ... }
class DropboxBackend implements StorageBackend { ... }

Extension 3: Team Sync

Extend to sync shared team configurations:

claude-sync team add teammate@email.com
claude-sync team share hooks/code-review.ts
claude-sync team pull  # Get team shared configs

Self-Assessment Checklist

Conceptual Understanding

  • Can you explain the Claude Code configuration hierarchy?
  • Can you describe push/pull vs bidirectional sync tradeoffs?
  • Can you explain how to detect conflicts in distributed systems?
  • Can you list what files should and shouldn’t be synced?
  • Can you explain encryption options for secrets?

Implementation Skills

  • Can you scan a directory and generate file hashes?
  • Can you implement git-based push and pull?
  • Can you encrypt/decrypt files with age?
  • Can you detect and present conflicts?
  • Can you create a user-friendly CLI?

Code Quality

  • Is your code organized into logical modules?
  • Are storage backends swappable?
  • Is error handling comprehensive?
  • Are operations atomic where needed?
  • Can another developer add a new backend?

The Core Question You’ve Answered

“How do you maintain consistent Claude Code behavior across all your development machines?”

When you have a laptop, desktop, and work machine, keeping hooks, skills, and preferences in sync manually is error-prone. This project automates that synchronization.

By building this sync tool, you have mastered:

  1. Configuration Management: Understanding the hierarchy and what to sync
  2. Distributed Sync: Detecting changes, conflicts, and resolving them
  3. Secret Handling: Keeping sensitive data secure in transit
  4. CLI Development: Building tools developers want to use

You can now maintain a consistent Claude Code experience everywhere you work.


Project Guide Version 1.0 - December 2025