Pairs the EGB-713 skew WARNING with a fix path. `secrets upgrade` fast-forwards
the tool's own git checkout (git -C "$SCRIPT_DIR" pull --ff-only; never merges or
rewrites local commits), reports vOLD -> vNEW, then best-effort re-checks the
store's recorded writer-version against the new version so the operator sees
whether the nudge is cleared. `secrets upgrade --check` reports availability
without pulling. Thin and explicit: no auto-update, no background polling
(security tool). Directed errors for not-a-checkout / no-upstream / diverged /
offline. cmd_upgrade never calls check_initialized (it's about the tool, not
the store); the skew re-check is silent unless a store with a writer-version
resolves.
Wired into the dispatcher (upgrade) shift; cmd_upgrade "$@") and cmd_help.
Tests: test/upgrade.bats (8) run a relocated script copy in a throwaway git
repo with a bare upstream, so the real checkout is never touched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EGB-677 stage-1 structural cleanups, no behavior change for the v2 happy path:
1. cmd_which reuses _json_external_entries (the push/pull extractor) instead of
its own duplicated jq @tsv projection, so `which` applies the same
properties->gradle-properties normalization + skip-with-warning rules the
sync path does and can't drift from it.
2. The two external-manifest read guards are factored into _json_readable
(plain regular file, silent) / _legacy_readable (warn+skip a symlinked
.secrets-files), shared by _external_entries_for_push/_pull.
3. The legacy (manifest-less) pull path now warns when nested <project>/<relpath>.age
blobs exist that its non-recursive globs can't see (external/ excluded —
pull_external_files handles those), so it never silently under-restores.
Tests: +4 in test/manifest.bats (normalized which display, malformed external
skipped by which, nested-blob warning fires, external-only no false warning).
Full suite green (286/286).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
README rewritten to the install.sh + init --remote + join flow; dropped the
macOS-only prerequisite (age+jq install hints now cover apt/dnf too). CHANGELOG
entry for EGB-671.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Ships in the repo (clone already done). Checks age + jq + git, then PRINTS the
PATH line, onboarding next-steps, upgrade one-liner, and key-transfer hint.
Never edits shell rc, never runs sudo (prints the command). Exits non-zero with
an install hint when a dependency is missing.
Add second-machine onboarding as a first-class verb rather than a manual
clone + key-copy sequence:
- secrets join --remote <url> --key <path>: clone the vault, install the key
at mode 600, then decrypt-test it before declaring success. An empty vault
reports "nothing to verify yet" (never a false VERIFIED); a wrong key fails
loudly. Reuses the audited core (resolve_store, get_pubkey, _verify_all) —
no security logic re-implemented.
- secrets init --remote <url>: wire the remote and push the initial store so
the upstream branch exists (fixes the commit_and_push_secrets pull --ff-only
die against a brand-new empty remote). init also offers an interactive
first-add of a project (default No; skipped under --yes / non-interactive).
- cmd_push first-manifest scaffold writes an explicit committed options.autoAdd
value, asked once when interactive (EGB-677 contract #2).
- secrets pull now dies loudly when a blob fails to decrypt (all three decrypt
paths) instead of warning and exiting 0 — a wrong key can't pass silently.
- Interactive prompts gate on stdin AND stdout being ttys, so bats/CI never hang.
- Dispatcher routes init/join args correctly; second-machine trap points at join.
Tests: 20 new (join, autoAdd, pty-no-hang regression); 2 trap tests updated.
Add a --json flag to `secrets list` that emits a structured object for
tooling/CI instead of the human table — feeds the EGB-671 install scripts,
which need to enumerate a cloned store programmatically.
Contract: {"store", "projects":[{"name","entries":[...]}]}, each entry
self-describing via a type discriminator — {type:dotenv,path} or
{type:external,subtype:properties|file,path}. cmd_list_json mirrors the same
recursive store walk as the human list (nested <project>/<relpath>.age +
external/<slug>.age); jq assembles the JSON so paths escape correctly and
stdout stays pure JSON (the non-default-store hint is suppressed; jq is a
hard dep only in --json mode).
Tests: 7 new bats cases (dotenv, nested relpath, external properties + file
subtypes, empty store, pure-stdout-under-notice, store path). Full suite
261 pass / 0 fail.
VERSION 0.7.1.0 -> 0.7.2.0; CHANGELOG/README/CLAUDE.md updated.
- README: test suite total 236 -> 237
- CLAUDE.md: manifest.bats 77 -> 78 tests
- CLAUDE.md: note `secrets which` now prints the .secrets.json schema
version in its manifest header line alongside `format: vN`
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes EGB-700 — the store-format line (format: vN) shipped in v0.6.0.0;
this adds the manifest schema version to the `which` manifest header
(version N, validated == MANIFEST_VERSION by _check_manifest_file). Now a
single `secrets which` surfaces both the store format and the manifest
version for the dual-format debugging window. 1 bats test. Suite 237/237.
Pre-landing review (1 critical, 4 informational):
- CRITICAL: per-project `secrets verify` during the migration window flagged
the freshly-written v2 twin as a spurious orphan and exited 1 (store still
reads v1, so verify's expected set only held the .gradle-properties.age name).
Breaks the documented migrate->verify->finalize workflow and CI. Fix: verify's
orphan set now accounts for BOTH suffix forms of a properties external, so the
twin is never a false orphan mid-migration. Regression test added.
- Copy-forward now pushes the twins (mirrors push/rekey) so a --finalize on
another machine sees them; previously twins were local-only until finalize,
a multi-machine footgun.
- Dropped the undocumented `--force` alias (keep `--yes`).
- Clarified the EGB-700 comment (which-format line, folded into EGB-703).
Deferred to EGB-701: the two finalize find-walks over *.gradle-properties.age
could collapse to one pass. Full suite 236/236.
Coverage audit found 4 untested branches (all single-test fills): finalize on
a v1 dotenv/file-only store (v1count==0 stamp path — a real user scenario),
the migrate positional-arg die, finalize on an already-v2 store, and the
_store_format strict-parse defense (garbage marker reads as v1). Full suite 235/235.
Self-review of the data-safety paths (verdict SHIP-SAFE) flagged two
non-blocking nits, both fixed:
- _store_format used `tr -dc '0-9'` which read garbage like "v2"/"x2x" as v2.
Tightened to a strict exact match (modulo line endings) so only "2" reads as
v2; anything else falls back to v1, the safe default.
- The stamp-before-delete comment claimed a re-run "cleans" crash-orphaned v1
blobs; it doesn't (finalize early-returns once the store is v2). Corrected to
note the orphans are harmless and `verify` flags them for manual removal.
Stage 2 of EGB-677. Makes the store self-describing and unifies the legacy
external `properties` blob suffix, via a copy-forward migration that never
destroys data until an explicit, gated finalize.
Scope decision (see eureka): the EGB-677 CEO plan's "flatten dotenv blobs to
basename" was dropped as LOSSY — it discards the restore relpath that makes
the store self-describing and adds basename collisions. Engineering analysis
(4 parallel design agents) showed the store is already relpath-self-describing;
the only real v1→v2 delta is the `properties` suffix. This implements the
minimal, safe v2 that achieves the epic's self-describing goal.
What's added:
- `.secrets-format` marker (committed, one line `2`). Absence ⇒ v1 (every
pre-EGB-703 store). `_store_format()` reads it; `init` stamps fresh stores
born-v2. `secrets which` prints `format: vN` (EGB-700 folded in).
- `_external_blob_suffix(type)` — single source of truth for the external
suffix (v2: gradle-properties → properties; file unchanged). push/pull/verify
all route through it, so v1 and v2 stores never disagree on blob location.
- `secrets migrate` — per-project copy-forward (writes `.properties.age` twins
beside v1 blobs; idempotent; needs the project manifest), `--dry-run`
(reports old→new, writes nothing), `--finalize` (store-wide, the only
destructive step: gates on `verify --all` green + every v1 blob twinned,
cuts a `pre-v2-migrate-<sha>` recovery tag, stamps the marker, then drops v1
blobs; refuses without `--yes`/operator confirmation).
rekey and verify --all stay format-agnostic (recursive find walk) — no change.
21 new bats tests (test/migrate.bats): marker/born-v2, format-aware suffix,
v1 back-compat, dry-run, copy-forward idempotency, no-manifest die, finalize
gates (verify-not-green refusal, untwinned refusal, recovery tag, confirmation),
and full v1→window→finalize round-trip. Updated 4 existing tests for the
born-v2 suffix. Full suite 231/231, bash 3.2 clean.
Pre-landing review (0 critical, all informational) auto-fixes:
- Tighten external-corrupt test to assert the decrypt-fail message, not any
external finding (was *"external"*, now *"does not decrypt"*).
- Pin the verified-count in the nested+external happy-path test so a silent
under-count (exit 0 while skipping a blob) is caught.
- Account for an unsafe dotenv entry in `expected` so a matching stray blob
isn't double-reported as both unsafe and orphan.
- Discard the unused external `keys` read field (read -r etype epath _).
- Document the optional [project] positional in the README verify row.
Deferred to EGB-701 (stage-2 dedup): the external blob-path literal and the
find-walk overlap with cmd_rekey/cmd_list. Full suite 210/210.
Coverage audit found 5 untested branches in cmd_verify (all single-test
fills, no logic defects): external blob decrypt-failure (only the missing
case was covered), the rail-skip finding for an unsafe dotenv path in the
manifest, the unknown-flag die, a malformed manifest through the verify
entry point, and the empty-store 'verify --all' no-op. Full suite 210/210.
Read-only integrity check, the safety net for the stage-2 store migration.
Default mode (current project) cross-checks $PWD/.secrets.json against the
store both ways — declared-but-missing blobs and orphaned blobs (no manifest
entry) — and decrypt-tests every dotenv + external blob with the current key,
streaming plaintext to /dev/null so nothing is ever written to disk. `verify
--all` decrypt-tests every blob in every project (integrity only; the store
carries no manifests, so consistency can't be checked store-wide). Both
recurse the whole project tree (find -type f), the same walk rekey/list use,
so nested manifest blobs are covered. Exits non-zero on any finding so it can
gate `migrate --finalize` and CI.
12 bats tests (clean, nested+external, missing blob, decrypt failure, orphan,
missing external, no-manifest die, symlink refusal, --all clean/corrupt/orphan,
nested decrypt failure). Full suite 205/205. bash 3.2 clean.
CLAUDE.md: manifest.bats 58 -> 60 tests (actual @test count).
README.md: total 191 -> 193 tests across both files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>