CLI tool with no platform deploy. Released via merge to main + optional version tags. Recorded so /land-and-deploy skips the dry-run on subsequent invocations.
2.9 KiB
secrets
Encrypted env file sync between machines using age key-file encryption + a private git repo.
Quick Start
brew install age
./secrets init # Create ~/.secrets repo + generate age key
cd ~/my-project && ./secrets push # Encrypt .env* files, commit, push
# On other machine:
cd ~/my-project && ./secrets pull # Pull + decrypt .env* files
Testing
brew install bats-core
bats test/secrets.bats
Architecture
Single bash script (secrets) with subcommands: init, push, pull, list, rm, rekey.
- Encryption:
agewith key files (not passphrases — age passphrases are non-scriptable) - Storage: Private git repo at
~/.secrets/ - Convention: Tracks
.env,.env.*, and.dev.vars(not.envrc,.environment-*) - Workspaces:
--workspacesflag readspackage.jsonworkspaces, requiresjq - Safety: Pre-commit hook rejects plaintext secret files
Project Structure
secrets # CLI script (~300 lines bash)
hooks/pre-commit # Pre-commit hook template
test/
secrets.bats # bats-core test suite (25 tests)
test_helper.bash # Shared setup/teardown
README.md # User-facing documentation
CLAUDE.md # This file
Key file
~/.secrets/key.txt is the age identity (private key). It is gitignored and must be copied manually to each machine once.
Multi-store resolution
The active store directory is picked by resolve_store() using these rules, highest precedence first:
--store <dir>flag (parsed in the main pre-pass intoSTORE_OVERRIDE)..secrets-storefile in cwd or any ancestor, walk-up bounded by$HOME(never reads$HOME/.secrets-storeitself or anything above).SECRETS_DIRenv var (legacy escape hatch).~/.secretsdefault.
resolve_store mutates BOTH SECRETS_DIR and KEY_FILE so the existing single-store code paths just work. STORE_SOURCE reports which rule won. _LAST_FOUND_AT (when rule 2 fires) holds the path of the file that was read.
.secrets-store parsing is deliberately conservative: first non-empty non-comment line wins, no shell expansion (no $VAR, $(), backticks). Bare names map via _expand_store_path: work → $HOME/.secrets-work, default → $HOME/.secrets.
Deploy Configuration
- Platform: NONE (distributed via
git clonefrom GitHub) - Production URL: N/A (no live service)
- Release model: merge to
mainis the release. Optionally tagged withv<X.Y.Z.W>. - Verification after merge: a fresh
git cloneshould produce a workingsecrets whichagainst an isolated$HOME. No canary URL. - Staging: none.
- Rollback: revert the merge commit on
main(and delete the tag) to roll back.
Environment variable
SECRETS_DIR overrides the default ~/.secrets location (useful for testing). Per-project bindings via .secrets-store file beat this env var; use --store <dir> for one-shot overrides that beat everything.