This makes actual functionality follow status quo documentation. Also
does not access ~/.ssh unless user ir explicit about that.
If age.ssh-key-path is set, it is loaded in addition to age.sshkeys, if
that is enabled, or instead of those, if not.
Fixes#3522
Signed-off-by: Daniel Lublin <daniel@lublin.se>
Go Report Card has been retired, so the badge no longer renders useful
status. Replace it with a GitHub Actions workflow status badge for our
golangci-lint workflow, styled like the existing build status badge.
Assisted-by: Claude-Code:GLM-5.3
Signed-off-by: Zexin Yuan <git@yzx9.xyz>
* ci: bump golangci-lint to v2.12.2 and migrate gomodguard to gomodguard_v2
golangci-lint v2.12.0 deprecated the gomodguard linter in favour of
gomodguard_v2. Running the current .golangci.yml under v2.12.2 emits:
The linter 'gomodguard' is deprecated (since v2.12.0) due to:
new major version. Replaced by gomodguard_v2.
Bump both pins (Makefile and the golangci-lint-action workflow) and rename
the linter in one commit: naming gomodguard_v2 while CI still ran v2.11.4
would hard-fail the lint job on an unknown linter.
The rename carries no semantic change -- .golangci.yml has no
settings.gomodguard block, so the linter is currently allow-everything.
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: update golangci-lint to v2.13.1
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
---------
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
.github/dependabot.yml only configured the github-actions ecosystem, so Go
module updates were never raised. The result is 18 outdated direct
dependencies in go.mod today, including golang.org/x/net 0.47 -> 0.57 and
golang.org/x/crypto 0.45 -> 0.54. Dependabot did open #3490 for x/crypto,
but only because that is a security advisory, which it raises without any
configuration.
Add the gomod ecosystem on a weekly schedule, and:
- cooldown on both ecosystems, so a freshly published release has a few
days to be pulled if it turns out to be broken or compromised before we
adopt it. Go major bumps get 30 days; patch releases get 3.
- a golang-x group, because golang.org/x/* modules move in lockstep and
pull each other in transitively -- one pull request per module produces
a pile of mutually conflicting branches.
- a terminal group for the mattn/* and fatih/color modules, which are all
small and share one manual smoke test.
- an explicit commit-message prefix, so the "chore(deps):" subjects the
repository already gets are configured rather than inferred from history.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
.github/workflows/build.yml pinned dependabot/fetch-metadata to the
mutable @v3 tag while every other action in the repository is pinned by
commit SHA. A mutable tag can be repointed at arbitrary code by anyone who
can push to that repository, and this particular step runs in a job that
auto-approves and squash-merges pull requests. OpenSSF Scorecard reports
this under Pinned-Dependencies, and .github/workflows/scorecard.yml is
enabled here.
v3 and v3.1.0 currently resolve to the same commit
(25dd0e34f4fe68f24cc83900b1fe3fe149efef98, verified via the GitHub API),
so this is a pure pin with no version change.
While here, add the missing "# vX.Y.Z" comments to the four docker/*
actions that were pinned by SHA without one, so every "uses:" line in the
repository states which release the SHA corresponds to. Tags verified via
the GitHub API against each pinned SHA; no SHAs were changed.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The 2025 key is about to expire in a few months and we need
to make a release with the new key soon or we risk breaking
the upgrade workflow.
Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
Move TestGPGVerifyIn6Months from verify_test.go to verify_canary_test.go
behind the canary build tag. This prevents upcoming key expiration from
failing the standard test suite and obscuring other test results.
Add a dedicated GitHub Actions workflow (key-canary.yml) that runs the
canary test on push, pull request, and a weekly schedule. When the
updater signing key is within 6 months of expiration, the canary workflow
will fail in isolation, providing a clear indication in the Actions
summary.
Closes#3523
Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
The age agent read its line-oriented protocol with a default
bufio.Scanner, which caps lines at 64 KiB (bufio.MaxScanTokenSize).
The decrypt command carries the base64-encoded ciphertext on a
single line, so any secret larger than ~48 KiB made scanner.Scan()
fail with bufio.ErrTooLong, silently dropping the connection and
leaving the client with EOF (e.g. "failed to send identities to
agent: EOF" from gopass age agent unlock).
Raise the scanner buffer cap to 16 MiB, matching the
privateKeySizeLimit already used when parsing identities. No buffer
is preallocated: the scanner starts at bufio's small initial size and
only doubles as the longest line actually received requires, so
this does not allocate anything upfront. Also log
scanner.Err() so future scan failures are no longer silent.
Implements Option 1 from #3508. A chunked or length-prefixed
protocol (Option 2) requires agent protocol versioning first.
Signed-off-by: Zexin Yuan <git@yzx9.xyz>
Assisted-by: Claude-Code:GLM-5.2
golangci-lint's usetesting linter flags both the os.Chdir call and its
deferred restore in TestBinaryCopyNameAmbiguity. t.Chdir does the same
thing and registers the restore with the testing framework, so it also
runs when the test fails early.
Safe here: the test does not call t.Parallel (t.Chdir panics if it does),
and it already mutates the global out.Stdout, so it is inherently serial.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diskutil mount without an explicit "rw" mount option can mount the
ramdisk read-only, causing gopass edit to fail with "Read-only file
system" when writing the temporary plaintext file. Pass "rw,noatime"
explicitly to -mountOptions so the ramdisk is always mounted
read-write.
Signed-off-by: Ryosuke Akiyama <r@554.jp>
Get, Set, Move, Delete, and Exists all validate a name resolves to a
path under the store root via safePath before touching the
filesystem. IsDir and Prune build their path directly with an
unguarded filepath.Join instead. Prune then calls os.RemoveAll on that
unguarded path, so a name containing ../ segments deletes a directory
tree entirely outside the store root.
Route both functions through safePath, matching the other five. Doing
so surfaces a related bug in safePath itself: its traversal check
required a trailing path separator after the store root, which
incorrectly rejected the store root's own path (name "" or ".") as a
traversal. This was never exercised by the four functions already
using it, but is reachable once IsDir is corrected (a store's own
root is a legitimate "is this a directory" query, e.g. for a mounted
sub-store). Fixed by also accepting the resolved path being exactly
equal to the store root.
Adds a regression test covering both functions against a traversal
payload.
Co-authored-by: Gogs <gogs@fake.local>
GnuPG 2.5+ can produce v5 keys (e.g. with PQC algorithms like
ky768_cv25519) whose fingerprints are 64 hex characters instead of
the traditional 40. The colons-output parser and Key.ID() only
expected 40-character fingerprints, causing v5 keys to be silently
ignored or to produce empty short IDs.
- Key.ID(): add a branch for 64-char fingerprints, taking the first
16 characters as the short ID (matching GnuPG's own convention).
- ReadNamesFromKey/GetFingerprint: replace ProtonMail/go-crypto
openpgp calls with gpg --show-keys --with-colons, since go-crypto
does not support v5 key packets and fails to parse them.
- Add unit tests for v5 key parsing and ID extraction.
Signed-off-by: Q-er <q-er@mailbox.org>
The original Android Password Store projects (zeapo/Android-Password-Store
and android-password-store/Android-Password-Store) are archived and no
longer maintained. Update the docs to reference the active fork
agrahn/Android-Password-Store, distributed via F-Droid (app.passwordstore.agrahn).
Signed-off-by: Zexin Yuan <git@yzx9.xyz>
I am trying gopass as an enhanced alternative to pass and as a backup vault
for my primary password manager. I used pass occasionally for many years and
noticed that gopass uses the past tense in some generated commit messages.
Even though it is really a nit, I see the usual pass commit messages alongside
gopass's slightly off past-tense ones in the same vault as I adapt to using
gopass.
This aligns those messages with the imperative-mood convention accepted in
#1222.
Most changes were edited and reviewed with AI assistance.
Signed-off-by: Ivan Kovnatsky <75213+ivankovnatsky@users.noreply.github.com>
Add the memorable (BIP39 word-based) generator to the pwgen subcommand via
the existing pwgen.GenerateMemorablePassword, matching what generate already
supports.
- --memorable (-m) and --memorable-capitalize (--mc) flags; dispatch to
memorableGen after the xkcd branch (matching flag-registration order).
- memorableGen reuses --symbols, reads pwgen.memorable-capitalize config
(overridden by --memorable-capitalize and --no-capitalize), and rejects
the contradictory --no-numerals since memorable always injects a digit
per word.
- --memorable and --xkcd are mutually exclusive (both select a generator):
combining them is a usage error rather than a silent precedence choice.
- Fix ArgsUsage to [length] [count].
- Document flags in docs/commands/pwgen.md and the config key in docs/config.md.
Assisted-by: Claude-Code:GLM-5.2
Signed-off-by: Zexin Yuan <git@yzx9.xyz>
* docs(adr): resolve the A-13 collision, zero-pad numbers and add an index
Two records carried the number A-13:
docs/adr/A-13-expired-gpg-key-handling.md
docs/adr/A-13-screenshot-build-tag.md
An ADR number is a stable identifier, so a duplicate makes every citation
ambiguous. A-13-expired-gpg-key-handling.md keeps the number: it is cited
from docs/commands/recipients.md, docs/usecases/team-workflows.md and
docs/adr/A-14-team-workflows.md. The screenshot record has no inbound
citations and is renumbered to A-15.
Zero-pad A-3 through A-9 to A-03 through A-09 so the directory sorts
correctly now that the set has passed ten entries. None of these has an
inbound citation from another document; the single reference in
internal/backend/storage/fs/rcs.go is updated in this commit.
Add docs/adr/README.md as the index, recording the naming rules, the status
and authoring date of every record, and three facts that are otherwise only
discoverable from git history:
- A-01 and A-02 are cited from the CHANGELOG unreleased section but no file
was ever written for either; the numbers stay reserved.
- The A-13 collision and which record was renumbered.
- SECURITY_AUDIT_REPORT.md and CODE_QUALITY_REPORT.md, cited as the Source
of A-03 through A-10, were removed in 77894053 and are not in the tree.
No record content is changed apart from the H1 lines, which must match the
file names.
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: add docs/conventions.md and adopt Conventional Commits
CONTRIBUTING.md required a bracketed [TAG] prefix on every commit subject.
That has not matched practice for some time: of the 344 commit subjects
since 2025-01-01, 225 are Conventional Commits and 67 use a [TAG]. The
CHANGELOG unreleased section uses a third set including [SECURITY] and
[PKG-BREAK], neither of which CONTRIBUTING.md lists.
Replace the [TAG] rule with Conventional Commits and add
docs/conventions.md as the single normative reference for:
- commit types (a closed list) and the scopes derived from the package
layout, including the five values that appear as types in the history but
are scopes: otp, age, bug, fscopy, openbsd;
- the distinction between a CLI break, which uses "!" and a
BREAKING CHANGE: footer and forces a major release, and a break confined
to pkg/gopass, which uses a PKG-BREAK: footer and does not (ADR A-12);
- Semantic Versioning, and which surfaces it does and does not cover;
- branch and tag names, including the release/ and prep/ prefixes owned by
the release automation;
- file naming for ADRs, documentation and Go sources.
The Developer Certificate of Origin requirement is unchanged. Conventional
Commits governs the subject line and the DCO adds a trailer, so the two are
independent.
Also correct the API Stability section of ARCHITECTURE.md, which still
described pkg/gopass/doc.go as carrying "an explicit instability warning"
and instructed consumers to "treat any pkg/ type or function change as
potentially breaking". Both statements predate ADR A-12: doc.go now
declares the package best-effort stable and permits additive changes in any
release. The section also referred to issue #3414 as an open decision; that
decision is recorded in A-12 with status accepted.
Extend the folder list in AGENTS.md with the five pkg/ directories it does
not mention (otp, passkey, pinentry/cli, protect, qrcon), using each
package's own doc comment as the description.
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(changelog): migrate to Keep a Changelog 1.1.0
CHANGELOG.md used a bespoke format with three overlapping entry conventions:
the bracketed [TAG] prefixes CONTRIBUTING.md mandated, the Conventional
Commit subjects that have been in use since 2025, and a third set in the
unreleased section including [SECURITY] and [PKG-BREAK] that appeared in
neither. Release 1.16.0 contains the first two mixed together.
Adopt Keep a Changelog 1.1.0 and generate the entries from commit subjects.
New package helpers/commitmsg
Parse and classify commit subjects. It is the single source of truth for
both the changelog generator and any future commit linting, so the two
cannot disagree about what a valid subject is. It implements the closed
type list from docs/conventions.md, the Changelog-Section:, PKG-BREAK:,
BREAKING CHANGE: and RELEASE_NOTES= footers, and the legacy [TAG] and
bracketed-type forms so a release spanning the transition still classifies
its older commits.
Classify returns a disposition rather than a bare boolean, distinguishing
a deliberate omission -- a dependency bump, a CI change -- from a subject
it could not recognise. That distinction matters: over the 137 commits
since v1.16.1 it classifies 47, omits 60 and cannot recognise 30. Among
the 30 are real user-facing changes such as "otp: hide --snip flag when
built with noscreenshot tag", which uses a scope where a type belongs.
The release helper now prints those subjects instead of dropping them
silently.
helpers/release: fix the section ordering defect
writeChangelog inserted the new release before the first "## " heading.
Once an unreleased section existed, that heading was the unreleased one,
so the release landed above it and the hand-written entries below were
orphaned -- never published, and silently carried into every subsequent
release. The 30 entries currently under "## Next" are in exactly that
state.
The new implementation in helpers/release/changelog.go parses the file
into header, unreleased block, released body and link references; merges
the hand-written entries with the generated ones and de-duplicates;
renders the release with only its non-empty subsections, in Keep a
Changelog order; leaves an empty Unreleased section behind; and rewrites
the two link references a release changes.
helpers/changelog: skip the unreleased section
The extractor printed everything between the first and second "## "
heading. After the migration the first heading is an empty
"## [Unreleased]", which would have produced empty GitHub release notes.
It now extracts the first *versioned* section.
CHANGELOG.md data migration
The 30 entries under "## Next" move into "## [Unreleased]" and are
distributed by their existing tags. Three are placed by meaning rather
than by tag, because Keep a Changelog has sections their tags do not:
the two [UX] entries that remove a CLI alias go to Removed, and the [UX]
entry about the GOPASS_AUTOSYNC_INTERVAL deprecation goes to Deprecated.
The bracketed prefixes are dropped; the trailing audit identifiers such
as (A-1) and (B-8) are kept, since they are the only trace back to the
audit that produced those entries.
All 86 released headings become "## [X.Y.Z] - YYYY-MM-DD" with their
entry text untouched. The two headings that carried no date, 1.10.0 and
1.10.1, take theirs from their git tags. A link reference block is
appended; every one of the 86 versions has a matching tag.
The bullet count is unchanged at 837.
Verified end to end: "go run ./helpers/changelog" against the migrated file
extracts the 1.16.1 section and does not capture [Unreleased].
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(release): correct the swapped Next/Prev labels and fold duplicate entries
Two follow-ups found while verifying the changelog migration end to end.
The "Version overview" block printed by every release run labels its third
and fourth values "Next version flag" and "Prev version flag", but passes
them as prevVerFlag, nextVerFlag. Running
go run helpers/release/main.go --dry-run v1.17.0
therefore reported "Next version flag: ''" and "Prev version flag: '1.17.0'"
for a flag that sets the next version. The values are swapped to match the
labels. Output only; no behaviour depends on it.
Changelog entries are now de-duplicated case-insensitively. A hand-written
unreleased entry and the subject of the commit that implemented it commonly
differ only in their first letter, for example "Add gopass doctor diagnostic
command (I-4)" against "add gopass doctor diagnostic command (I-4)". This
cannot catch two genuinely different wordings of the same change; those
still need a human pass before the release.
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(adr): align A-12 with the Keep a Changelog format
Three statements in A-12 no longer hold once CHANGELOG.md follows Keep a
Changelog:
- The entry goes in "## [Unreleased]", not "## Next".
- "The existing helpers/changelog generator reads CHANGELOG.md verbatim; no
changes to that tool are needed" is false. The extractor had to learn to
skip the unreleased section, and the release helper now classifies commits
rather than copying subjects.
- "[SECURITY], [BUGFIX] and [FEATURE]" are no longer tags. They are the
Security, Fixed and Added subsections.
State the mechanism instead: a PKG-BREAK: commit footer, which
helpers/commitmsg reads and helpers/release renders as a [PKG-BREAK]-prefixed
bullet inside the appropriate subsection. [PKG-BREAK] survives as a bullet
prefix because it qualifies an entry rather than categorising it, which is
exactly what a Keep a Changelog subsection cannot express.
Writing the prefix by hand into "## [Unreleased]" still works; the release
helper preserves it.
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two records carried the number A-13:
docs/adr/A-13-expired-gpg-key-handling.md
docs/adr/A-13-screenshot-build-tag.md
An ADR number is a stable identifier, so a duplicate makes every citation
ambiguous. A-13-expired-gpg-key-handling.md keeps the number: it is cited
from docs/commands/recipients.md, docs/usecases/team-workflows.md and
docs/adr/A-14-team-workflows.md. The screenshot record has no inbound
citations and is renumbered to A-15.
Zero-pad A-3 through A-9 to A-03 through A-09 so the directory sorts
correctly now that the set has passed ten entries. None of these has an
inbound citation from another document; the single reference in
internal/backend/storage/fs/rcs.go is updated in this commit.
Add docs/adr/README.md as the index, recording the naming rules, the status
and authoring date of every record, and three facts that are otherwise only
discoverable from git history:
- A-01 and A-02 are cited from the CHANGELOG unreleased section but no file
was ever written for either; the numbers stay reserved.
- The A-13 collision and which record was renumbered.
- SECURITY_AUDIT_REPORT.md and CODE_QUALITY_REPORT.md, cited as the Source
of A-03 through A-10, were removed in 77894053 and are not in the tree.
No record content is changed apart from the H1 lines, which must match the
file names.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(adr): resolve the A-13 collision, zero-pad numbers and add an index
Two records carried the number A-13:
docs/adr/A-13-expired-gpg-key-handling.md
docs/adr/A-13-screenshot-build-tag.md
An ADR number is a stable identifier, so a duplicate makes every citation
ambiguous. A-13-expired-gpg-key-handling.md keeps the number: it is cited
from docs/commands/recipients.md, docs/usecases/team-workflows.md and
docs/adr/A-14-team-workflows.md. The screenshot record has no inbound
citations and is renumbered to A-15.
Zero-pad A-3 through A-9 to A-03 through A-09 so the directory sorts
correctly now that the set has passed ten entries. None of these has an
inbound citation from another document; the single reference in
internal/backend/storage/fs/rcs.go is updated in this commit.
Add docs/adr/README.md as the index, recording the naming rules, the status
and authoring date of every record, and three facts that are otherwise only
discoverable from git history:
- A-01 and A-02 are cited from the CHANGELOG unreleased section but no file
was ever written for either; the numbers stay reserved.
- The A-13 collision and which record was renumbered.
- SECURITY_AUDIT_REPORT.md and CODE_QUALITY_REPORT.md, cited as the Source
of A-03 through A-10, were removed in 77894053 and are not in the tree.
No record content is changed apart from the H1 lines, which must match the
file names.
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs: add docs/conventions.md and adopt Conventional Commits
CONTRIBUTING.md required a bracketed [TAG] prefix on every commit subject.
That has not matched practice for some time: of the 344 commit subjects
since 2025-01-01, 225 are Conventional Commits and 67 use a [TAG]. The
CHANGELOG unreleased section uses a third set including [SECURITY] and
[PKG-BREAK], neither of which CONTRIBUTING.md lists.
Replace the [TAG] rule with Conventional Commits and add
docs/conventions.md as the single normative reference for:
- commit types (a closed list) and the scopes derived from the package
layout, including the five values that appear as types in the history but
are scopes: otp, age, bug, fscopy, openbsd;
- the distinction between a CLI break, which uses "!" and a
BREAKING CHANGE: footer and forces a major release, and a break confined
to pkg/gopass, which uses a PKG-BREAK: footer and does not (ADR A-12);
- Semantic Versioning, and which surfaces it does and does not cover;
- branch and tag names, including the release/ and prep/ prefixes owned by
the release automation;
- file naming for ADRs, documentation and Go sources.
The Developer Certificate of Origin requirement is unchanged. Conventional
Commits governs the subject line and the DCO adds a trailer, so the two are
independent.
Also correct the API Stability section of ARCHITECTURE.md, which still
described pkg/gopass/doc.go as carrying "an explicit instability warning"
and instructed consumers to "treat any pkg/ type or function change as
potentially breaking". Both statements predate ADR A-12: doc.go now
declares the package best-effort stable and permits additive changes in any
release. The section also referred to issue #3414 as an open decision; that
decision is recorded in A-12 with status accepted.
Extend the folder list in AGENTS.md with the five pkg/ directories it does
not mention (otp, passkey, pinentry/cli, protect, qrcon), using each
package's own doc comment as the description.
Signed-off-by: Pavel Lavrukhin <46395539+dantte-lp@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The age agent reads its line-oriented protocol with a bufio.Scanner and
parses only the tokens on the "identities" command line (it re-joins args
with newlines before parsing). The client, however, serialized identities
newline-separated and wrote them with a single Fprintln, so every identity
after the first landed on its own line and was discarded as an "unknown
command". As a result the agent loaded at most one identity per send.
This was masked by Go's randomized map iteration order in getAllIds: each
gopass process (e.g. a short-lived browser jsonapi process vs. a CLI
invocation) sent the identities in a different order. When a native X25519
identity happened to sort first the agent loaded it and decryption via the
agent succeeded; when an SSH identity sorted first the whole bundle failed
to parse and nothing loaded. Hence the browser extension and the CLI
appeared to behave differently.
SSH identities (filippo.io/age/agessh) compound this: their private-key
types implement no String() method, so they formatted as an unparseable Go
struct (e.g. "&{[185 .. 233]}") and could never be sent to the agent at
all.
Fix, client-side (the agent already expected this format):
* identitiesToString now emits all identities space-separated on one
line, matching what the agent parses. All included encodings are
bech32 and thus whitespace-free, so space-separation is unambiguous.
* Only natively serializable identity types (X25519, Hybrid, plugin and
gopass plugin/wrapped identities) are included; SSH identities are
skipped (decryption for them falls back to the local code path exactly
as before — they never worked via the agent).
* tryStartAgent now uses identitiesToString too, replacing its own
fmt.Sprintf("%s", id) + newline-join loop (which additionally mangled
non-Stringer types as %!s(...)).
* Callers skip the send when there is nothing serializable to avoid a
noisy "missing identities" agent error.
Regression tests:
* TestIdentitiesToStringSpaceSeparated - serialization is single-line,
space-separated, and skips non-serializable identities.
* TestAgentMultipleIdentities - the agent loads all identities from a
single space-separated command (decrypts ciphertext encrypted to the
second identity).
* TestDecryptMultipleIdentitiesViaAgent - full self-heal path: a keyring
with two identities, Age.Decrypt routes through the agent, and a secret
encrypted to the non-first identity decrypts via the agent.
Assisted-by: Claude-Code:GLM-5.2
Signed-off-by: Zexin Yuan <git@yzx9.xyz>