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>
This commit is contained in:
Florian BRUNIAUX 2026-02-02 18:17:42 +01:00
parent 5b69db64a9
commit 0630fcd883
6 changed files with 1591 additions and 0 deletions

View file

@ -0,0 +1,145 @@
{
"__comment": "settings.local.json - Machine-specific overrides (gitignored)",
"__usage": [
"This file allows you to override team settings.json without Git conflicts",
"Place in: .claude/settings.local.json (project) or ~/.claude/settings.local.json (global)",
"Precedence: global < project settings.json < settings.local.json",
"This file should be listed in .gitignore"
],
"__example_use_cases": [
"1. Skip linting on laptop (slower hardware)",
"2. Use different MCP endpoints for local development",
"3. Personal permission overrides without affecting team",
"4. Machine-specific hooks (e.g., notify Slack only on CI server)"
],
"hooks": {
"__comment": "Override team hooks for this machine only",
"PreToolUse": [
{
"__example": "Skip expensive pre-commit checks on laptop",
"matcher": "Bash|Edit|Write",
"hooks": [
{
"type": "command",
"command": "echo 'Skipping security check (local override)'",
"timeout": 100
}
]
}
],
"PostToolUse": [
{
"__example": "Disable auto-formatting on this machine (prefer manual)",
"matcher": "Edit|Write",
"hooks": []
}
],
"UserPromptSubmit": [
{
"__example": "Add machine-specific context (hostname, branch info)",
"matcher": "",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/local-context.sh"
}
]
}
]
},
"permissions": {
"__comment": "Personal permission overrides (more permissive or restrictive)",
"allow": [
"Bash(npm *)",
"Bash(pnpm *)",
"Bash(git *)",
"Edit",
"Write",
"WebSearch"
],
"deny": [
"__example": "Block dangerous commands even if team allows them",
"Bash(rm -rf *)",
"Bash(sudo *)",
"Bash(dd *)",
"Bash(mkfs.*)"
],
"ask": [
"__example": "Ask before expensive operations (slow on laptop)",
"Bash(npm run build)",
"Bash(docker build)",
"Bash(cargo build --release)"
]
},
"mcpServers": {
"__comment": "Machine-specific MCP server overrides",
"__use_case": "Use local MCP server instead of team's remote endpoint",
"postgres": {
"__example": "Override team's production DB with local dev DB",
"command": "npx",
"args": ["@modelcontextprotocol/server-postgres"],
"env": {
"DATABASE_URL": "postgresql://localhost:5432/dev_db"
}
},
"serena": {
"__example": "Use local Serena build for development",
"command": "node",
"args": ["/Users/yourname/dev/serena-mcp/dist/index.js"],
"env": {
"DEBUG": "true"
}
}
},
"__real_world_examples": {
"__laptop_skip_heavy_checks": {
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Edit|Write",
"hooks": [
{
"type": "command",
"command": "true",
"timeout": 10
}
]
}
]
}
},
"__ci_server_strict": {
"permissions": {
"deny": [
"Bash(git push)",
"Bash(npm publish)",
"WebSearch"
]
}
},
"__local_dev_overrides": {
"mcpServers": {
"database": {
"env": {
"DATABASE_URL": "postgresql://localhost:5432/test"
}
}
}
}
}
}

View file

