* chore: ignore .gstack/ (per-project local state)
* feat: optional remote URL in .secrets-store (EGB-282)
A second whitespace-separated token after the store name in .secrets-store
is treated as the store's git remote URL. When a teammate clones a project
bound to a store they don't have locally yet, the directed missing-store
error now fills in `git clone <url> <path>` so they can copy-paste instead
of asking the original setter for the URL.
Backward compatible: single-token .secrets-store files (the v0.1.0.x
format) continue to work and produce the existing `<their-store-remote>`
placeholder.
Security hardening (caught by adversarial review during /ship):
- The rendered git clone line is meant to be copy-pasted by a teammate.
Without sanitization, `work evil.git;rm -rf ~` would render verbatim
and execute `rm -rf ~` on paste. The parser now rejects URLs containing
shell metacharacters (;&|<>$`(){}*?!"'\\), control characters (incl.
ANSI escape sequences that could spoof terminal output), and embedded
whitespace. Rejected URLs are dropped with a stderr warning; the error
falls back to the safe placeholder.
- Switched from `set -- $line` to `read -r spec rest` so the URL field
isn't glob-expanded or word-split — important so `work *` from a
populated directory doesn't leak filenames into the URL field.
Tests 72 → 80. New: backward compat, SSH+HTTPS+~/-prefix URL forms,
comment-and-URL form, four named injection vectors (shell metachar,
backtick, $(), ANSI escape), multi-token URL, glob char, and a positive
test asserting standard git URL chars round-trip unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version and changelog (v0.1.1.0)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.9 KiB
6.9 KiB
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to a four-digit MAJOR.MINOR.PATCH.MICRO version scheme.
0.1.1.0 - 2026-05-09
Added
- Optional git remote URL in
.secrets-store. Add a second whitespace-separated token after the store name to give teammates a copy-paste-ready clone command:
When a teammate clones a project bound to a store they don't have on their machine yet, the directed error now fills in the actualwork git@github.com:acme/work-secrets.gitgit clone <url> <path>line — they no longer have to ask the original setter for the URL. The URL is optional; existing single-token.secrets-storefiles continue to work and show the<their-store-remote>placeholder as before. (EGB-282)
Security
- Hardened
.secrets-storeURL parser against copy-paste shell injection. The URL is rendered into agit cloneline that a teammate is likely to copy-paste from the directed error. Without sanitization, a malicious.secrets-storecontainingwork evil.git;rm -rf ~would render verbatim and executerm -rf ~on paste. The parser now rejects URLs containing shell metacharacters (;&|<>$\(){}*?!"'\`), control characters (including ANSI escape sequences that could spoof terminal output), and embedded whitespace. Rejected URLs are dropped with a stderr warning; the directed error falls back to the safe placeholder. Found by adversarial review during /ship; verified with regression tests for every named attack vector. - Switched URL parsing from
set -- $linetoread -r spec rest. The previous form word-split and glob-expanded —work *from a populated directory would have leaked filenames into the URL field. The new form preserves the rest of the line verbatim into a single variable, so glob characters and internal whitespace are noticed by the sanitizer instead of silently expanded.
Tests
- 72 → 80 (+8). New coverage: backward-compat single-token form, two-token URL form (SSH, HTTPS,
~/-prefixed), comment-and-URL form, copy-paste injection (rm -rf payload), backtick injection,$()injection, ANSI escape injection, multi-token URL, glob-character URL, and a positive test asserting standard git URL chars (-,+,_,:,/,@,.) round-trip unchanged.
0.1.0.1 - 2026-05-09
Fixed
secrets whichnow prints the full path of the.secrets-storefile that won resolution. Before this fix,_find_secrets_store_fileset_LAST_FOUND_ATinside a$(...)subshell, so the parent shell never saw it; the source line read.secrets-store file ()with empty parens. The function now returns a tab-separated<dir>\t<source-file-path>tuple thatresolve_storesplits in the parent shell. Caught by the/land-and-deploypost-merge fresh-clone check; the existing test was too lenient and matched the truncated form. Test tightened to assert the full path appears in the parenthetical.
0.1.0.0 - 2026-05-09
Added
- Multiple stores per user. Run
secrets pushandsecrets pullagainst any encrypted store directory you choose, not just~/.secrets/. Use cases: keep work secrets isolated from personal, run a separate store per client, or onboard a teammate to one project without giving them every other project's keys. .secrets-storefile for per-project bindings. Drop a one-line file at the project root (e.g.echo work > .secrets-store && git add .secrets-store && git commit) and every machine that clones the project automatically uses~/.secrets-work/for that repo. No env var to remember, no per-machine setup.--store <dir>flag for one-shot overrides on any subcommand.secrets --store ~/.secrets-clientA pull myappworks without touching files. Bare names like--store workexpand to$HOME/.secrets-work.--store defaultis sugar for~/.secrets.secrets whichprints the active store path and which rule chose it (flag,.secrets-storefile, env var, or default). Aliases:secrets where,secrets status.- Directed errors for teammate onboarding. When
.secrets-storeresolves to an uninitialized store or one missingkey.txt, the error message names both recovery paths:git clone <remote>to join an existing store, orsecrets --store <name> initto start fresh. - Active-store echo.
secrets pushandsecrets pullprint==> Store: <path> (from <source>)whenever a non-default store is active, so wrong-store mistakes surface immediately.
Changed
secrets listnow hints atsecrets whichwhen a non-default store is active.cmd_helpdocuments the four-rule resolution order (--store>.secrets-storefile >SECRETS_DIR> default).- Error messages for missing init / missing key file are now context-aware: they distinguish between "default store on a fresh machine" and "non-default store referenced by
.secrets-store."
Security
- Path expansion in
.secrets-storeis literal-only. Noeval, no$VARinterpolation, no$(...)execution. A committed.secrets-storecontaining$(rm -rf ~)reads as plain text, not as a command. - Walk-up bounded by
$HOME.secretsnever reads$HOME/.secrets-store, never walks past$HOMEto/, and never follows symlinked.secrets-storefiles. Symlinks (potential supply-chain attack via committed link to~/.aws/credentialsor similar) are ignored. KEY_FILEre-derives after--storeswitches stores. Previously, callingsecrets pull --store otherwould have decrypted ciphertext from the new store using the default store's key. Nowsecretsupdates bothSECRETS_DIRandKEY_FILEtogether insideresolve_store().secrets runcleanup survives paths with apostrophes. The EXIT trap is now a named function rather than a string-interpolated command, so projects at e.g./Users/you/Mom's Mac/codestill get plaintext cleared after the wrapped command exits.- Test isolation: the bats suite now sets
HOME=$TEST_TMPDIRso.secrets-storewalk-up cannot wander into the developer's real home directory. --storeflag value validation. Empty values (--store=) and flag-shaped values (--store --workspaces) are rejected with directed errors instead of silently mapping to~/.secrets--something.- Unset
HOMEis detected with a directed error before the script tries to expand it. Helps cron, sudo without-H, and minimal CI runners.
Tests
- 37 → 66 tests. New coverage: store resolution rules and precedence, walk-up boundaries, command-injection prevention, key-file re-derivation across stores, teammate-onboarding error path, monorepo workspace binding, F1–F5 adversarial regressions.