The origin moved to the private Forgejo instance at git.dev.egbt.com
(egbt/secrets). Git history, all 16 branches, 13 tags, and all 13 releases
were migrated with identical SHAs and release bodies.
- CLAUDE.md: replace the "Codeberg operations" section with "Forge
operations", switching tea invocations to `--login egbt --repo
egbt/secrets`. Records three things that are easy to get wrong:
Forgejo's SSH listens on port 2222 (port 22 is the host's own sshd, so
a bare git@host:path fails with "Permission denied (publickey)"); the
host is Tailscale-only, so off-VPN failures are connectivity, not
credentials; and the stale `codeberg` tea login is still configured and
can be silently fallen back to in non-interactive mode.
- CLAUDE.md: note the instance has an Actions runner available, though no
workflow is configured yet — local bats remains the merge gate.
- README.md: clone URLs now point at the new host.
- CHANGELOG.md: release links now point at the new host.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrUoYuUMoTj91rzV4vxGPB
Workspace patterns came from package.json's `workspaces` key only, at both
call sites. pnpm declares them in pnpm-workspace.yaml instead, so no pnpm
monorepo ever resolved a workspace: `push -w` refused outright, and plain
`push` failed silently — _maybe_workspace_env_files returned 0 the moment
the key was absent, leaving the auto-discovery that covers push's root-only
scan inert and printing "Nothing new to add", indistinguishable from a repo
with genuinely nothing new. That silence cost two sessions on the same repo.
Both call sites now resolve through one shared source, _workspace_patterns:
package.json when it declares any, else pnpm-workspace.yaml's `packages:`
block. The YAML read is deliberately not a parser — block sequence only,
stopping at the next top-level key so pnpm 10's onlyBuiltDependencies:/
catalog: cannot leak in as globs, with quote/comment handling and a symlink
refusal.
Also fixes yarn's object form. `.workspaces // .workspaces.packages | .[]`
short-circuits on the truthy object, iterating its values and yielding the
pattern array as one token; only npm's array form ever worked. Note the
obvious reorder is NOT the fix — `.workspaces.packages` errors on an array —
so the filter is type-aware.
Patterns are validated before reaching the unquoted glob expansion (no
absolute paths, `..`, metacharacters, or whitespace; pnpm `!` negations
skipped), matching the .secrets-store/.secrets-files posture. jq is now
required only when package.json is the source. A monorepo-shaped root that
resolves nothing warns and points at `secrets add` instead of returning in
silence, and `-w`'s error names pnpm-workspace.yaml when that is the file
present.
Scope note: the workspace re-scan still runs only for projects that already
have a .secrets.json — push's root-scan-only first push is by design
(EGB-677 E13), and this bug is the fallback covering it never engaging.
test/workspaces.bats: 18 new tests. Full suite 371/371 green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrUoYuUMoTj91rzV4vxGPB
`tea` autodetection fails in this repo, and the machine carries a second
login (`egbt`) pointing at a different forge that tea silently falls back
to in non-interactive mode. Document the explicit
`--login codeberg --repo egbt/secrets` form for every verb.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrUoYuUMoTj91rzV4vxGPB
EGB-1230: `cmd_pull` synced the store with `git pull >/dev/null 2>&1` under
`set -euo pipefail`. A store that couldn't fast-forward killed the script at
that line with git's exit 128 and nothing on stdout or stderr — a banner, no
restored files, no reason, and invisible in a pipeline. The sync now routes
through `_store_sync_pull`, which guards the pull, captures git's output as
the diagnosis, and dies naming the store path and `secrets sync`. It is now
`--ff-only` to match the push path, so a plain pull can no longer manufacture
a merge commit in the store.
EGB-1231: once a store diverged there was no way out — push demanded a
fast-forward and pointed at pull, which couldn't fast-forward either, so the
advice looped and recovery meant hand-running git next to encrypted blobs.
Adds `secrets sync`: fetch, stash, rebase onto the remote, restore the stash,
then a confirmation-gated push of local commits (`--yes` to skip the prompt,
`--dry-run` to report only). Non-destructive by construction — no merge, no
force-push, no `reset --hard`, no `stash drop`; a rebase conflict names the
conflicting files and leaves the store exactly as found. `secrets which` gains
a `remote:` line reporting ahead/behind/dirty, and push's dead-end message now
points at `sync`.
test/sync.bats: 25 new tests. Full suite 353/353 green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BrUoYuUMoTj91rzV4vxGPB
Reconcile the multi-recipient branch (cut from v0.6.1.0) with origin/main
at v0.7.4.0. The two feature lines are largely orthogonal; the one real
integration point is the external-blob encrypt path:
- EGB-712 added an additive-v2 dual-write loop (_external_blob_write_targets,
writing v2 + any v1 twin). EGB-283 routes every encrypt site through
RECIPIENT_ARGS for N-recipient encryption. Resolution keeps the dual-write
loop but encrypts each target to the full recipient set
(age "${RECIPIENT_ARGS[@]}" per write target), so dual-write and
multi-recipient compose. cmd_push loads recipients before both external
push sites; legacy single-key rekey keeps its fresh-keypair pubkey path.
Version: 0.6.2.0 + 0.7.4.0 -> 0.7.5.0. Docs (CLAUDE.md/README/CHANGELOG)
merged to carry both feature sets; subcommand list now includes
recipients/reencrypt and upgrade.
Tests: full `bats test/` green except 6 pre-existing host-environment
failures (4 chmod-600 restore assertions + 2 jq-PATH-shadow tests, all
macOS-authored), none touching merged code. recipients.bats 34/34 pass;
external/dual-write area passes except the same mode-600 host artifacts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix 1 (IMPORTANT): _recipients_rm and _recipients_add now call _load_recipients
BEFORE any mutation. A hand-corrupted recipients.txt dies at validation, leaving
the file untouched — prevents inconsistent state where the file is changed but
blobs are not re-encrypted. On a legacy store (no recipients.txt), _load_recipients
succeeds via the derived-pubkey path so the bootstrap path still works.
- Fix 2 (MINOR): Guard _check_blob_recipient_count behind a successful decrypt in
both _verify_all and _verify_project — an undecryptable blob no longer produces a
spurious "encrypted to 0 recipients" finding. Reword _verify_all summary to
"failed (decrypt or recipient-count)" since both failure modes now increment the
counter.
- Fix 3 (MINOR): Correct README offboarding comment from "New blobs are no longer
readable" (contradicts the re-encrypt of EVERY blob) to "Existing blobs are
re-encrypted; the removed key can no longer decrypt them."
- Fix 4 (MINOR): cmd_reencrypt prints an advisory when no recipients.txt exists
(single-key store), so the operator knows they can add teammates.
- Fix 5 (MINOR): Test coverage for ambiguous-name rm refusing to remove when
multiple recipients share a --name label.
Tests: 5 new tests in test/recipients.bats (34 total, all pass). Full suite
276 tests: 5 known pre-existing failures (3 mode-600/stat, 2 jq-PATH), none new.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap _load_recipients check order so [ -L ] (symlink) runs before [ ! -e ]
(missing), closing the gap where a dangling symlink bypassed the refusal
and silently fell back to legacy single-key mode.
Add 6 SECURITY-tagged fixtures to test/recipients.bats: dangling-symlink
refused (the ordering gap), shell-metachar injection (no execution), extra-
age-flag-looking line, control/ANSI chars, embedded whitespace via add, and
symlinked file refused on add. All 6 pass immediately after the ordering fix;
the existing rails (_validate_age_recipient, -L checks) were already tight
enough that only the production swap was needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Require a non-empty argument after --name (dies if it is the last token)
and reject whitespace-only labels that would write a blank comment line.
Two regression tests added to test/recipients.bats.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add `secrets recipients add <age1...> [--name <label>]`: validates the
age key and optional display name, bootstraps recipients.txt with the
local pubkey on a legacy store (keeping the operator as a recipient),
rejects duplicates, appends the new key (with optional name comment),
then re-encrypts the entire store to the updated recipient list.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add RECIPIENTS_FILE_NAME / RECIPIENTS_FILE constants; update resolve_store to re-derive RECIPIENTS_FILE after store resolution.
- Add _validate_age_recipient (native age1 X25519 key format check, injection rail).
- Add RECIPIENT_ARGS global array and _load_recipients (absent → single pubkey legacy path; present → parse+validate recipients.txt, refuse symlink, die on bad/empty).
- Rewire all 5 push encrypt sites (push_dir_to_project, cmd_push inline, push_external_files ×2, cmd_push_workspaces) to use RECIPIENT_ARGS; drop pubkey threading from push_dir_to_project and push_external_files signatures.
- New test/recipients.bats (4 tests): legacy single-key, multi-recipient decrypt, invalid key rejection, symlink rejection.
- Fix test/test_helper.bash: set GIT_AUTHOR/COMMITTER env vars so git commit works with isolated $HOME.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.