secrets/CLAUDE.md
Brian Majewski 7e6ddf3a12
v0.1.0.0 feat: multi-store support (EGB-281) (#1)
* feat: multi-store support via .secrets-store + --store flag

Layer four-rule store resolution on top of the existing SECRETS_DIR primitive
so users can manage multiple isolated encrypted stores (work vs personal,
per-client, etc.) without giving up the tool's small-bash-script pitch.

Resolution order (highest first):
  1. --store <dir>  flag (parsed in main pre-pass)
  2. .secrets-store file in cwd or any ancestor up to $HOME
  3. SECRETS_DIR    env var (legacy escape hatch)
  4. ~/.secrets     default

resolve_store() updates both SECRETS_DIR and KEY_FILE so existing single-store
codepaths just work. New cmd_which / where / status report the active store.
cmd_init, push, pull, push_workspaces, pull_workspaces, list, rm, rekey, run,
which all call resolve_store at entry.

Hardening from the EGB-281 adversarial review:
- F1: cmd_run EXIT trap is now a named function (not string-interpolated),
  so paths with apostrophes still get plaintext cleaned up
- F2: symlinked .secrets-store files are skipped, never read
- F3/F4: --store flag rejects flag-shaped values and empty --store=
- F5: HOME unset is detected up-front with a directed error
- F11: check_initialized / check_key give context-aware errors that name
  both recovery paths (git clone vs secrets init) when a teammate clones
  a project bound to a non-existent store on their machine

Tests: 37 → 66 (29 new). HOME=\$TEST_TMPDIR added to test setup so the
walk-up logic stays bounded inside fixtures.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: add Multiple stores section to README

Five subsections walk users through: how store resolution works, how to
set up a second store on a machine, how to bind a project, how teammates
join a bound project, and how to undo or change a binding. SECRETS_DIR
table entry now points readers at the new --store flag and .secrets-store
file as the preferred mechanisms.

* chore: bump version and changelog (v0.1.0.0)

First formal release. EGB-281 adds multi-store support; this commit
seeds the VERSION file (4-digit MAJOR.MINOR.PATCH.MICRO) and the
CHANGELOG.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:30:28 -07:00

2.5 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: age with 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: --workspaces flag reads package.json workspaces, requires jq
  • 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:

  1. --store <dir> flag (parsed in the main pre-pass into STORE_OVERRIDE).
  2. .secrets-store file in cwd or any ancestor, walk-up bounded by $HOME (never reads $HOME/.secrets-store itself or anything above).
  3. SECRETS_DIR env var (legacy escape hatch).
  4. ~/.secrets default.

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.

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.