@ -0,0 +1,163 @@
#!/bin/bash
# pre-commit-secrets.sh - Pre-commit hook to detect secrets in staged files
# Version: 1.0.0
# Purpose: Prevent accidental commits of API keys, tokens, and credentials
#
# Installation:
# cp examples/hooks/bash/pre-commit-secrets.sh .git/hooks/pre-commit
# chmod +x .git/hooks/pre-commit
#
# Test:
# echo "GITHUB_TOKEN=ghp_test" > test.txt
# git add test.txt
# git commit -m "Test"
# # Should fail with secret detection error
set -euo pipefail
# Colors
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Secret patterns (extended regex)
declare -A PATTERNS=(
["OpenAI API Key"]="sk-[A-Za-z0-9]{48}"
["GitHub Token (ghp)"]="ghp_[A-Za-z0-9]{36}"
["GitHub Token (gho)"]="gho_[A-Za-z0-9]{36}"
["GitHub Token (ghu)"]="ghu_[A-Za-z0-9]{36}"
["GitHub Token (ghs)"]="ghs_[A-Za-z0-9]{36}"
["GitHub Token (ghr)"]="ghr_[A-Za-z0-9]{36}"
["AWS Access Key"]="AKIA[A-Z0-9]{16}"
["AWS Secret Key"]="[A-Za-z0-9/+=]{40}"
["Anthropic API Key"]="sk-ant-[A-Za-z0-9-]{95,}"
["Generic API Key"]="api[_-]?key[\"']?\s*[:=]\s*[\"']?[A-Za-z0-9]{20,}"
["Generic Secret"]="secret[\"']?\s*[:=]\s*[\"']?[A-Za-z0-9]{20,}"
["Generic Token"]="token[\"']?\s*[:=]\s*[\"']?[A-Za-z0-9]{20,}"
["Database URL with Password"]="(postgres|mysql|mongodb)://[^:]+:[^@]+@"
["Private Key"]="-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----"
["JWT Token"]="eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}"
)
# Whitelisted patterns (safe to ignore)
WHITELIST=(
"your_token_here"
"your_key_here"
"example.com"
"localhost"
"placeholder"
"XXXXXX"
"\${env:" # Template variable syntax
"sk-ant-example" # Example in documentation
)
# Files to always skip (even if staged)
SKIP_FILES=(
"*.md" # Documentation often contains example secrets
"*.txt" # Same for text files
"*example*"
"*template*"
"*.sample"
)
# Check if a file should be skipped
should_skip_file() {
local file=$1
for pattern in "${SKIP_FILES[@]}"; do
# Convert glob to regex
local regex="${pattern//\*/.*}"
if [[ $file =~ $regex ]]; then
return 0 # Skip this file
fi
done
return 1 # Don't skip
}
# Check if a match is whitelisted
is_whitelisted() {
local match=$1
for whitelist in "${WHITELIST[@]}"; do
if [[ $match == *"$whitelist"* ]]; then
return 0 # Whitelisted
fi
done
return 1 # Not whitelisted
}
# Main secret detection logic
detect_secrets() {
local files
files=$(git diff --cached --name-only --diff-filter=ACM)
if [ -z "$files" ]; then
exit 0 # No staged files
fi
local found_secrets=0
local secrets_report=""
# Iterate through staged files
while IFS= read -r file; do
# Skip if file should be ignored
if should_skip_file "$file"; then
continue
fi
# Skip if file doesn't exist (deleted)
if [ ! -f "$file" ]; then
continue
fi
# Get staged content
local content
content=$(git show ":$file" 2>/dev/null || continue)
# Check each pattern
for pattern_name in "${!PATTERNS[@]}"; do
local pattern="${PATTERNS[$pattern_name]}"
local matches
matches=$(echo "$content" | grep -noE "$pattern" || true)
if [ -n "$matches" ]; then
# Check each match against whitelist
while IFS= read -r match; do
local line_num="${match%%:*}"
local matched_text="${match#*:}"
if ! is_whitelisted "$matched_text"; then
found_secrets=1
secrets_report+=" ${file}:${line_num} - ${pattern_name}\n"
secrets_report+=" Content: ${matched_text:0:50}...\n"
fi
done <<< "$matches"
fi
done
done <<< "$files"
# Report findings
if [ $found_secrets -eq 1 ]; then
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${RED}✗ COMMIT BLOCKED: Secrets detected in staged files${NC}"
echo -e "${RED}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo -e "${YELLOW}Found potential secrets:${NC}"
echo -e "$secrets_report"
echo ""
echo -e "${YELLOW}Remediation steps:${NC}"
echo " 1. Remove secrets from files"
echo " 2. Use environment variables: \${env:VAR_NAME}"
echo " 3. Store secrets in ~/.claude/.env (gitignored)"
echo " 4. See: guide/ultimate-guide.md Section 8.3.1"
echo ""
echo -e "${YELLOW}If this is a false positive:${NC}"
echo " - Edit .git/hooks/pre-commit and add to WHITELIST array"
echo " - Or skip hook: git commit --no-verify (USE WITH CAUTION)"
echo ""
exit 1
fi
exit 0
}
# Run detection
detect_secrets

View file

@ -0,0 +1,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 "$@"