forked from FlorianBruniaux/claude-code-ultimate-guide
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsync-claude-config.sh
More file actions
350 lines (295 loc) · 9.9 KB
/
sync-claude-config.sh
File metadata and controls
350 lines (295 loc) · 9.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
#!/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 "$@"