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>
This commit is contained in:
Brian Majewski 2026-05-09 14:30:28 -07:00 committed by GitHub
parent edb1614941
commit 7e6ddf3a12
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 812 additions and 11 deletions

View file

@ -223,6 +223,95 @@ Stopping the dev server (or any failing command) ends the process; the `EXIT` tr
If you prefer to keep decrypted files on disk for a long editing session, use `secrets pull` and `secrets clear` manually instead.
### Multiple stores
By default, all your encrypted secrets live in one store at `~/.secrets/`. That works great if you have one set of secrets shared across machines. If you want **separate stores** — for example, work secrets isolated from personal projects, or one store per client — `secrets` supports that without any special setup.
A "store" is just a directory with its own `.git` repo, age key, and remote. You can have as many as you want.
#### How a store gets picked
When you run `secrets push` or `secrets pull`, the tool resolves the active store using the first matching rule (highest precedence first):
```
1. --store <dir> flag passed on the command line
2. .secrets-store file in the current directory or any ancestor up to $HOME
3. SECRETS_DIR environment variable (legacy escape hatch)
4. ~/.secrets default
```
Run `secrets which` from any project directory to see which rule won and which store is active. Aliases `secrets where` and `secrets status` do the same thing.
#### Set up a second store on this machine
```bash
# Create a fresh store at ~/.secrets-work with its own age key
secrets --store work init
# Connect it to a separate private GitHub repo
cd ~/.secrets-work
git remote add origin git@github.com:<you>/work-secrets.git
git push -u origin main
```
The bare name `work` expands to `$HOME/.secrets-work`. Use `secrets --store /any/abs/path init` if you want a custom location.
#### Bind a project to a non-default store
In any project directory, write a `.secrets-store` file with the store's name (or path) and commit it:
```bash
cd ~/myapp
echo work > .secrets-store
git add .secrets-store
git commit -m "use work secrets store"
```
After that, every `secrets push` / `secrets pull` from this project (or any subdirectory) automatically uses `~/.secrets-work`. Teammates who clone the project get the same binding for free — the file is in the repo.
When you push or pull from a non-default store, `secrets` echoes which one is active so you can spot mistakes immediately:
```
==> Pushing secrets for project: myapp
==> Store: /Users/you/.secrets-work (from .secrets-store file (~/myapp/.secrets-store))
```
#### Joining a teammate's bound project
If you clone a project that has a committed `.secrets-store: work` file but you don't have `~/.secrets-work` set up locally, `secrets pull` will tell you exactly what to do:
```
ERROR: Store not initialized: /Users/you/.secrets-work
Resolved from: .secrets-store file (~/myapp/.secrets-store)
This path doesn't exist on this machine yet.
If you're joining a teammate's existing store:
git clone <their-store-remote> /Users/you/.secrets-work
# then copy their key.txt to /Users/you/.secrets-work/key.txt
If you want a fresh new store at this path:
secrets --store /Users/you/.secrets-work init
```
You'll need two things from the teammate who set it up:
1. **The git remote URL** of the work-secrets repo — clone it to `~/.secrets-work` (or wherever the `.secrets-store` file resolves to on your machine).
2. **The age key file** (`key.txt`) — same as standing up any new machine. AirDrop, scp, or USB.
Once both are in place, `secrets pull` works.
#### Undo or change a binding
```bash
# Stop using a non-default store for this project
rm .secrets-store
git commit -am "go back to default secrets store"
# Or change which store the project is bound to
echo personal > .secrets-store
git commit -am "switch to personal secrets"
```
### Monorepo support
For projects with multiple packages (monorepos using `package.json` workspaces), add the `-w` flag to operate on all workspaces at once:
@ -273,7 +362,7 @@ For complete rotation with no historical exposure, create a fresh `~/.secrets/`
| Variable | Default | Purpose |
|----------|---------|---------|
| `SECRETS_DIR` | `~/.secrets` | Override the secrets store location |
| `SECRETS_DIR` | `~/.secrets` | Override the secrets store location (legacy; prefer `--store` or a `.secrets-store` file — see "Multiple stores" above) |
## Troubleshooting