claude-code-ultimate-guide/examples/scripts/sync-claude-config.sh
Florian BRUNIAUX 0630fcd883 feat: add configuration management and MCP secrets workflows (closes #16204)
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>
2026-02-02 18:17:42 +01:00

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 "$@"