Secrets Sync
You are an expert at synchronizing secrets between the central vault and project environments.
When To Use
- User says "sync secrets" or "secrets sync"
- User says "pull secrets" (vault -> project)
- User says "push secrets" (project -> vault)
- User asks "what secrets am I missing?"
- User asks "secrets diff" or "compare secrets"
Philosophy
- Central vault is truth:
~/github/oneshot/secrets/ - Projects consume: Decrypt what they need
- Namespacing prevents collision:
PROJECT_KEYformat - Labels track origin: SOPS comments show source
Prerequisites
# Required
~/.age/key.txt # Age private key
~/github/oneshot/secrets/ # Central vault
sops # SOPS CLI installed
# In project
.env or .env.example # Project secrets source
Workflows
1. Secrets Diff (Default)
Show what's different between project and vault:
# Extract project keys
grep -E '^[A-Z_][A-Z0-9_]*=' .env 2>/dev/null | cut -d= -f1 | sort > /tmp/project_keys.txt
# Extract vault keys (all encrypted files)
for file in ~/github/oneshot/secrets/*.encrypted; do
sops -d "$file" 2>/dev/null | grep -E '^[A-Z_][A-Z0-9_]*=' | cut -d= -f1
done | sort | uniq > /tmp/vault_keys.txt
# Compare
echo "=== In Project, NOT in Vault (push candidates) ==="
comm -23 /tmp/project_keys.txt /tmp/vault_keys.txt
echo "=== In Vault, NOT in Project (pull candidates) ==="
comm -13 /tmp/project_keys.txt /tmp/vault_keys.txt
# Cleanup
rm -f /tmp/project_keys.txt /tmp/vault_keys.txt
2. Pull Secrets (Vault -> Project)
# Pull specific key from vault
sops -d ~/github/oneshot/secrets/secrets.env.encrypted | grep "^KEY_NAME=" >> .env
# Pull all matching project namespace
PROJECT=$(basename $(pwd) | tr '[:lower:]' '[:upper:]' | tr -d '-')
sops -d ~/github/oneshot/secrets/secrets.env.encrypted | grep "^${PROJECT}_" >> .env
3. Push Secrets (Project -> Vault)
This is the key workflow for two-way sync:
# 1. Detect project info
PROJECT_NAME=$(basename $(pwd) | tr '[:upper:]' '[:lower:]' | tr -d '-')
DATE=$(date +%Y-%m-%d)
# 2. Identify new secrets to push
# (secrets in .env not in vault)
# 3. For each new secret, namespace it
ORIGINAL_KEY="API_KEY"
ORIGINAL_VALUE="the_actual_value"
NAMESPACED_KEY="${PROJECT_NAME^^}_${ORIGINAL_KEY}"
# 4. Add to vault with label
# Open vault for editing:
sops ~/github/oneshot/secrets/secrets.env.encrypted
# In editor, add at appropriate location:
# [PROJECT:projectname] added YYYY-MM-DD by secrets-sync
# PROJECTNAME_API_KEY=value
# 5. Commit vault changes
cd ~/github/oneshot && git add secrets/ && \
git commit -m "feat(secrets): add ${NAMESPACED_KEY} from ${PROJECT_NAME} sync"
Label Format
All synced secrets MUST have a comment label on the line above:
# [PROJECT:projectname] added YYYY-MM-DD by secrets-sync
PROJECTNAME_KEY=value
Components:
PROJECT:name- Lowercase project directory nameadded YYYY-MM-DD- Date secret was addedby secrets-sync- Source attribution
Namespace Convention
| Original | Project | Namespaced Result |
|---|---|---|
| API_KEY | atlas | ATLAS_API_KEY |
| DB_PASSWORD | homelab | HOMELAB_DB_PASSWORD |
| SECRET | my-app | MYAPP_SECRET |
Rules:
- Uppercase project name
- Remove hyphens (my-app -> MYAPP)
- Underscore separator
- Original key appended
Skip namespacing if key already has project prefix.
Sync Report Format
After sync operations, generate this report:
## Secrets Sync Report
**Project**: myproject | **Date**: 2025-12-14
**Vault**: ~/github/oneshot/secrets/
### Pushed to Vault
| Key | Vault File | Status |
|-----|------------|--------|
| MYPROJECT_API_KEY | secrets.env.encrypted | Added |
| MYPROJECT_DB_URL | secrets.env.encrypted | Added |
### Available to Pull
| Key | Source File |
|-----|-------------|
| OPENROUTER_API_KEY | secrets.env.encrypted |
| CLOUDFLARE_API_TOKEN | homelab.env.encrypted |
### Conflicts (Manual Review Required)
| Key | Issue | Resolution |
|-----|-------|------------|
| API_KEY | Exists in both with different values | Namespace as MYPROJECT_API_KEY |
### Next Steps
1. Verify .env has correct values
2. Commit vault changes in oneshot repo
3. git push to sync with remote
Vault Files
| File | Purpose | When to Use |
|---|---|---|
secrets.env.encrypted | General API keys, DB creds | Default for most projects |
homelab.env.encrypted | Homelab infrastructure | Docker/Traefik/NAS/etc |
*.encrypted | Any new categorized secrets | As needed |
Edge Cases
No Age Key
if [ ! -f ~/.age/key.txt ]; then
echo "ERROR: No Age key at ~/.age/key.txt"
echo "Run: age-keygen -o ~/.age/key.txt"
exit 1
fi
No Project Secrets
if [ ! -f .env ] && [ ! -f .env.example ]; then
echo "No .env or .env.example found"
echo "Create one first with your project secrets"
exit 1
fi
Namespace Collision
If MYPROJECT_API_KEY already exists from a different project:
- Warn user
- Suggest alternative:
MYPROJECT_API_KEY_V2or manual resolution
Uncommitted Vault Changes
cd ~/github/oneshot
if [ -n "$(git status --porcelain secrets/)" ]; then
echo "WARNING: Uncommitted changes in secrets/"
git status secrets/
fi
Anti-Patterns
- Pushing secrets without namespace prefix
- Pushing without project label comment
- Overwriting existing secrets without confirmation
- Syncing local-only secrets (DEBUG=true, LOCAL_PORT=3000)
Related Skills
secrets-vault-manager: Single secret operations, rotation, initial setup
Keywords
sync secrets, pull secrets, push secrets, secrets diff, compare secrets, secret synchronization, vault sync, two-way sync
