|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +`claude-code-sync` is a CLI tool for syncing Claude Code configs (`~/.claude/`) across machines via GitHub with age encryption. Core functionality: selective encryption (sensitive files like `settings.json` encrypted, non-sensitive like `CLAUDE.md` plain text), cross-platform single binary (Go), native age encryption (no external CLI). |
| 8 | + |
| 9 | +## Build Commands |
| 10 | + |
| 11 | +```bash |
| 12 | +# Build for current platform |
| 13 | +go build -o claude-code-sync.exe ./cmd/claude-code-sync/ # Windows |
| 14 | +go build -o claude-code-sync ./cmd/claude-code-sync/ # Unix |
| 15 | + |
| 16 | +# Build with version (for development) |
| 17 | +go build -ldflags="-X main.version=0.2.1-dev" -o claude-code-sync.exe ./cmd/claude-code-sync/ |
| 18 | + |
| 19 | +# Install to system PATH |
| 20 | +cp claude-code-sync.exe C:\Users\<user>\.local\bin\ # Windows |
| 21 | +sudo mv claude-code-sync /usr/local/bin/ # Unix |
| 22 | + |
| 23 | +# Run tests |
| 24 | +go test ./... |
| 25 | +go test -v ./internal/config/ # Specific package |
| 26 | + |
| 27 | +# Format and lint |
| 28 | +go fmt ./... |
| 29 | +go vet ./... |
| 30 | +``` |
| 31 | + |
| 32 | +## Architecture |
| 33 | + |
| 34 | +### Entry Point & Version Injection |
| 35 | + |
| 36 | +- `cmd/claude-code-sync/main.go`: Entry point with `var version = "dev"` (overridden by goreleaser's `-ldflags "-X main.version={{.Version}}"`) |
| 37 | +- Calls `cmd.SetVersion(version)` then `cmd.Execute()` |
| 38 | + |
| 39 | +### Core Layers |
| 40 | + |
| 41 | +1. **CLI Layer** (`internal/cmd/`): Cobra commands (root.go registers all subcommands) |
| 42 | +2. **Business Logic**: Config patterns (`internal/config/`), crypto (`internal/crypto/`), git wrapper (`internal/git/`), sync engine (`internal/sync/`) |
| 43 | +3. **External**: Shells out to `git` CLI, native age lib for encryption |
| 44 | + |
| 45 | +### Key Architectural Decisions |
| 46 | + |
| 47 | +**Git wrapper vs go-git**: Shells out to `git` CLI for simplicity (users already have it, easier debugging). All git ops in `internal/git/git.go` with `exec.Command("git", "-C", repoDir, args...)`. |
| 48 | + |
| 49 | +**Native age encryption**: Uses `filippo.io/age` Go library directly (no external `age` CLI). Streaming I/O via `age.Encrypt()`/`age.Decrypt()` - no full files in memory. |
| 50 | + |
| 51 | +**Pattern matching**: Two systems: |
| 52 | +- **Encrypt patterns** (`internal/config/config.go:ShouldEncrypt`): Filename match (e.g., `settings.json`) or path wildcard (e.g., `skills/*/resources/*`) |
| 53 | +- **Exclude patterns** (`internal/config/config.go:ShouldExclude`): Directory prefix (e.g., `plans/` matches `plans/foo/bar.md`) or filename wildcard (e.g., `*.log`) |
| 54 | + |
| 55 | +**Data flow (Push)**: |
| 56 | +1. Walk `~/.claude/` (`internal/sync/sync.go`) |
| 57 | +2. For each file: check exclude → check encrypt → copy plain or encrypt to `~/.claude-sync/repo/` |
| 58 | +3. Generate `.sync-manifest` (SHA256 checksums) |
| 59 | +4. `git add -A && git commit && git push` (`internal/git/git.go`) |
| 60 | + |
| 61 | +**Data flow (Pull)**: |
| 62 | +1. Backup current `~/.claude/` to `~/.claude-sync/backups/TIMESTAMP/` |
| 63 | +2. `git pull` (retries with `--allow-unrelated-histories` if needed) |
| 64 | +3. Walk repo, decrypt `.age` files or copy plain to `~/.claude/` |
| 65 | +4. Verify SHA256 checksums |
| 66 | + |
| 67 | +### File Locations |
| 68 | + |
| 69 | +``` |
| 70 | +~/.claude-sync/ |
| 71 | +├── config # Repo URL (plain text) |
| 72 | +├── identity.key # age private key (chmod 600) |
| 73 | +├── backups/ # Auto backups before pull |
| 74 | +└── repo/ # Git clone |
| 75 | + ├── CLAUDE.md # Plain |
| 76 | + ├── commands/, agents/, skills/ # Plain (except skills/*/resources/* encrypted) |
| 77 | + ├── settings.json.age # Encrypted |
| 78 | + └── .sync-manifest # SHA256 checksums |
| 79 | +``` |
| 80 | + |
| 81 | +### Patterns (internal/config/config.go) |
| 82 | + |
| 83 | +**DefaultEncryptPatterns**: |
| 84 | +```go |
| 85 | +"settings.json", "settings.local.json", "claude.json", |
| 86 | +".credentials.json", "client_secret_*.json", |
| 87 | +"skills/*/resources/*" |
| 88 | +``` |
| 89 | + |
| 90 | +**DefaultExcludePatterns** (note specific exclusions): |
| 91 | +```go |
| 92 | +"plans", "projects", "local", "statsig", "todos", "debug", |
| 93 | +"file-history", "ide", "shell-snapshots", "telemetry", "sessionStorage", |
| 94 | +"plugins/cache", "plugins/marketplaces", // But NOT plugins/ itself |
| 95 | +"history.jsonl", "stats-cache.json", |
| 96 | +"*.log", "*.tmp", "*.cache", "*.local-backup-*", ".git" |
| 97 | +``` |
| 98 | + |
| 99 | +## Critical Implementation Details |
| 100 | + |
| 101 | +### Error Handling |
| 102 | + |
| 103 | +Return errors up the stack (don't panic), wrap with context: `fmt.Errorf("context: %w", err)`. User-facing errors via `logError()` in `internal/cmd/root.go`. |
| 104 | + |
| 105 | +### Cross-Platform Paths |
| 106 | + |
| 107 | +`toUnixPath()` in `internal/cmd/init.go` converts backslashes to forward slashes for Git Bash compatibility. Always use `filepath.ToSlash()` when displaying paths. |
| 108 | + |
| 109 | +### Git Operations |
| 110 | + |
| 111 | +All git commands in `internal/git/git.go`: |
| 112 | +- `run()`: Executes git, returns stderr on error |
| 113 | +- `Pull()`: Auto-retries with `--allow-unrelated-histories` if unrelated histories error |
| 114 | +- `IsValidRepoURL()`, `CheckRemote()`: Validate before cloning |
| 115 | + |
| 116 | +### Encryption |
| 117 | + |
| 118 | +- Key generation: `age.GenerateX25519Identity()` returns identity with both private key and public recipient |
| 119 | +- Public key derivation: `identity.Recipient().String()` |
| 120 | +- Key file format: Comment lines + `AGE-SECRET-KEY-...` |
| 121 | +- Encrypt: Stream `io.Copy(age.Encrypt(out, recipient), in)` |
| 122 | +- Decrypt: Stream `io.Copy(out, age.Decrypt(in, identity))` |
| 123 | + |
| 124 | +## Adding New Commands |
| 125 | + |
| 126 | +1. Create `internal/cmd/newcommand.go`: |
| 127 | +```go |
| 128 | +var newCmd = &cobra.Command{ |
| 129 | + Use: "new", |
| 130 | + Short: "Description", |
| 131 | + RunE: runNew, |
| 132 | +} |
| 133 | + |
| 134 | +func runNew(cmd *cobra.Command, args []string) error { |
| 135 | + // Implementation |
| 136 | +} |
| 137 | +``` |
| 138 | + |
| 139 | +2. Register in `internal/cmd/root.go` init(): |
| 140 | +```go |
| 141 | +rootCmd.AddCommand(newCmd) |
| 142 | +``` |
| 143 | + |
| 144 | +## Release Process |
| 145 | + |
| 146 | +Triggered by git tag push: |
| 147 | +```bash |
| 148 | +git tag -a v0.3.0 -m "Release v0.3.0" |
| 149 | +git push origin v0.3.0 |
| 150 | +``` |
| 151 | + |
| 152 | +GitHub Actions (`.github/workflows/release.yaml`) runs goreleaser: |
| 153 | +- Builds for Windows/macOS/Linux (amd64/arm64) |
| 154 | +- Injects version via `-ldflags "-X main.version={{.Version}}"` |
| 155 | +- Creates GitHub release |
| 156 | +- Updates Homebrew tap and Scoop bucket |
| 157 | + |
| 158 | +## Known Limitations |
| 159 | + |
| 160 | +- No tests yet (see ARCHITECTURE.md technical debt) |
| 161 | +- No custom encrypt/exclude patterns (hardcoded in `config.go`) |
| 162 | +- No lock file (concurrent operations possible but risky) |
| 163 | +- Shells out to git (requires git in PATH) |
0 commit comments