Major additions to address critical gaps in Claude Code configuration: ## New Documentation Sections 1. Section 3.2.1 "Version Control & Backup" (guide/ultimate-guide.md:4085) - Configuration hierarchy: global → project → local - Git strategy for ~/.claude (symlinks approach) - Backup strategies: Git remote, cloud sync, cron - Multi-machine sync workflows - Disaster recovery procedures - Documented .claude/settings.local.json (previously undocumented) 2. Section 8.3.1 "MCP Secrets Management" (guide/ultimate-guide.md:8113) - Three practical approaches: OS Keychain, .env, Secret Vaults - Secrets rotation workflow - Pre-commit secret detection - Verification checklist - Best practices summary ## New Templates 1. sync-claude-config.sh (examples/scripts/) - Commands: setup, sync, backup, restore, validate - .env parsing + envsubst for variable substitution - Git repo creation with symlinks - Validation checks (secrets not in Git) 2. pre-commit-secrets.sh (examples/hooks/bash/) - Detects 10+ secret patterns (OpenAI, GitHub, AWS, etc.) - Whitelist system for false positives - Clear error messages with remediation steps 3. settings.local.json.example (examples/config/) - Machine-specific overrides template - Example use cases and patterns ## Resource Evaluation - Added docs/resource-evaluations/ratinaud-config-management-evaluation.md - Score: 5/5 (CRITICAL) - Validated via 3 Perplexity searches + technical-writer agent challenge - Community demand: GitHub #16204 + brianlovin/claude-config ## Updated References - machine-readable/reference.yaml: 22 new entries - Configuration management sections - MCP secrets workflows - Community resources (Ratinaud, brianlovin, GitHub issue) ## Impact - Security: Pre-commit hook prevents secret leaks - Productivity: Multi-machine sync reduces manual reconfig - Team coordination: Onboarding workflow for ~/.claude setup - Disaster recovery: Backup/restore strategies documented Credits: - Martin Ratinaud (504 sessions, LinkedIn post) - brianlovin/claude-config (community example) - GitHub Issue #16204 (community request) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
350 lines
9.9 KiB
Bash
350 lines
9.9 KiB
Bash
#!/bin/bash
|
|
# sync-claude-config.sh - Sync Claude Code global configuration via Git + .env substitution
|
|
# Version: 1.0.0
|
|
# Inspired by: brianlovin/claude-config + Martin Ratinaud (504 sessions)
|
|
#
|
|
# Features:
|
|
# - Parse .env for MCP secrets
|
|
# - Substitute variables in mcp.json from template
|
|
# - Validate .gitignore (prevent secret leaks)
|
|
# - Backup to cloud storage (optional)
|
|
# - Multi-machine sync via Git
|
|
#
|
|
# Usage:
|
|
# ./sync-claude-config.sh setup # Initial setup (Git repo + symlinks)
|
|
# ./sync-claude-config.sh sync # Pull latest from Git, regenerate configs
|
|
# ./sync-claude-config.sh backup # Push to Git + optional cloud backup
|
|
# ./sync-claude-config.sh restore # Restore from backup
|
|
# ./sync-claude-config.sh validate # Verify .gitignore and secrets isolation
|
|
|
|
set -euo pipefail
|
|
|
|
# Configuration
|
|
CLAUDE_DIR="$HOME/.claude"
|
|
BACKUP_DIR="$HOME/claude-config-backup"
|
|
ENV_FILE="$CLAUDE_DIR/.env"
|
|
MCP_TEMPLATE="$BACKUP_DIR/mcp.json.template"
|
|
MCP_CONFIG="$CLAUDE_DIR/mcp.json"
|
|
SETTINGS_TEMPLATE="$BACKUP_DIR/settings.template.json"
|
|
SETTINGS_CONFIG="$CLAUDE_DIR/settings.json"
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Helper functions
|
|
log_info() { echo -e "${GREEN}✓${NC} $1"; }
|
|
log_warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
|
log_error() { echo -e "${RED}✗${NC} $1"; }
|
|
|
|
check_requirements() {
|
|
local missing=()
|
|
command -v git >/dev/null 2>&1 || missing+=("git")
|
|
command -v envsubst >/dev/null 2>&1 || missing+=("envsubst")
|
|
|
|
if [ ${#missing[@]} -gt 0 ]; then
|
|
log_error "Missing required commands: ${missing[*]}"
|
|
log_info "Install with: brew install gettext (macOS) or apt install gettext-base (Linux)"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Setup: Create backup repo with symlinks
|
|
setup() {
|
|
log_info "Setting up Claude Code configuration backup..."
|
|
|
|
# Create backup directory
|
|
if [ -d "$BACKUP_DIR" ]; then
|
|
log_warn "Backup directory already exists: $BACKUP_DIR"
|
|
read -p "Overwrite? (y/N): " -n 1 -r
|
|
echo
|
|
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 0
|
|
rm -rf "$BACKUP_DIR"
|
|
fi
|
|
|
|
mkdir -p "$BACKUP_DIR"
|
|
cd "$BACKUP_DIR"
|
|
git init
|
|
log_info "Created Git repository: $BACKUP_DIR"
|
|
|
|
# Create symlinks for directories (not files with secrets)
|
|
for dir in agents commands hooks skills rules; do
|
|
if [ -d "$CLAUDE_DIR/$dir" ]; then
|
|
ln -sf "$CLAUDE_DIR/$dir" "$BACKUP_DIR/$dir"
|
|
log_info "Symlinked: ~/.claude/$dir"
|
|
else
|
|
log_warn "Directory not found: ~/.claude/$dir (skipping)"
|
|
fi
|
|
done
|
|
|
|
# Create .gitignore
|
|
cat > .gitignore << 'EOF'
|
|
# Never commit these (contain secrets)
|
|
.env
|
|
mcp.json
|
|
settings.json
|
|
*.local.json
|
|
|
|
# Session history (large, personal)
|
|
projects/
|
|
|
|
# Backup artifacts
|
|
*.tar.gz
|
|
*.bak
|
|
EOF
|
|
log_info "Created .gitignore"
|
|
|
|
# Create template files if they don't exist
|
|
if [ -f "$MCP_CONFIG" ]; then
|
|
# Convert existing mcp.json to template
|
|
sed 's/"[a-zA-Z0-9_-]\{20,\}"/"${env:\U&}"/' "$MCP_CONFIG" > "$MCP_TEMPLATE"
|
|
log_info "Created mcp.json.template from existing config"
|
|
fi
|
|
|
|
if [ -f "$SETTINGS_CONFIG" ]; then
|
|
cp "$SETTINGS_CONFIG" "$SETTINGS_TEMPLATE"
|
|
log_info "Created settings.template.json"
|
|
fi
|
|
|
|
# Create example .env
|
|
if [ ! -f "$ENV_FILE" ]; then
|
|
cat > "$ENV_FILE" << 'EOF'
|
|
# Claude Code MCP Secrets
|
|
# Add your API keys here (this file is gitignored)
|
|
|
|
# GitHub
|
|
GITHUB_TOKEN=ghp_your_token_here
|
|
|
|
# OpenAI
|
|
OPENAI_API_KEY=sk_your_key_here
|
|
|
|
# Database
|
|
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
|
|
|
# Add more secrets as needed
|
|
EOF
|
|
chmod 600 "$ENV_FILE"
|
|
log_info "Created .env template (edit with your secrets)"
|
|
fi
|
|
|
|
# Initial commit
|
|
git add .
|
|
git commit -m "Initial Claude Code configuration backup"
|
|
log_info "Initial commit created"
|
|
|
|
echo ""
|
|
log_info "Setup complete! Next steps:"
|
|
echo " 1. Edit $ENV_FILE with your secrets"
|
|
echo " 2. Add Git remote: git -C $BACKUP_DIR remote add origin <your-private-repo-url>"
|
|
echo " 3. Run: $0 backup"
|
|
}
|
|
|
|
# Sync: Pull from Git and regenerate configs
|
|
sync() {
|
|
log_info "Syncing Claude Code configuration..."
|
|
|
|
if [ ! -d "$BACKUP_DIR/.git" ]; then
|
|
log_error "Backup directory not initialized. Run: $0 setup"
|
|
exit 1
|
|
fi
|
|
|
|
cd "$BACKUP_DIR"
|
|
|
|
# Pull latest from remote
|
|
if git remote get-url origin >/dev/null 2>&1; then
|
|
log_info "Pulling latest from Git..."
|
|
git pull --rebase
|
|
else
|
|
log_warn "No Git remote configured (local only)"
|
|
fi
|
|
|
|
# Regenerate configs from templates + .env
|
|
if [ ! -f "$ENV_FILE" ]; then
|
|
log_error ".env file not found: $ENV_FILE"
|
|
log_info "Create it with your secrets, then run sync again"
|
|
exit 1
|
|
fi
|
|
|
|
# Export .env variables
|
|
set -a
|
|
source "$ENV_FILE"
|
|
set +a
|
|
|
|
# Substitute variables in mcp.json template
|
|
if [ -f "$MCP_TEMPLATE" ]; then
|
|
envsubst < "$MCP_TEMPLATE" > "$MCP_CONFIG"
|
|
log_info "Regenerated mcp.json from template"
|
|
else
|
|
log_warn "mcp.json.template not found (skipping)"
|
|
fi
|
|
|
|
# Copy settings (no substitution needed unless you use env vars)
|
|
if [ -f "$SETTINGS_TEMPLATE" ]; then
|
|
cp "$SETTINGS_TEMPLATE" "$SETTINGS_CONFIG"
|
|
log_info "Updated settings.json"
|
|
fi
|
|
|
|
log_info "Sync complete! Restart Claude Code to apply changes."
|
|
}
|
|
|
|
# Backup: Push to Git + optional cloud storage
|
|
backup() {
|
|
log_info "Backing up Claude Code configuration..."
|
|
|
|
if [ ! -d "$BACKUP_DIR/.git" ]; then
|
|
log_error "Backup directory not initialized. Run: $0 setup"
|
|
exit 1
|
|
fi
|
|
|
|
cd "$BACKUP_DIR"
|
|
|
|
# Check for changes
|
|
if git diff-index --quiet HEAD --; then
|
|
log_info "No changes to backup"
|
|
return 0
|
|
fi
|
|
|
|
# Commit changes
|
|
git add agents/ commands/ hooks/ skills/ rules/ *.template.json .gitignore 2>/dev/null || true
|
|
git commit -m "Backup Claude Code config - $(date +%Y-%m-%d\ %H:%M:%S)"
|
|
log_info "Changes committed"
|
|
|
|
# Push to remote if configured
|
|
if git remote get-url origin >/dev/null 2>&1; then
|
|
git push
|
|
log_info "Pushed to remote Git repository"
|
|
else
|
|
log_warn "No Git remote configured. Add with:"
|
|
echo " git remote add origin git@github.com:yourusername/claude-config-private.git"
|
|
fi
|
|
|
|
# Optional: Backup to cloud storage (Box, Dropbox, etc.)
|
|
# Uncomment and customize:
|
|
# if command -v rclone >/dev/null 2>&1; then
|
|
# rclone sync "$BACKUP_DIR" remote:claude-config-backup
|
|
# log_info "Synced to cloud storage (rclone)"
|
|
# fi
|
|
}
|
|
|
|
# Restore: Restore from backup
|
|
restore() {
|
|
log_info "Restoring Claude Code configuration..."
|
|
|
|
if [ ! -d "$BACKUP_DIR/.git" ]; then
|
|
log_error "Backup directory not found. Clone your backup repo to: $BACKUP_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
cd "$BACKUP_DIR"
|
|
|
|
# Recreate symlinks
|
|
for dir in agents commands hooks skills rules; do
|
|
if [ -d "$BACKUP_DIR/$dir" ]; then
|
|
rm -f "$BACKUP_DIR/$dir"
|
|
ln -sf "$CLAUDE_DIR/$dir" "$BACKUP_DIR/$dir"
|
|
log_info "Recreated symlink: ~/.claude/$dir"
|
|
fi
|
|
done
|
|
|
|
# Regenerate configs
|
|
sync
|
|
|
|
log_info "Restore complete!"
|
|
}
|
|
|
|
# Validate: Check .gitignore and secrets isolation
|
|
validate() {
|
|
log_info "Validating Claude Code configuration..."
|
|
|
|
local issues=0
|
|
|
|
# Check .env not in Git
|
|
if [ -f "$ENV_FILE" ] && git -C "$BACKUP_DIR" ls-files --error-unmatch "$ENV_FILE" >/dev/null 2>&1; then
|
|
log_error ".env is tracked by Git (CRITICAL SECURITY ISSUE)"
|
|
issues=$((issues + 1))
|
|
else
|
|
log_info ".env is not tracked by Git"
|
|
fi
|
|
|
|
# Check file permissions
|
|
if [ -f "$ENV_FILE" ]; then
|
|
perm=$(stat -f "%A" "$ENV_FILE" 2>/dev/null || stat -c "%a" "$ENV_FILE" 2>/dev/null)
|
|
if [ "$perm" != "600" ]; then
|
|
log_warn ".env permissions are $perm (should be 600)"
|
|
chmod 600 "$ENV_FILE"
|
|
log_info "Fixed permissions to 600"
|
|
else
|
|
log_info ".env permissions are correct (600)"
|
|
fi
|
|
fi
|
|
|
|
# Check secrets in staged files
|
|
if git -C "$BACKUP_DIR" diff --cached --name-only | xargs grep -E "(sk-[A-Za-z0-9]{48}|ghp_[A-Za-z0-9]{36}|AKIA[A-Z0-9]{16})" >/dev/null 2>&1; then
|
|
log_error "Secrets detected in staged files (DO NOT COMMIT)"
|
|
issues=$((issues + 1))
|
|
else
|
|
log_info "No secrets detected in staged files"
|
|
fi
|
|
|
|
# Check .gitignore exists
|
|
if [ ! -f "$BACKUP_DIR/.gitignore" ]; then
|
|
log_error ".gitignore missing (create one to prevent secret leaks)"
|
|
issues=$((issues + 1))
|
|
else
|
|
log_info ".gitignore exists"
|
|
|
|
# Verify critical patterns
|
|
for pattern in ".env" "mcp.json" "*.local.json"; do
|
|
if ! grep -q "^$pattern" "$BACKUP_DIR/.gitignore"; then
|
|
log_warn ".gitignore missing pattern: $pattern"
|
|
issues=$((issues + 1))
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ $issues -eq 0 ]; then
|
|
log_info "Validation passed! Configuration is secure."
|
|
return 0
|
|
else
|
|
log_error "Validation failed with $issues issues"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Main command dispatcher
|
|
main() {
|
|
check_requirements
|
|
|
|
case "${1:-}" in
|
|
setup)
|
|
setup
|
|
;;
|
|
sync)
|
|
sync
|
|
;;
|
|
backup)
|
|
backup
|
|
;;
|
|
restore)
|
|
restore
|
|
;;
|
|
validate)
|
|
validate
|
|
;;
|
|
*)
|
|
echo "Usage: $0 {setup|sync|backup|restore|validate}"
|
|
echo ""
|
|
echo "Commands:"
|
|
echo " setup - Initial setup (Git repo + symlinks)"
|
|
echo " sync - Pull latest from Git, regenerate configs"
|
|
echo " backup - Push to Git + optional cloud backup"
|
|
echo " restore - Restore from backup"
|
|
echo " validate - Verify .gitignore and secrets isolation"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|