• v0.7.5.0 2558ea3c23

    brian released this 2026-06-24 14:55:29 -07:00 | 6 commits to main since this release

    Multi-recipient age encryption (EGB-283): store-scoped recipients.txt encrypts every blob to N age keys (one per team member). Managed via secrets recipients add/rm/list; new secrets reencrypt re-encrypts to the current set. Composes with EGB-712 additive-v2 dual-write. Absence of recipients.txt preserves legacy single-key behavior.

    Downloads
  • v0.7.3.0 57f1280262

    brian released this 2026-06-08 16:30:08 -07:00 | 30 commits to main since this release

    EGB-671 — real install / onboarding scripts.

    • secrets join --remote --key : one-command second-machine onboarding that verifies the key decrypts the store before declaring success (empty vault never shows a false VERIFIED; wrong key fails loudly).
    • secrets init --remote : wires the remote + pushes the initial store so the first project push won't ff-only-die.
    • install.sh: thin bootstrap (checks age+jq+git, prints PATH/next-steps/upgrade/key hint; never edits rc, never runs sudo).
    • options.autoAdd prompt on first-manifest scaffold (EGB-677 contract #2).
    • Fix: secrets pull now dies loudly on a wrong-key decrypt instead of warning and exiting 0.

    282 bats tests pass. Operator security sign-off recorded for the branch HEAD.

    Downloads
  • v0.7.2.0 ec538d7ef0

    brian released this 2026-06-08 14:00:47 -07:00 | 34 commits to main since this release

    feat: secrets list --json machine-readable output (EGB-699)

    Adds a --json flag to secrets list emitting a structured object for tooling/CI (feeds the EGB-671 install scripts): {store, projects[].entries[]}, each entry self-describing via a type discriminator (dotenv→path / external→subtype+path). Mirrors the recursive store walk; jq assembles it so stdout stays pure JSON (notices→stderr); jq required only in --json mode. 7 new bats tests; full suite 261 pass / 0 fail.

    Downloads
  • v0.6.0.1 633d19e5f7

    brian released this 2026-06-08 06:23:18 -07:00 | 55 commits to main since this release

    EGB-700: secrets which now prints the .secrets.json schema version in its manifest header line (manifest (... version N):), alongside the store-format line shipped in v0.6.0.0. A single secrets which now surfaces both the store format and the manifest version. Full bats suite: 237/237. Operator security sign-off recorded.

    Downloads
  • v0.6.0.0 eaa08e8561

    brian released this 2026-06-07 17:00:34 -07:00 | 59 commits to main since this release

    EGB-677 stage 2 (EGB-703): self-describing store format + secrets migrate.

    Added: a committed .secrets-format marker (fresh stores born v2; secrets which prints the format); secrets migrate (per-project copy-forward of encrypted blobs to the v2 layout — non-destructive), migrate --dry-run (preview), and migrate --finalize (the one destructive step, run store-wide; refuses unless secrets verify passes and every blob has its v2 twin, cuts a pre-v2-migrate recovery tag, then drops the old blobs).

    Changed: the external properties blob is stored as <name>.properties.age in a v2 store (was .gradle-properties.age); push/pull/verify pick the right name automatically so v1 and v2 stores both keep working during a migration.

    Full bats suite: 236/236. Operator security sign-off recorded.

    Downloads
  • v0.5.0.0 144ff3692b

    brian released this 2026-06-07 15:34:02 -07:00 | 66 commits to main since this release

    Adds secrets verify (EGB-698) — a read-only integrity check.

    secrets verify cross-checks a project's committed .secrets.json against the store both ways (declared-but-missing blobs + orphaned blobs) and decrypt-tests every dotenv + external blob with the current key. secrets verify --all decrypt-tests every blob in every project (store-wide integrity sweep). Read-only (plaintext streamed to /dev/null); recurses the whole project tree; exits non-zero on any finding so it can gate CI and the stage-2 migrate --finalize.

    Full bats suite: 210/210. Operator security sign-off recorded.

    Downloads
  • v0.4.0.0 fb71b956da

    brian released this 2026-06-07 13:57:21 -07:00 | 72 commits to main since this release

    EGB-677 stage 1: .secrets.json manifest-driven sync.

    Added: committed .secrets.json manifest (version 2; dotenv[]/external[]/options) as the source of truth for what syncs; secrets add command; manifest-aware push (auto-add via options.autoAdd, --frozen, --dry-run, legacy .secrets-files absorb); manifest-driven pull (nested restore, restore-time path rail, empty no-op); jq gating + platform-aware install hints.

    Fixed: secrets rekey and secrets list now walk the entire project tree, so nested manifest blobs and external/ blobs survive key rotation (previously rekey could orphan nested blobs under the old key = data loss); bash 3.2 assertion correctness.

    Full bats suite: 193/193. Operator security sign-off recorded.

    Downloads
  • v0.3.0.0 a3c71f293b

    brian released this 2026-06-07 07:22:43 -07:00 | 85 commits to main since this release

    What's new

    • file external type for .secrets-files (EGB-652): whole-file verbatim sync for files outside the project — binary-safe, built for the Android upload keystore. Targets are restored at mode 600, with a .secrets-bak backup taken if a divergent file already exists. The gradle.properties basename restriction is waived for this type; all other path-validation rails apply.

    Full diff: https://codeberg.org/egbt/secrets/compare/v0.2.1.0...v0.3.0.0

    Downloads
  • v0.2.1.0 6ab22c2b95

    brian released this 2026-06-05 10:50:13 -07:00 | 90 commits to main since this release

    Fixed

    • secrets rekey no longer bricks dotenv stores. The re-encrypt loop used a bare "$dir"* glob, which never matches dotfiles — so .env blobs were decrypted to the temp dir but never re-encrypted, leaving them on the old key while the new key overwrote key.txt. After a rotation, every dotenv file in the store was undecryptable. The glob now mirrors the decrypt loop ("$dir"* "$dir".*), and a round-trip test (push → rekey → pull) pins it. If you ran rekey on an earlier version and pull now fails with no identity matched any of the recipients, your blobs are on a pre-rotation key — recover with an old key.txt from another machine.
    • secrets init on a second machine now fails helpfully instead of half-initializing. Copying key.txt into ~/.secrets and then running init (instead of cloning your secrets repo) used to run git init, crash on the existing key, and leave a store with no .gitignore — a state where a later push would commit the private key. The guard now fires before git init, leaves the key untouched, and prints the exact git clone command to run — using the real remote URL when your .secrets-store file declares one.

    Security

    • The private key can no longer be committed by a store missing its .gitignore. push, pull, and rekey now self-heal store protections immediately before any git add -A: a missing or corrupted .gitignore (one without the key.txt line) is rewritten, and the pre-commit hook is reinstalled if absent. The heal runs after the fast-forward pull, closing a window where remote history without a .gitignore could strip protection mid-push.
    • An already-tracked key.txt is now untracked automatically. .gitignore can't untrack a file that was committed in the past; the heal now removes a tracked key from the index with a warning that history may need scrubbing and the key may warrant rotation.

    Changed

    • Project CLAUDE.md gained agent skill-routing guidance and an updated test-suite count (126 bats tests, up from 113).
    Downloads
  • v0.2.0.0 e947cfde19

    brian released this 2026-05-26 15:02:55 -07:00 | 99 commits to main since this release

    Added

    • Sync designated keys from external files (Gradle properties). A new committed .secrets-files manifest lets secrets track specific keys from files that live outside the project root — the motivating case being ~/.gradle/gradle.properties, where Android builds read Clerk publishable keys (beaconClerkPkTest, beaconClerkPkLive) that Android Studio's GUI builds can only get from that persistent global file, not from terminal env vars. One entry per line: gradle-properties ~/.gradle/gradle.properties beaconClerkPkTest beaconClerkPkLive. (EGB-531)
      • push extracts only the named keys and encrypts them under <project>/external/ in the store.
      • pull merges those keys into the target file, preserving every unrelated key, comment, and line order. An existing managed key is updated in place; the target is backed up to gradle.properties.secrets-bak before each merge.
      • secrets which reads back the parsed manifest; secrets list shows [external] entries; secrets rekey re-encrypts external blobs alongside dotenv ones.
      • Backward compatible: no .secrets-files → identical behavior to before.

    Security

    • The merge is pure bash with exact-string key matching — no sed/regex. This is deliberate: a sed-based substitution would corrupt values containing &, \, or / (common in API keys) and would false-match substring keys (beaconClerkPk vs beaconClerkPkTest). Values are treated as opaque literals and round-trip byte-exact.
    • External write targets are validated against an attacker-controlled path. Because the target path comes from a committed manifest, the writable target is locked down: basename must be gradle.properties, the path must resolve inside $HOME, .. traversal is rejected, and symlinked targets (file or parent dir) are refused — blocking a malicious manifest from appending decrypted keys to ~/.gitconfig, ~/.bashrc, etc. Manifest parsing rejects shell metacharacters and control characters in paths and keys, mirroring the .secrets-store posture. Writes are atomic (temp-in-same-dir + rename), mode-preserving, and default to 600 on create.
    • Storage isolation. External blobs live in a <project>/external/ subdir so the existing broad *.age globs (pull, list, rekey) structurally never decrypt them into the working directory or orphan them.
    • Note on plaintext. Merged Gradle keys are written as permanent plaintext into the target file (secrets clear does not remove them) — appropriate for publishable/low-secrecy values like Clerk publishable keys, by design.

    Fixed

    • secrets rekey was broken and never completed. Two latent bugs, exposed by the new rekey test: (1) age-keygen -o key.txt aborts because age-keygen refuses to overwrite an existing file — the new key is now generated into a temp dir and moved into place only on success, so the old key survives a failed rotation; (2) the EXIT trap referenced the function-local $tmpdir after the function returned, erroring under set -u and leaking the plaintext temp dir — the temp dir is now removed explicitly and the trap cleared on normal completion.

    Tests

    • 80 → 113 (+33). New coverage: manifest parse/read-back, key extraction across =/:/space separators, merge (preserve unrelated/comments/order, substring-key isolation, sed-metachar value round-trip, duplicate-key collapse, continuation-line safety, idempotency), path validation (wrong basename, outside $HOME, symlinked target, symlinked parent dir), first-create mode 600, manifest injection/symlink/unsafe-key rejection, rekey round-trip of external blobs, glob isolation (blob not leaked to cwd), list surfacing, pre-commit blocking plaintext gradle.properties, workspace (push -w/pull -w) external sync, multi-entry manifests, partial-key push warnings, missing-blob pull warnings, source-side comment/continuation skipping, and backward compatibility.
    Downloads