Adr/A 03 Separate Storage Rcs
A-03: Separate Storage and RCS interfaces
Status: deferred — current implementation intentionally keeps them merged
Source: CODE_QUALITY_REPORT.md § A-3
---
Background
backend.Storage (defined in internal/backend/storage.go) currently embeds
the unexported rcs interface (defined in internal/backend/rcs.go). This
means every Storage implementation must satisfy ~15 VCS methods: Add,Commit, Push, Pull, TryAdd, TryCommit, TryPush, InitConfig,AddRemote, RemoveRemote, Revisions, GetRevision, Status, Compact.
The fs backend (internal/backend/storage/fs/) has no VCS support and
implements all of these as stubs that return store.ErrGitNotInit orbackend.ErrNotSupported.
This was a deliberate architectural choice: an earlier version of the codebase
had separate Storage and RCS interfaces, but merging them simplified the
leaf store significantly by eliminating a type-assertion at every VCS call site.
Since the overwhelming majority of storage backends (gitfs, fossilfs,jjfs) are also RCS backends, the merged interface is correct in practice
even if it is not strictly correct in theory.
---
Trade-offs of the current merged design
Pros
- Single interface, no type-assertion boilerplate in
leaf.Store or callers.- Adding a new backend that supports VCS is straightforward — implement one
interface and you're done.
- No layering confusion:
leaf.Store.storage holds everything the store needs.Cons
- Pure-storage backends (
fs) must implement ~15 no-op stubs.- The
Storage interface is larger than necessary, making it harder to mock intests.
- Conceptually misleading: the interface promises VCS capabilities that some
backends cannot deliver.
---
Implementation plan (if splitting is chosen in the future)
Phase 1 — Export rcs as a standalone RCS interface
internal/backend/rcs.go
- Rename the unexported rcs interface to exported RCS.
- Add a concrete NopRCS type in the same file that provides the same no-op
behaviour currently in fs/rcs.go:
- TryAdd, TryCommit, TryPush, InitConfig, Compact → nil
- Add, Commit, Push, Pull → store.ErrGitNotInit
- Revisions → single {Hash:"latest", Date:time.Now()} + ErrNotSupported
- GetRevision("HEAD"|"latest") → delegates to a supplied getter func; other
revisions → ErrNotSupported
- Status, AddRemote, RemoveRemote → ErrNotSupported
internal/backend/storage.go
- Remove the rcs embed from Storage so it contains only file-operation
methods: Get, Set, Delete, Exists, Move, List, IsDir, Prune,
Link, Name, Path, Version, Fsck, fmt.Stringer.
Phase 2 — Add an rcs field to leaf.Store
internal/store/leaf/store.go
- Add rcs backend.RCS field to the Store struct.
- In leaf.Init(), after the storage backend is created, derive rcs via a
type assertion:
if r, ok := st.(backend.RCS); ok {
s.rcs = r
} else {
s.rcs = backend.NopRCS{}
}- Update leaf.GitInit() in internal/store/leaf/rcs.go to re-derive s.rcs
after reassigning s.storage.
Leaf files that call RCS methods on s.storage — change s.storage.X tos.rcs.X for all VCS operations:
| File | Methods affected |
|---|---|
| internal/store/leaf/write.go | TryAdd, TryCommit, TryPush |
| internal/store/leaf/move.go | TryAdd, TryCommit, TryPush |
| internal/store/leaf/link.go | Add |
| internal/store/leaf/fsck.go | Compact, Push, TryCommit |
| internal/store/leaf/reencrypt.go | TryAdd, TryCommit, TryPush |
| internal/store/leaf/recipients.go | TryAdd, TryCommit, Push, Add |
| internal/store/leaf/templates.go | TryAdd, Add |
| internal/store/leaf/rcs.go | Revisions, GetRevision, Status |
Phase 3 — Delete fs/rcs.go
internal/backend/storage/fs/rcs.go can be deleted in its entirety. Thefs.Store type no longer needs to implement RCS.
Phase 4 — Verify other consumers
These locations hold a backend.Storage variable but call only file-op methods
and require no changes beyond a successful compile check:
- internal/action/reorg.go — var storage backend.Storage
- internal/create/wizard.go — backend.Storage parameter
The StorageLoader interface in internal/backend/registry.go returnsStorage — no change needed.
Phase 5 — Update and add tests
- Delete internal/backend/storage/fs/rcs_test.go (tests for the removed
stubs).
- Any mock Storage type in leaf or other test packages can stop implementing
RCS methods; embed or compose backend.NopRCS instead.
- Add a small unit test for NopRCS in internal/backend/rcs_test.go.
---
Files changed at a glance
| File | Action |
|---|---|
| internal/backend/rcs.go | Export RCS; add NopRCS |
| internal/backend/storage.go | Remove rcs embed from Storage |
| internal/store/leaf/store.go | Add rcs backend.RCS field + init logic |
| internal/store/leaf/rcs.go | Use s.rcs; re-derive in GitInit |
| internal/store/leaf/{write,move,link,fsck,reencrypt,recipients,templates}.go | s.storage.X → s.rcs.X for VCS calls |
| internal/backend/storage/fs/rcs.go | Delete |
| internal/backend/storage/fs/rcs_test.go | Delete |
| internal/backend/rcs_test.go | Add NopRCS tests |
gitfs, fossilfs, and jjfs require no changes — they already implement
the full rcs interface and will satisfy backend.RCS via the type assertion
with zero modification.
---
Adr/A 04 Grep Match Error Counters
A-04: Fix grep Match and Error Counters
Status: open — not yet fixed
Source: SECURITY_AUDIT_REPORT.md § M-1, § Q-5
---
Background
internal/action/grep.go declares two integer counters, matches anderrors, intended to tally the number of matching secrets and decryption
failures respectively. Neither counter is ever incremented inside the loop:
var matches int
var errors int
for _, v := range haystack {
sec, err := s.Store.Get(ctx, v)
if err != nil {
out.Errorf(ctx, "failed to decrypt %s: %v", v, err)
// errors++ missing here
continue
}
if matchFn(string(sec.Bytes())) {
out.Printf(ctx, "%s matches", color.BlueString(v))
// matches++ missing here
}
}
out.Printf(ctx, "\nScanned %d secrets. %d matches, %d errors", len(haystack), matches, errors)As a result the summary line always reads 0 matches, 0 errors regardless of
what the search actually found.
---
Impact
This is a correctness bug, not a security vulnerability. Users who rely on
the summary count to verify grep results will receive misleading output and
cannot tell whether any secrets matched their query.
---
Decision
Fix by adding matches++ inside the matchFn branch and errors++ inside
the error branch. This is a trivial one-line change per counter. No API or
behaviour changes are required.
---
Implementation
In internal/action/grep.go, locate the loop body and add the two missing
increment statements:
if err != nil {
out.Errorf(ctx, "failed to decrypt %s: %v", v, err)
errors++ // add this
continue
}
if matchFn(string(sec.Bytes())) {
out.Printf(ctx, "%s matches", color.BlueString(v))
matches++ // add this
}A test should assert that the summary line reports the correct counts after a
search against a known fixture store.
---
Adr/A 05 Template Engine Text Vs Html
A-05: Template Engine Uses text/template Instead of html/template
Status: deferred — current design is acceptable given the constraints
Source: SECURITY_AUDIT_REPORT.md § M-2
---
Background
The gopass template engine (internal/tpl/) uses Go's text/template package.
Go's standard library ships two closely related template packages:
| Package | Auto-escaping | Intended use |
|---------|--------------|--------------|
| text/template | No | Arbitrary text generation |
| html/template | Yes (HTML/JS/CSS contexts) | HTML document generation |
html/template is often recommended for security-sensitive contexts because it
prevents XSS by automatically escaping output that is interpolated into HTML
attributes, script blocks, and URL parameters.
---
Why html/template Does Not Directly Apply Here
gopass templates generate plain text secrets, not HTML documents. Switching
to html/template would:
1. Corrupt non-HTML output. Characters like <, >, &, ", and '
are common in passwords and secret values. html/template would HTML-escape
these on output (e.g. & → &), producing incorrect secrets.
2. Provide no meaningful security benefit. The auto-escaping in
html/template protects against XSS injection into HTML pages. The gopass
template engine renders secrets to a terminal or file — not a browser. There
is no HTML parsing context to exploit.
The current text/template approach is therefore correct for the use case.
---
Residual Risk
text/template allows calling methods on any value passed into the template.
The current payload struct passed to templates contains only string fields,
limiting the callable surface to string methods. The risk would escalate if:
- payload gains a field or method that returns a complex type with
side-effect-bearing methods.
- A template function is added whose return type exposes dangerous methods.
---
Decision
Keep text/template. Document the constraint here so that future contributors:
1. Do not add methods with observable side effects to the payload struct.
2. Audit any new template function's return types for unexpectedly callable
methods.
3. Reconsider this decision if the template engine ever grows an HTML rendering
mode (e.g. for browser integrations), in which case a separate HTML-specific
template path using html/template should be introduced rather than
switching the existing engine wholesale.
---
Adr/A 06 Minimum Password Length
A-06: Minimum Password Length Enforcement
Status: deferred — user autonomy preserved; warning to be added
Source: SECURITY_AUDIT_REPORT.md § M-4
---
Background
The gopass generate command accepts a length argument that is ultimately
stored in the generate.length config key or read from theGOPASS_PW_DEFAULT_LENGTH environment variable. Currently no lower bound is
enforced below the generator's character-class minimum (which can be as low as
1 character). A user can therefore generate a single-character "password".
---
Impact
Accidentally generating very short passwords provides the appearance of
security while offering none. Users who script gopass (e.g. in CI pipelines)
may silently configure trivially weak credentials.
---
Decision
Enforcing an absolute minimum is a policy decision that gopass deliberately
avoids making for users. Some legitimate use cases require short codes (e.g.
4-digit device PINs, legacy system constraints). A hard cutoff would break
these workflows.
The chosen approach is:
1. Display a warning when the requested length is below 12 characters,
making the risk visible without blocking the operation.
2. Do not impose a hard minimum that silently rejects user input.
---
Implementation
In internal/action/generate.go (or wherever length is read and validated
before the generator is called), add:
const warnBelowLength = 12if length < warnBelowLength {
out.Warningf(ctx, "Generating a password of only %d characters. This may be too weak for most uses.", length)
}
The warning should be visible in non-interactive mode as well so that scripted
invocations are not silently insecure.
No config key changes are required. Users who want to suppress the warning can
do so by setting a length ≥ 12 in their config.
---
Adr/A 07 Hook System Dead Code
A-07: Hook System Dead Code and CVE-2023-24055
Status: deferred — hooks remain disabled; safe re-enablement path documented
Source: SECURITY_AUDIT_REPORT.md § M-5
---
Background
gopass ships a hook system in internal/hook/hook.go that was designed to
allow users to run custom commands at lifecycle events (e.g. pre-commit,
post-decrypt). The system was disabled by inserting a hardcoded early
return:
if true {
// TODO(GH-2546) disabled until further discussion, cf. CVE-2023-24055
return nil
}The code below this guard remains in the repository. It parses hook command
strings using shellquote.Split() and then executes the result, which is
the same pattern that was the root cause of CVE-2023-24055 in the KeePass
ecosystem (untrusted config values leading to arbitrary command execution via
shell-style parsing).
---
Why It Was Disabled
If an attacker can write to the gopass configuration file (e.g. via a
malicious git repository that includes a synced config) they could set a hook
to any command value and have gopass execute it on the next relevant lifecycle
event. The shellquote.Split approach would allow argument injection even
without shell metacharacters.
---
Decision
The hooks remain disabled. The dead code should not be removed yet because
the feature is under active discussion (GH-2546). Removing it prematurely
would force a larger re-implementation effort when the discussion concludes.
The dead code is harmless as long as the early-return guard stays in place.
---
Requirements for Safe Re-enablement
Before hooks can be re-enabled the implementation must satisfy all of the
following:
1. No shell parsing of hook values. Hook commands must be stored as
structured config (e.g. an array of strings for binary + arguments) rather
than a single shell-quoted string. shellquote.Split must not be used.
2. Binary existence check. The resolved hook binary must pass an
exec.LookPath check before execution (consistent with the fix applied
in H-3 for the editor command).
3. Explicit user consent. Hooks should require explicit opt-in through a
first-class config key (e.g. hooks.enabled = true) that is not synced
via git-managed config by default, so that a shared/cloned repository
cannot silently activate hooks on a new machine.
4. Audit trail. Each hook execution should be logged at info level so users
can observe what commands are being run on their behalf.
---
Implementation Sketch
// safe hook execution — no shell parsing
hookBin, err := exec.LookPath(hookConfig.Command[0])
if err != nil {
return fmt.Errorf("hook binary %q not found: %w", hookConfig.Command[0], err)
}
cmd := exec.CommandContext(ctx, hookBin, hookConfig.Command[1:]...)When GH-2546 reaches a resolution, this ADR should be updated with the chosen
design and then closed.
---
Adr/A 08 Shred Modern Storage Limitations
A-08: Shred Operation Is Ineffective on Modern Storage
Status: accepted — limitation documented; advisory notice to be added
Source: SECURITY_AUDIT_REPORT.md § M-6
---
Background
pkg/fsutil/fsutil.go provides a Shred() function that overwrites a file
with random bytes a configurable number of times before deleting it. The intent
is to prevent recovery of the plaintext after deletion.
---
Why Shred Does Not Work on Modern Storage
The overwrite-before-delete approach relies on the assumption that writing to a
file path overwrites the same physical storage blocks each time. This assumption
has not held in practice for many years:
| Storage / FS type | Reason shred fails |
|-------------------|--------------------|
| SSD with wear levelling | Controller may write to different physical cells; old data remains until garbage-collected |
| ext4, NTFS, HFS+ (journaling) | Journal entries may contain copies of original data blocks |
| ZFS, Btrfs (copy-on-write) | Old snapshot blocks are never overwritten; new blocks are written alongside the old |
| APFS (macOS) | Copy-on-write; snapshots are created automatically by Time Machine |
| Network filesystems | Local overwrite does not affect server-side block allocation |
NIST SP 800-88 and academic literature (Gutmann, 1996; Wei et al., 2011)
confirm that software overwrite is unreliable on solid-state media.
---
Current Mitigations Already in Place
gopass already uses the correct approach for the security-critical path:
plaintext is written to a ramdisk (macOS) or /dev/shm (Linux) for
editor sessions, so the sensitive data never reaches persistent storage where
shred would apply. The Shred() function is called on other files (e.g. old
encrypted files after re-encryption), where the data being removed is already
encrypted ciphertext. Shredding ciphertext provides minimal additional security
over simple deletion because the data is useless without the private key.
---
Decision
Do not remove or deprecate Shred(). It provides a marginal additional layer
on rotating-disk (HDD) storage and its cost is low. However, it must not be
presented to users as a guaranteed secure-deletion mechanism.
Actions taken / to be taken:
1. Add a notice to the Shred() function's godoc comment explaining the
limitation on SSDs and journaling/CoW filesystems.
2. If gopass ever presents a "securely deleted" user-facing message after
calling Shred(), that message should be softened to "deleted" or include
a caveat.
3. Sensitive plaintext must continue to be handled exclusively in ramdisk-backed
temporary files (the existing behaviour) and never written to persistent
disk in cleartext.
---
References
- Gutmann, P. (1996). "Secure Deletion of Data from Magnetic and Solid-State Memory"
- Wei, M. et al. (2011). "Reliably Erasing Data from Flash-Based Solid State Drives" (FAST '11)
- NIST SP 800-88 Rev. 1: Guidelines for Media Sanitization
---
Adr/A 09 Low Severity Informational Findings
A-09: Low Severity and Informational Findings
Status: accepted — risks documented; mitigations noted where applicable
Source: SECURITY_AUDIT_REPORT.md § L-1 through L-6
---
L-1: GPG Ciphertext Logged to Debug Output
Location: internal/backend/crypto/gpg/cli/encrypt.go
Encrypted ciphertext is hex-dumped to the debug log via io.MultiWriter:
hexLogger := hex.Dumper(debug.LogWriter)
cmd.Stdout = io.MultiWriter(buf, hexLogger)While ciphertext is not plaintext, persisting it to a debug log could assist
offline brute-force or analysis of the encryption scheme.
Mitigation: The debug log is only written when GOPASS_DEBUG_LOG is
explicitly set. Ciphertext should be logged at a higher verbosity level
(e.g. debug.V(2)) rather than unconditionally, so that standard debug output
does not include bulk ciphertext.
---
L-2: GOPASS_GPG_BINARY Allows Binary Override Without Validation
The GOPASS_GPG_BINARY environment variable allows the user to override the
path to the GPG binary. An attacker with control over the process environment
could point it to a malicious program.
Assessment: If an attacker controls the environment of the gopass process,
they already have equivalently powerful attack vectors (e.g. replacingGOPATH, PATH, or setting LD_PRELOAD). The incremental risk from this
specific variable is low.
Mitigation: No change required. The variable is documented as a
developer/debugging aid and requires environment access that implies broader
privilege.
---
L-3: Updater Relies on CA-Signed TLS Without Certificate Pinning
Location: internal/updater/download.go
The built-in updater connects to github.com over TLS 1.3 (min version
enforced) but does not pin the server certificate or its public key. A CA
that issues a fraudulent certificate for github.com could perform a
man-in-the-middle attack against the update download.
Assessment: The risk is mitigated by defence-in-depth: the downloaded
binary's SHA-256 checksum is verified against a checksum file, and that
checksum file's GPG signature is verified against a hardcoded project
public key embedded in the binary. An attacker would therefore need to
compromise both a trusted CA and the project's GPG signing key, which
is an implausible combination.
Certificate pinning would add implementation complexity and create an
operational burden (the pin must be updated with every certificate renewal)
without meaningfully reducing the realistic threat surface.
Mitigation: No change required. The current GPG-signed checksum
verification provides sufficient defence-in-depth.
---
L-4: text/template Panic on Nil Interface in Custom Functions
If a custom template function receives a nil interface value and attempts a
method call on it, the Go runtime will panic. The process command currently
passes nil as the secret payload; the template engine handles {{.Field}}
access on nil gracefully via reflection, but custom Go functions called from
within templates are not similarly protected.
Assessment: Exploitable only if a user deliberately authors a template that
triggers the nil path — this is a denial-of-service against the user's own
session rather than a privilege-escalation vector.
Mitigation: Custom template functions should guard against nil receivers
using explicit nil checks. New template functions added in the future should be
reviewed for nil-safety.
---
L-5: DetectCrypto Returns nil, nil
Location: internal/backend/registry.go
DetectCrypto() can return (nil, nil) when no matching backend is detected.
The function has a // TODO comment acknowledging this. Callers that do not
check for a nil crypto backend before using it will panic with a nil-pointer
dereference.
Assessment: All current call sites do check the error + nil before use.
The risk is that a future caller forgets.
Mitigation: The function should be updated to return an explicitfmt.Errorf("no crypto backend detected for %s", path) error instead ofnil, nil. This is a small refactor that eliminates the ambiguous return and
allows callers to use standard Go error-checking patterns without needing to
special-case nil.
---
L-6: HTTP Proxy Honoured for Update Downloads
Location: internal/updater/download.go
Proxy: http.ProxyFromEnvironment,The updater respects HTTP_PROXY / HTTPS_PROXY / NO_PROXY. A malicious
proxy could intercept or block the update download.
Assessment: The GPG-signed checksum verification (see L-3) means a proxy
cannot substitute a malicious binary without also forging the project GPG
signature. Proxy support is a legitimate operational requirement in corporate
environments where direct internet access may be disallowed.
Mitigation: No change required. Proxy support is correct and expected
behaviour in enterprise environments; GPG signature verification prevents
payload substitution.
---
Adr/A 10 Code Quality Findings
A-10: Code Quality Findings
Status: open — improvements tracked here for future work
Source: SECURITY_AUDIT_REPORT.md § Q-1 through Q-4
---
Q-1: Path Traversal Protection Inconsistency
Problem: Path sanitisation is performed differently at each entry point:
| Location | Approach |
|----------|----------|
| leaf.Store.Set() | Rejects names containing // |
| fs storage layer | filepath.Clean + bounds check (added in C-1 fix) |
| Binary action | isInStore() with filepath.Abs() |
| create wizard | CleanFilename() |
| gopass show | No explicit check |
The inconsistency creates a maintenance risk: a new entry point that forgets
to apply the correct check would silently reintroduce a path traversal
vulnerability.
Recommendation: Introduce a single exported function:
// internal/backend/storage/fs/validate.go
func ValidateSecretName(storePath, name string) error {
resolved := filepath.Join(storePath, filepath.Clean(name))
if !strings.HasPrefix(resolved, storePath+string(filepath.Separator)) {
return fmt.Errorf("path traversal detected: %q escapes store root", name)
}
return nil
}Replace all divergent ad-hoc checks with calls to this function. This is a
medium-effort refactor but significantly reduces the attack surface for future
regressions.
---
Q-2: Platform-Specific Behaviour Differences
Problem: Several security-relevant behaviours differ across platforms in
ways that are not always intentional or documented:
| Behaviour | Linux/macOS | Windows |
|-----------|-------------|---------|
| Path traversal (../) | Rejected by C-1 fix | Rejected by C-1 fix (test expectation updated) |
| Tempfile ramdisk | /dev/shm or macOS ramdisk | Falls back to OS temp dir — not a ramdisk |
| Clipboard clear | Kills predecessor unclip processes | No predecessor killing |
| Editor parsing | shellquote.Split + LookPath | Direct argument passing, no LookPath |
The editor parsing inconsistency on Windows is notable: the H-3 fix addedexec.LookPath validation on non-Windows platforms only (consistent with the
existing runtime.GOOS != "windows" guard), so Windows users do not get the
benefit of early binary validation.
Recommendation:
- Extend the LookPath check to Windows (the exec.LookPath function works
on Windows; the guard is in the shellquote.Split branch which is
Windows-only excluded).
- Document the tempfile ramdisk limitation on Windows so users understand that
plaintext may temporarily appear in the OS temp directory on that platform.
---
Q-3: Error Handling Inconsistencies
Problem: Several recurring patterns make errors invisible or misleading:
1. _ = os.Setenv(...) — environment variable set failures are silently
ignored. A failed Setenv could mean secrets are not exported to the
subprocess in gopass env, which would produce a confusing user experience
without any diagnostic.
2. fsck.go permission repair — permission change failures are logged as
warnings but not returned as errors, so gopass fsck --fix exits 0 even if
it could not actually fix the issues it found.
3. DetectCrypto returning nil, nil — documented in L-5; should return
an explicit error.
4. Template functions returning err.Error() as a string — partially
addressed in H-1 (error strings are now generic), but the underlying pattern
(swallowing the error and embedding it in the output) should be replaced with
proper error propagation.
Recommendation: Audit all _ = ... error discards and replace them with
at minimum a debug.Log call. Review fsck.go to ensure errors from repair
operations are aggregated and returned.
---
Q-4: Dead Code
Problem: Several code paths are permanently unreachable but remain in the
repository, increasing maintenance burden and creating confusion about intent:
1. Hook system (internal/hook/hook.go#L47–L91) — unreachable below the
hardcoded if true { return nil } guard. Documented in A-7; should be kept
until GH-2546 is resolved, but the intent must be clearly stated.
2. GitHub recipient support in age backend — the github: key prefix was
removed as a supported feature, but the age backend still contains handling
code that logs a warning when it is encountered. If the feature is truly
gone, this code should be removed.
3. Passage identity loading — deprecated format support for the passage
fork's identity file layout. If this format is no longer supported, the
parsing code should be removed to reduce the attack surface of the identity
loading path.
Recommendation: Each piece of dead code should be explicitly categorised:
- Keep with comment (hook system — pending GH-2546)
- Remove (github: prefix warning, passage identity loading) if those
features are confirmed gone
A go vet + staticcheck run in CI would catch some categories of dead code
automatically and should be part of the standard make codequality target.
---
Adr/A 11 Secret Service
A-11: Integrate org.freedesktop.secrets D-Bus Service
Status: proposed
Source: GitHub Issue #3434
---
Background
The freedesktop.org Secret Service specification
defines a D-Bus API that desktop applications (Firefox, Chrome, VS Code, Electron apps,
NetworkManager, secret-tool, …) use to store and retrieve secrets. On most systems it is served
by GNOME Keyring or KDE Wallet. Users who keep their passwords in gopass currently maintain two
separate secret stores.
The request is to add a gopass secret-service daemon subcommand that implements this D-Bus API on
top of the existing gopass store, so that GUI and CLI applications write their secrets into the
same GPG-encrypted, git-backed password store.
---
Prior Art
Two reference implementations inform the design:
| Project | Language | License | Notes |
|---------|----------|---------|-------|
| nikicat/gopass-secret-service | Go (90 %) | MIT | Standalone daemon that invokes gopass CLI. Most relevant. |
| grimsteel/pass-secret-service | Rust | GPL-3.0 | Standalone daemon for pass. Pure-Rust D-Bus. |
nikicat/gopass-secret-service implements the full spec in Go and re-uses gopass via its CLI.
The key difference for this ADR is that the integrated version will use the gopass Go API
(github.com/gopasspw/gopass/pkg/gopass/api) directly rather than shelling out.
---
Decision
Implement gopass secret-service as:
1. A new Linux-only subcommand of the main gopass binary.
2. A long-running daemon that acquires org.freedesktop.secrets on the D-Bus session bus and
serves the full Secret Service interface.
3. Uses github.com/godbus/dbus/v5 (already in go.mod at v5.1.0, pure Go, BSD-2 licensed).
4. All crypto uses stdlib crypto/... packages — no CGo, no new external dependencies.
5. Secrets are stored under a configurable gopass path prefix (default: secret-service).
---
Feasibility Summary
* Pure-Go, zero-CGo: godbus/dbus/v5 is already a direct dependency; all required crypto
(crypto/aes, crypto/sha256, math/big for DH) is in the stdlib.
* No new external dependencies: Only stdlib + existing godbus dependency needed.
* License-compatible: godbus/dbus/v5 is BSD-2 (≡ MIT compatible per .license-lint.yml).
* Linux-only build tag: The entire feature is gated with //go:build linux (same pattern as
internal/notify/notify_dbus.go and pkg/clipboard/unclip_linux.go).
* Architectural risk: gopass is a short-lived CLI tool; this feature requires a persistent
daemon process. This is handled by a blocking gopass secret-service serve subcommand (the user
manages the lifecycle via systemd or similar).
---
D-Bus Interface Mapping
All objects live under the well-known service name org.freedesktop.secrets.
| Interface | Object path | Implementation type |
|-----------|-------------|---------------------|
| org.freedesktop.Secret.Service | /org/freedesktop/secrets | service.Service |
| org.freedesktop.Secret.Collection | /org/freedesktop/secrets/collection/{name} | service.Collection |
| org.freedesktop.Secret.Item | /org/freedesktop/secrets/collection/{name}/{id} | service.Item |
| org.freedesktop.Secret.Session | /org/freedesktop/secrets/session/{id} | service.Session |
| org.freedesktop.Secret.Prompt | /org/freedesktop/secrets/prompt/{id} | service.Prompt |
---
Storage Layout in gopass
Secrets are stored under a configurable prefix (secret-service by default):
~/.password-store/
└── secret-service/
├── _aliases.age # Map of alias → collection name (JSON)
├── default/
│ ├── _meta.age # Collection metadata (label, created, modified)
│ └── i<uuid>.age # Secret items
└── work/
├── _meta.age
└── i<uuid>.ageEach item secret file uses the standard gopass multi-line format:
the-secret-value
---
_ss_label: My GitHub Token
_ss_created: 2026-01-15T10:30:00Z
_ss_modified: 2026-01-15T10:30:00Z
_ss_content_type: text/plain
username: [email protected]
service: github.comThe first line is the secret value; _ss_* keys are internal metadata; all
other key/value pairs are user-visible item attributes (used for lookup bySearchItems).
---
Crypto Sessions
The spec defines two OpenSession algorithms:
| Algorithm | Description | Implementation |
|-----------|-------------|----------------|
| plain | No transport encryption | Return empty bytes; secret value passed as-is |
| dh-ietf1024-sha256-aes128-cbc-pkcs7 | DH key exchange + AES-128-CBC | stdlib crypto/aes, crypto/sha256, math/big |
The plain algorithm is secure because D-Bus session bus traffic is carried over a local UNIX
socket with kernel-enforced access control. Implementing dh-ietf1024-sha256-aes128-cbc-pkcs7 is
required for compatibility with libsecret-based applications.
DH parameters: RFC 3526 1024-bit MODP group 2.
The server:
1. Generates a DH ephemeral key pair on the MODP-1024 group.
2. Receives the client's public key in OpenSession.
3. Computes shared = clientPub^serverPriv mod p.
4. Left-pads shared to 128 bytes (a known pitfall — see grimsteel commit c781717).
5. Derives AES key: aes_key = SHA256(shared)[0:16].
6. Each secret returned has its own random 16-byte IV prepended to the ciphertext.
---
Package Structure
All new code lives under internal/secretservice/ (Linux-only files) and integrates
via a new secretservice_linux.go action handler shim.
internal/secretservice/
├── doc.go # Package doc
├── service.go # org.freedesktop.Secret.Service implementation
├── collection.go # org.freedesktop.Secret.Collection implementation
├── item.go # org.freedesktop.Secret.Item implementation
├── session.go # Session lifecycle + crypto dispatch
├── prompt.go # Prompt objects (required by spec for async ops)
├── crypto/
│ ├── crypto.go # Session interface + factory
│ ├── plain.go # "plain" algorithm
│ └── dh.go # "dh-ietf1024-sha256-aes128-cbc-pkcs7"
├── store.go # Adapter between Secret Service and gopass API
├── errors.go # D-Bus error definitions (org.freedesktop.DBus.Error.*)
├── types.go # D-Bus type aliases (Secret struct, path constants)
└── service_test.go # Unit tests (mock D-Bus + mock gopass API)CLI integration:
internal/action/
├── secretservice_linux.go # SecretService() handler, registers via GetCommands()
└── secretservice_other.go # Stub for non-Linux platforms that prints "linux only"Systemd / D-Bus activation files (installed by gopass secret-service install):
contrib/secret-service/
├── org.freedesktop.secrets.service # D-Bus session activation (ExecStart=gopass secret-service serve)
└── gopass-secret-service.service # systemd user unit---
Implementation Phases
This feature is too large for a single prompt. Each phase below is self-contained and can be
implemented and tested independently. Phases must be implemented in order because each phase
depends on the previous.
---
Phase 1 — Foundation: types, errors, session, crypto
Goal: acquire the org.freedesktop.secrets bus name and negotiate a session.
No collections or items yet; stub implementations may be used.
Files to create:
- internal/secretservice/doc.go
- internal/secretservice/types.go
- internal/secretservice/errors.go
- internal/secretservice/crypto/crypto.go
- internal/secretservice/crypto/plain.go
- internal/secretservice/crypto/dh.go
- internal/secretservice/session.go
- internal/secretservice/service.go (skeleton: bus name, OpenSession, CloseSession)
- internal/secretservice/service_test.go
types.go — key D-Bus types:
//go:build linuxpackage secretservice
import "github.com/godbus/dbus/v5"
const (
ServiceName = "org.freedesktop.secrets"
ServicePath = dbus.ObjectPath("/org/freedesktop/secrets")
ServiceIface = "org.freedesktop.Secret.Service"
CollectionIface = "org.freedesktop.Secret.Collection"
ItemIface = "org.freedesktop.Secret.Item"
SessionIface = "org.freedesktop.Secret.Session"
PromptIface = "org.freedesktop.Secret.Prompt"
CollectionPathPrefix = "/org/freedesktop/secrets/collection/"
SessionPathPrefix = "/org/freedesktop/secrets/session/"
PromptPathPrefix = "/org/freedesktop/secrets/prompt/"
)
// Secret is the D-Bus Secret struct as defined in the spec.
// It is the wire format for passing secrets across D-Bus.
type Secret struct {
Session dbus.ObjectPath // session used for transport encryption
Parameters []byte // IV (empty for "plain" algorithm)
Value []byte // encrypted (or plaintext) secret value
ContentType string // MIME type, e.g. "text/plain; charset=utf-8"
}
errors.go — see spec §11 for all error names. Key ones:
//go:build linuxpackage secretservice
import "github.com/godbus/dbus/v5"
var (
ErrNoSession = dbus.NewError("org.freedesktop.Secret.Error.NoSession", nil)
ErrNoSuchObject = dbus.NewError("org.freedesktop.Secret.Error.NoSuchObject", nil)
ErrIsLocked = dbus.NewError("org.freedesktop.Secret.Error.IsLocked", nil)
ErrAlreadyExists = dbus.NewError("org.freedesktop.Secret.Error.AlreadyExists", nil)
ErrNotSupported = dbus.NewError("org.freedesktop.Secret.Error.NotSupported", nil)
)
crypto/crypto.go — session interface:
//go:build linuxpackage crypto
// CryptoSession is an established client session.
type CryptoSession interface {
// Decrypt decrypts a Secret.Value using Parameters as IV.
Decrypt(params, ciphertext []byte) ([]byte, error)
// Encrypt encrypts plaintext and returns (params/IV, ciphertext).
Encrypt(plaintext []byte) (params, ciphertext []byte, err error)
}
// NewSession negotiates a new CryptoSession.
// algorithm is one of "plain" or "dh-ietf1024-sha256-aes128-cbc-pkcs7".
// clientInput is the client public key (empty for "plain").
// Returns the CryptoSession and server output (empty for "plain", server pub key for DH).
func NewSession(algorithm string, clientInput []byte) (CryptoSession, []byte, error) { ... }
crypto/plain.go:
//go:build linuxpackage crypto
type plainSession struct{}
func (plainSession) Decrypt(_, ciphertext []byte) ([]byte, error) { return ciphertext, nil }
func (plainSession) Encrypt(plaintext []byte) ([]byte, []byte, error) { return nil, plaintext, nil }
crypto/dh.go implementation outline:
//go:build linuxpackage crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"math/big"
)
// RFC 3526 MODP 1024-bit group 2
var (
dhPrime, _ = new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA2...", 16) // full 1024-bit prime
dhGen = big.NewInt(2)
)
type dhSession struct{ aesKey []byte }
// NewDHSession computes the shared secret and derives the AES key.
// clientPubBytes is the client's public key.
// Returns the dhSession and the server's public key bytes.
func NewDHSession(clientPubBytes []byte) (*dhSession, []byte, error) {
// 1. Generate server private key (random 128-byte big.Int)
// 2. Compute serverPub = g^serverPriv mod p
// 3. Compute shared = clientPub^serverPriv mod p
// 4. Left-pad shared to 128 bytes (IMPORTANT: see grimsteel/pass-secret-service#24)
// 5. aesKey = SHA256(paddedShared)[0:16]
...
}
func (s *dhSession) Decrypt(params, ciphertext []byte) ([]byte, error) {
// AES-128-CBC with IV=params, key=s.aesKey, PKCS7 unpadding
}
func (s *dhSession) Encrypt(plaintext []byte) ([]byte, []byte, error) {
// random 16-byte IV, AES-128-CBC with PKCS7 padding
}
session.go:
//go:build linuxpackage secretservice
import (
"fmt"
"sync"
"github.com/godbus/dbus/v5"
"github.com/gopasspw/gopass/internal/secretservice/crypto"
)
type session struct {
id string
path dbus.ObjectPath
crypto crypto.CryptoSession
}
type sessionManager struct {
mu sync.RWMutex
sessions map[string]*session
}
func (sm sessionManager) Open(algorithm string, input []byte) (session, []byte, error) { ... }
func (sm sessionManager) Get(path dbus.ObjectPath) (session, error) { ... }
func (sm *sessionManager) Close(path dbus.ObjectPath) error { ... }
service.go skeleton:
//go:build linuxpackage secretservice
import (
"context"
"github.com/godbus/dbus/v5"
)
// Service implements org.freedesktop.Secret.Service.
type Service struct {
conn *dbus.Conn
sessions *sessionManager
// collections added in Phase 2
}
// New creates and starts the service.
// It acquires the org.freedesktop.secrets bus name.
func New(ctx context.Context) (*Service, error) {
conn, err := dbus.SessionBus()
...
reply, err := conn.RequestName(ServiceName, dbus.NameFlagDoNotQueue)
...
svc := &Service{ conn: conn, sessions: &sessionManager{} }
conn.Export(svc, ServicePath, ServiceIface)
conn.Export(introspect.NewIntrospectable(svc), ServicePath, "org.freedesktop.DBus.Introspectable")
return svc, nil
}
// OpenSession implements org.freedesktop.Secret.Service.OpenSession
func (s Service) OpenSession(algorithm string, input dbus.Variant) (dbus.Variant, dbus.ObjectPath, dbus.Error) { ... }
// CloseSession (called on Session object) is delegated to sessionManager.
Testing approach for Phase 1:
- Use dbus.SessionBusPrivate() with conn.Auth(nil) + conn.Hello() to create a private
peer-to-peer connection for unit tests, no real session bus needed.
- Test OpenSession("plain", ...) returns an empty variant and a valid session path.
- Test OpenSession("dh-ietf1024-sha256-aes128-cbc-pkcs7", clientPub) and verify a round-trip
encrypt/decrypt.
---
Phase 2 — Collections: CRUD and property management
Goal: implement org.freedesktop.Secret.Collection, map collections to
gopass subpaths under the secret-service/ prefix. Add CreateCollection,DeleteCollection, and supporting SearchItems / ReadAlias / SetAlias on
the Service.
Files to create/modify:
- internal/secretservice/collection.go (new)
- internal/secretservice/store.go (new — gopass API adapter)
- internal/secretservice/service.go (extend: CreateCollection, SearchItems, ReadAlias, SetAlias, GetSecrets)
store.go — adapter between Secret Service and gopass API.
Do NOT invoke the gopass CLI. Use pkg/gopass/api directly:
//go:build linuxpackage secretservice
import (
"context"
"github.com/gopasspw/gopass/pkg/gopass/api"
)
// Store wraps the gopass API and provides Secret-Service-specific operations.
type Store struct {
gp *api.Gopass
prefix string // default: "secret-service"
}
func NewStore(ctx context.Context, prefix string) (*Store, error) {
gp, err := api.New(ctx)
...
}
// CollectionPath returns the gopass path for a collection.
func (s *Store) CollectionPath(name string) string {
return s.prefix + "/" + name
}
// ItemPath returns the gopass path for an item.
func (s *Store) ItemPath(collection, id string) string {
return s.prefix + "/" + collection + "/i" + id
}
// MetaPath returns the gopass path for a collection's metadata.
func (s *Store) MetaPath(collection string) string {
return s.prefix + "/" + collection + "/_meta"
}
// AliasPath returns the gopass path for the aliases map.
func (s *Store) AliasPath() string {
return s.prefix + "/_aliases"
}
Collection metadata is stored as a gopass secret at MetaPath:
<empty first line>
---
label: Personal
created: 2026-01-15T10:30:00Z
modified: 2026-01-15T10:30:00Z
locked: falseCollection aliases are stored at AliasPath in JSON on the first line:
{"default":"default","login":"default"}collection.go key methods to implement:
// CreateItem, SearchItems, Delete
// Properties: Items (ao), Label (s), Locked (b), Created (t), Modified (t)D-Bus property access via org.freedesktop.DBus.Properties.Get/Set/GetAll.
Use godbus/dbus/v5's prop subpackage for property management.
Signal emission (required by spec):
- Collection.ItemCreated(item: o)
- Collection.ItemDeleted(item: o)
- Collection.ItemChanged(item: o)
- Service.CollectionCreated(collection: o)
- Service.CollectionDeleted(collection: o)
- Service.CollectionChanged(collection: o)
Testing approach for Phase 2:
- Mock Store using an in-memory map (no real gopass/GPG needed for tests).
- Test CreateCollection → verify gopass path created and D-Bus object exported.
- Test ReadAlias("default") before and after creating collections.
---
Phase 3 — Items: GetSecret, SetSecret, Delete, Search
Goal: implement org.freedesktop.Secret.Item. Full item CRUD with attribute-based search.
Files to create/modify:
- internal/secretservice/item.go (new)
- internal/secretservice/store.go (extend: item read/write/delete/search)
- internal/secretservice/collection.go (extend: CreateItem, SearchItems)
Item storage format (in gopass secret first-line + YAML):
secret-value-here
---
_ss_label: GitHub Token
_ss_created: 2026-05-01T12:00:00Z
_ss_modified: 2026-05-01T12:00:00Z
_ss_content_type: text/plain; charset=utf-8
username: octocat
server: github.comRules:
- Keys prefixed with _ss_ are reserved for internal use.
- All other key/value pairs are item attributes (arbitrary strings, per spec).
- The secret value is the first line of the gopass secret (the standard gopass password field).
item.go key methods:
// GetSecret(session: o) → secret: Secret
// SetSecret(secret: Secret) → nothing
// Delete() → prompt: o (return "/" as prompt path for immediate completion)
// Properties: Locked (b), Attributes (a{ss}), Label (s), Created (t), Modified (t)Store item search:
// SearchItems(attrs map[string]string) ([]dbus.ObjectPath, error)
// Lists all items in a collection, loads each item's attributes, filters by attrs.
// This is O(n) — acceptable given typical collection sizes.Locking: In this implementation lock state is in-memory only.
When a collection is "locked", GetSecret returns ErrIsLocked.
The underlying GPG file is always accessible if the GPG agent has a cached key.
Testing approach for Phase 3:
- Create item, verify it appears in Items property of collection.
- SearchItems with matching/non-matching attributes.
- Round-trip: SetSecret(plain) → GetSecret(plain) → verify value.
- Round-trip: SetSecret(dh) → GetSecret(dh) → verify value.
- Delete item → verify gone from Items and gopass store.
---
Phase 4 — Prompt, Lock/Unlock, GetSecrets (batch)
Goal: complete the spec. Implement Prompt objects, Service.Lock,Service.Unlock, Service.GetSecrets.
Files to create/modify:
- internal/secretservice/prompt.go (new)
- internal/secretservice/service.go (extend: Lock, Unlock, GetSecrets)
Prompts: The spec uses Prompt objects for operations that may require user
interaction. For Lock/Unlock in this implementation, the prompt completes
immediately (no actual user interaction needed because GPG-agent handles
passphrase caching). The pattern:
// Unlock(objects []dbus.ObjectPath) → (unlocked []dbus.ObjectPath, prompt dbus.ObjectPath)
// Returns prompt "/" (null prompt) when all objects are already unlocked.
// Returns a real prompt path when any object needs unlocking; the prompt
// signals Completed(dismissed bool, result dbus.Variant) when done.type Prompt struct {
path dbus.ObjectPath
conn *dbus.Conn
action func() ([]dbus.ObjectPath, error)
}
// Prompt.Dismiss() aborts the operation.
// For an immediate-complete prompt: export the object, fire Completed signal in a goroutine.
Service.GetSecrets:
// GetSecrets(items []dbus.ObjectPath, session dbus.ObjectPath) → map[dbus.ObjectPath]Secret
// Batch retrieval. For each path, resolve the item and call its GetSecret logic.Testing approach for Phase 4:
- Lock collection, verify GetSecret returns ErrIsLocked.
- Unlock collection → prompt completes → verify GetSecret succeeds.
- GetSecrets with mixed locked/unlocked items.
- Prompt dismiss returns correct dismissed=true.
---
Phase 5 — CLI integration: gopass secret-service
Goal: wire everything into the gopass CLI as a new subcommand.
Files to create/modify:
- internal/action/secretservice_linux.go (new)
- internal/action/secretservice_other.go (new — non-Linux stub)
- internal/action/commands.go (add entry point, with build constraints)
- contrib/secret-service/org.freedesktop.secrets.service (new — D-Bus activation)
- contrib/secret-service/gopass-secret-service.service (new — systemd user unit)
- docs/commands/secret-service.md (new — user documentation; expand separately)
CLI design:
gopass secret-service serve [--replace] [--prefix=secret-service] [--notify-on-access]
gopass secret-service install # installs systemd unit + D-Bus activation file
gopass secret-service uninstall # removes systemd unit + D-Bus activation file
gopass secret-service status # checks whether the service is runningsecretservice_linux.go action handler:
//go:build linuxpackage action
import (
"context"
"github.com/gopasspw/gopass/internal/secretservice"
"github.com/urfave/cli/v3"
)
func (s Action) SecretService(ctx context.Context, cmd cli.Command) error {
prefix := cmd.String("prefix")
replace := cmd.Bool("replace")
svc, err := secretservice.New(ctx, secretservice.Config{
Prefix: prefix,
Replace: replace,
})
if err != nil {
return err
}
return svc.Serve(ctx) // blocks until ctx is cancelled or fatal error
}
secretservice_other.go stub (for Windows/macOS):
//go:build !linuxpackage action
import (
"context"
"fmt"
"github.com/urfave/cli/v3"
)
func (s Action) SecretService(ctx context.Context, cmd cli.Command) error {
return fmt.Errorf("secret-service is only supported on Linux")
}
commands.go entry — add to GetCommands():
/ Detailed source-code truncated for AI context efficiency. /D-Bus activation file (org.freedesktop.secrets.service):
[D-BUS Service]
Name=org.freedesktop.secrets
Exec=/usr/bin/gopass secret-service servesystemd user unit (gopass-secret-service.service):
[Unit]
Description=gopass Secret Service D-Bus daemon
After=graphical-session.target
PartOf=graphical-session.target[Service]
Type=dbus
BusName=org.freedesktop.secrets
ExecStart=/usr/bin/gopass secret-service serve
Restart=on-failure
[Install]
WantedBy=graphical-session.target
GNOME Keyring conflict resolution: gopass secret-service serve --replace passesdbus.NameFlagReplaceExisting to conn.RequestName(...). Users must also disable GNOME
Keyring's secret service component:
cp /etc/xdg/autostart/gnome-keyring-secrets.desktop ~/.config/autostart/
echo "Hidden=true" >> ~/.config/autostart/gnome-keyring-secrets.desktopThe
install subcommand should offer to do this automatically.---
Phase 6 — Tests and Documentation (expand in a separate prompt)
Goal: integration tests, user documentation, and make test-integration compatibility.
Files to create/modify:
- internal/secretservice/*_test.go — expand unit tests
- tests/secret_service_test.go — new integration test using gptest
- docs/commands/secret-service.md — user-facing documentation
Integration test approach:
// tests/secret_service_test.go
// Uses gptest.NewGUnitTester to set up a real gopass store.
// Starts the service on a private D-Bus connection (dbus.SessionBusPrivate).
// Uses secret-tool (if available) or direct godbus calls to store/retrieve secrets.
// Verifies:
// - secret created via D-Bus appears in gopass (gopass show secret-service/default/i<uuid>)
// - secret created via gopass insert is visible via D-Bus GetSecret
// - attributes are searched correctly by SearchItemsKnown caveats to document:
1. GPG circular dependency: If pinentry-gnome3 tries to check libsecret for cached
passphrases at startup, a deadlock occurs. Solution: add no-allow-external-cache to
~/.gnupg/gpg-agent.conf. Document this prominently (see nikicat/gopass-secret-service#troubleshooting).
2. Lock state: Lock/Unlock are in-memory only. Locking a collection does not evict GPG
agent's cached passphrase.
3. Linux only: Ensure the subcommand entry in commands.go still compiles on all platforms
(use the secretservice_other.go stub pattern).
4. Session bus required: DBUS_SESSION_BUS_ADDRESS must be set. The daemon should fail
gracefully with a clear error if it is not.
---
Key Implementation Notes for LLM Agents
The following are common pitfalls to avoid:
1. DH shared-secret left-padding: shared = clientPub^serverPriv mod p may produce a
big.Int whose byte representation is shorter than 128 bytes. It must be left-padded with
zero bytes to exactly 128 bytes before SHA256. Failing to do this breaks compatibility with
libsecret clients. See commit c781717 in grimsteel/pass-secret-service.
2. godbus export pattern: To export a Go struct as a D-Bus object, use
conn.Export(obj, path, iface). The exported methods must have the exact signature
func(args...) (returns..., *dbus.Error). The D-Bus method names are the Go method names
exactly. Use introspect.NewIntrospectable to expose introspection.
3. D-Bus properties: Use the prop subpackage from godbus/dbus/v5/prop for the
org.freedesktop.DBus.Properties interface. Mandatory for libsecret compatibility.
4. Null prompts: When an operation completes immediately (no user interaction needed),
return dbus.ObjectPath("/") as the prompt path per spec §6.
5. Object lifecycle: When an item/collection is deleted, call conn.Export(nil, path, iface)
to unexport the D-Bus object.
6. Replace flag: conn.RequestName(ServiceName, dbus.NameFlagReplaceExisting) asks the
bus to evict the current holder. Without this flag, the second gopass secret-service serve
invocation quietly fails to acquire the name.
7. _meta and _aliases naming: These use underscore prefix to avoid collisions with
user-created secrets. In the SearchItems listing loop, always skip paths ending in
/_meta and /_aliases.
8. Build tags: Every file in internal/secretservice/ must start with //go:build linux.
The action shims in internal/action/ use the _linux.go / _other.go filename convention
(Go's implicit build tag from filename suffix) which is equivalent, and consistent with
notify_dbus.go and unclip_linux.go in the existing codebase.
9. gopass API vs CLI: Use pkg/gopass/api.New(ctx) (the public Go API), NOT exec.Command("gopass", ...).
The API is already used by gopass integrations and is stable enough for this purpose.
10. Error wrapping: D-Bus methods must return *dbus.Error, not error. Map internal errors
to the appropriate org.freedesktop.Secret.Error.* D-Bus errors defined in errors.go.
---
References
- Secret Service Spec (latest)
- godbus/dbus/v5 docs
- nikicat/gopass-secret-service — Go reference impl (MIT)
- grimsteel/pass-secret-service — Rust reference impl (GPL-3.0, for reference only, not to be copied)
- RFC 3526 — MODP DH groups — 1024-bit group 2
- pkg/gopass/api/api.go — gopass public API
- internal/notify/notify_dbus.go — existing godbus usage pattern
- pkg/clipboard/unclip_linux.go — existing Linux-only D-Bus pattern
---
Adr/A 12 Pkg Api Stability
A-12: pkg/gopass API Stability Contract
Status: accepted
Source: GitHub Issue #3414
---
Background
ARCHITECTURE.md (lines 37–43) documents that gopass applies semantic versioning to the CLI
tool only, not the Go module. pkg/gopass is the documented integration point for external
consumers (gopass-hibp, gopass-jsonapi, git-credential-gopass, gopass-summon-provider, and
others). There is currently no documented contract for what constitutes a breaking change or
how consumers will be notified.
A breaking change to pkg/gopass.Store, pkg/gopass.Secret, or related types can affect
consumers silently — the module version does not increment, no changelog entry was mandated,
and go get would pull the break without warning.
---
Decision
Implement Options B and C. They are complementary and together give consumers both
runtime/source-level signals (C) and a reliable notification channel (B).
Option A (full module semver / v2 path) is deferred — the current team size makes the
coordination overhead infeasible.
Option D (separate repository) is deferred for the same reasons.
---
Option B — "Best-effort stable" policy with mandatory changelog tags
Policy
pkg/gopass and its sub-packages are declared best-effort stable:
* Additive changes (new exported symbols, new optional parameters via functional options) may
appear in any release without prior notice.
* Breaking changes (removal or signature change of an exported symbol, change of error
semantics, change of an interface method set) require:
1. A PKG-BREAK: footer on the commit, which produces a [PKG-BREAK]-prefixed entry in the
## [Unreleased] section of CHANGELOG.md describing what changed and how consumers
should migrate.
2. A minimum deprecation window of two minor releases or three months, whichever is
longer, between the first deprecation notice and removal. During this window the old
symbol must remain available (possibly with a // Deprecated: GoDoc comment pointing to
the replacement).
Changelog tag convention
A breaking change to pkg/ is declared with a PKG-BREAK: commit footer:
refactor(pkg/gopass): drop Store.GetRevision in favour of Store.HistoryDeprecated since 1.17.0; the two-minor / three-month window has elapsed.
PKG-BREAK: pkg/gopass: Remove Store.GetRevision — use Store.History instead
Signed-off-by: Your Name <[email protected]>
helpers/commitmsg reads that footer and helpers/release renders the entry with a[PKG-BREAK] prefix inside the appropriate Keep a Changelog subsection, normally Changed.
A Changelog-Section: Removed footer moves it to Removed when the symbol is gone rather
than changed.
The footer is the machine-readable form of the tag. Writing [PKG-BREAK] by hand into## [Unreleased] also works and is preserved by the release helper.
Note: CHANGELOG.md now follows Keep a Changelog 1.1.0, so [SECURITY], [BUGFIX] and[FEATURE] are no longer tags — they are the Security, Fixed and Added subsections.[PKG-BREAK] remains a bullet prefix because it qualifies an entry rather than categorising
it. See docs/conventions.md.
Enforcement
* Code reviewers must reject PRs that remove or change exported pkg/ symbols without a
corresponding [PKG-BREAK] changelog entry and a prior deprecation notice.
* The golangci-lint godot and godox rules already catch missing doc-comment periods and
stray TODO/FIXME markers; no additional tooling is introduced.
---
Option C — Explicit stability annotations in package doc comments
Each package under pkg/gopass/ carries a doc comment that states its stability level.
| Package | Level | Rationale |
|---------|-------|-----------|
| pkg/gopass | best-effort stable | Core interfaces; multiple known consumers |
| pkg/gopass/api | best-effort stable | Primary API implementation |
| pkg/gopass/secrets | best-effort stable | Secret types consumed by integrations |
| pkg/gopass/apimock | testing helper — no stability guarantee | Internal test double; consumers should copy or vendor it |
The standard // Deprecated: GoDoc convention is used to signal pending removal of individual
symbols; godoc and pkg.go.dev render these prominently.
---
Affected packages
pkg/gopass/, pkg/gopass/api/, pkg/gopass/apimock/, pkg/gopass/secrets/
---
Consequences
* External integrators get a documented, reasonable stability promise without requiring a
module-path change today.
* The changelog [PKG-BREAK] tag gives integrators a single place to scan for migration work
when upgrading.
* The deprecation window gives integrators time to adapt before a symbol is removed.
* The policy can be upgraded to full module semver in the future if maintainer capacity grows,
without breaking any existing convention.
---
Adr/A 13 Expired Gpg Key Handling
A-13: Expired GPG Key Handling and Recipient Validity Warnings
Status: partially implemented — core silent-drop warning shipped; remaining work tracked below
Source: GitHub Issue #2885
---
Background
When a recipient's GPG key expires, gopass silently drops that recipient from
the encryption target list. Secrets written after the key expires can no
longer be decrypted by that recipient. Neither the writing user nor the
affected recipient receives any notification that this happened.
The encryption path is:
Set() → useableKeys() → FindRecipients() → [expired key silently filtered]
→ Encrypt(filtered_list) [warning in Encrypt() never fires]FindRecipients() calls KeyList.UseableKeys(), which returns only keys
whose ExpirationDate is either zero or in the future. The difference between
the original recipient list and the returned key list was never surfaced to the
user.
An existing CheckRecipients() function in internal/store/leaf/recipients.go
already performs the correct per-recipient check and is called beforeRecipientsAdd, but was not called on the write path.
---
Implemented fix (this branch)
useableKeys() in internal/store/leaf/store.go now iterates over the
original recipient list and, for each recipient that FindRecipients returns
no useable key for, emits an out.Warningf message naming that recipient.
The return value (the filtered key list used for encryption) is unchanged.
Encrypt() in internal/backend/crypto/gpg/cli/encrypt.go was also updated
to use out.Warningf instead of out.Printf for its own per-recipient check,
so the severity is correct if that path is ever reached.
A regression test (TestSetWarnsAboutInvalidRecipient) was added tointernal/store/leaf/write_test.go.
---
Remaining work
R-1: Add recipient key-expiry check to gopass audit
Problem: gopass audit checks only password strength (crunchy, HIBP). It
does not check whether any recipient's key is expired or about to expire.
A team could run gopass audit regularly and still have no indication that
a re-encryption silently excluded a recipient.
Recommendation: Add a recipient-validity check to internal/audit/. TheAuditor type already has a secretGetter interface; a separateRecipientAuditor (or an additional pass in the existing Batch method) could:
1. Collect the union of all recipient IDs across all secrets (or from the
.gpg-id files) without decrypting anything.
2. Call crypto.FindRecipients(ctx, id) for each one.
3. Report any ID with no useable key as an error and any ID whose
ExpirationDate is within a configurable window (default 60 days) as a
warning.
This requires plumbing a Crypto backend reference into the audit path, which
the current secretGetter interface does not expose. A separate interface or
an Auditor constructor parameter is the cleanest extension point.
Configuration key (proposed): audit.recipient-expiry-warning-days
(default 60; 0 disables the check).
---
R-2: Proactive expiry warning on sync and fsck
Problem: Users learn that a key has expired only when they attempt to
write a secret. There is no early warning before expiry, and no warning to
the affected recipient when they pull a store that now contains secrets they
cannot decrypt.
Recommendation:
- In gopass sync (internal/action/sync.go) and gopass fsck
(internal/store/leaf/fsck.go), call a lightweight
CheckRecipientExpiry(ctx, warningDays int) helper (to be added to
internal/store/leaf/recipients.go) that:
1. Loads all recipient IDs from the store's .gpg-id files.
2. Looks up each key via FindRecipients.
3. For keys that are already expired: out.Warningf.
4. For keys expiring within warningDays: out.Warningf including the
expiry date and the recovery instructions (see R-3).
- The check must be cheap: it reads only the local keyring and .gpg-id
files; no network calls or decryption.
Configuration key (proposed): core.recipient-expiry-warning-days
(default 60; 0 disables). Distinct from the audit key so that the two
features can be tuned independently.
---
R-3: Document the key-refresh recovery flow
Problem: The recovery path when a key has expired is not obvious. The
issue reporter required a multi-step manual process. A simpler path already
exists but is undocumented:
1. Recipient L extends the key expiry locally: gpg --edit-key <L> → expire.
2. L exports the updated key: gpg -a --export <L> > L.pub.asc.
3. L replaces .public-keys/<L> in the store with the new export, commits,
and pushes.
4. Any other recipient A runs gopass recipients add <L> (the existing
AddRecipient handler already asks for confirmation to re-encrypt when the
key is already in the store).
Step 4 is the re-encryption trigger. Gopass already supports this workflow;
it just needs to be documented.
Recommendation:
- Add a section "Recipient key expiry" to docs/commands/recipients.md
describing the flow above.
- Update the warning message emitted by R-1/R-2 to include a short hint, e.g.:
"Run 'gopass recipients add <id>' after the key is refreshed to re-encrypt.".
---
R-4: Detect that the committed public key differs from the local keyring
Problem: A recipient may extend their key locally and not update the copy
in .public-keys/. Conversely, another recipient may import an updated key
from .public-keys/ while the local keyring still has the old (expired)
version. In both cases gopass has no way to tell the user that the two copies
are out of sync.
Recommendation: In gopass fsck or gopass recipients, compare theExpirationDate of the key stored in .public-keys/<id> against the key
in the local GPG keyring. If they differ by more than a negligible delta
(suggest: one day), emit a warning.
This requires parsing the armored public key from .public-keys/ without
importing it, which can be done with golang.org/x/crypto/openpgp orgithub.com/ProtonMail/go-crypto/openpgp (already an indirect dependency via
the age backend). Care must be taken not to introduce a new direct dependency
on a CGo package; go-crypto is pure Go.
---
Rejected alternatives
Return an error from Set() when a recipient is dropped: This would be a
breaking behaviour change. Existing stores that happen to have stale recipient
entries (e.g. a team member who has left) would become unwritable. A warning
is the correct signal; the operator must decide whether to remove the recipient
or refresh the key.
Block writes entirely when any recipient has no useable key: Same objection
as above. A --strict flag could be added later if there is demand.
Check every recipient on every read (Get): Unnecessary overhead; expiry
affects the write path only.
---
Adr/A 14 Team Workflows
A-14: Effortless Team Workflows
Status: implemented (Stages 0–5 complete, June 2026)
Sources:
#2762,
#2620,
#1430
Related: ADR A-13,
use cases: team-workflows
Implementation: branch fix/issue-1430 (covers all three issues)
---
1. Context and problem statement
gopass is widely used by teams, but the team lifecycle (bootstrap a store, join
a team, add/remove members, rotate keys) is fragile. Three long-standing issues
share a common root cause: recipient identity is not canonical, and several
code paths handle the gap inconsistently.
The supported workflows are defined in
docs/usecases/team-workflows.md. This ADR
analyses the current implementation against those use cases, identifies the
defects, and proposes a staged plan.
1.1 How the store represents a team today
* .gpg-id — newline-separated recipient IDs (the team).
.public-keys/<id> — armored public key per recipient, *filename is the
recipient ID verbatim. Legacy fallback dir: .gpg-keys/<id>.
* Global config recipients.hash.<alias> — SHA256 of .gpg-id for tamper
detection (only when recipients.check is enabled).
1.2 Relevant code map (verified)
| Concern | Location |
| --- | --- |
| Recipient set type | internal/recipients/recipients.go (Add only TrimSpaces — no normalization) |
| Init store | internal/store/leaf/init.go Init() — normalizes via FindRecipients → rs.Add(kl[0]) |
| Add recipient | internal/store/leaf/recipients.go AddRecipient() — adds raw id (no normalization) |
| Remove recipient | internal/store/leaf/recipients.go RemoveRecipient() — 3-level fuzzy match |
| Save recipients | internal/store/leaf/recipients.go saveRecipients() — writes .gpg-id, exports keys if core.exportkeys, autopush |
| Export keys | exportPublicKey() / addMissingKeys() / UpdateExportedPublicKeys() — filename = .public-keys/<r> |
| Remove extra keys | removeExtraKeys() — disabled by default (recipients.remove-extra-keys, GH-2620) |
| Import keys | internal/store/leaf/crypto.go ImportMissingPublicKeys(), recipientCheck(), decodePublicKey(), getPublicKey() |
| Sync | internal/action/sync.go syncMount() → syncImportKeys() + syncExportKeys() |
| Clone | internal/action/clone.go Clone() / cloneCheckDecryptionKeys() — exports only the cloner's key |
| GPG backend | internal/backend/crypto/gpg/cli/ FindRecipients, ExportPublicKey, ImportPublicKey, GetFingerprint, ReadNamesFromKey |
---
2. Root-cause analysis
2.1 #2762 — ID vs. filename mismatch (the central bug)
Init() normalizes recipient IDs to the canonical key returned byFindRecipients before storing them:
// internal/store/leaf/init.go (Init)
kl, _ := s.crypto.FindRecipients(ctx, id)
rs.Add(kl[0]) // canonicalAddRecipient() does not:
// internal/store/leaf/recipients.go (AddRecipient)
rs.Add(id) // raw user input: email, short ID, or fingerprint
s.saveRecipients(...)saveRecipients → UpdateExportedPublicKeys → exportPublicKey then writes:
filename := filepath.Join(keyDir, r) // r == raw id, e.g. "[email protected]"
pk, _ := exp.ExportPublicKey(ctx, r) // gpg resolves the email fine
s.storage.Set(ctx, filename, pk) // -> .public-keys/[email protected]So after gopass recipients add [email protected]:
* .gpg-id line: [email protected]
* .public-keys/ file: [email protected]
On another machine, ImportMissingPublicKeys iterates .gpg-id IDs and callsrecipientCheck("[email protected]") → FindRecipients("[email protected]").
If the email is ambiguous, missing, or the local gpg matched a different key
than the store owner intended, the lookup fails or resolves to the wrong key,
producing:
Failed to decode public key [email protected]: public key "..." not foundThe blank-but-numbered line in gopass recipients output is the same symptom:
an entry in .gpg-id that the local keyring cannot resolve to a name.
Fix direction: make recipient identity canonical at every write path, not
just Init. The .gpg-id entry and the .public-keys/<id> filename must both
be the canonical fingerprint.
2.2 #2620 — clone wipes other recipients' public keys
Two divergent join paths exist:
* gopass setup --remote ... --alias x → works.
* gopass clone <url> x → broke: after clone, a save/re-encrypt ran with only
the keys the new member could resolve locally (their own), and the exported
public keys ended up reduced to just the new member's key. The reporter in
jonmz's comment showed the bug only triggered for members whose root store
was at the new default location, confirming a path/flow divergence rather
than a pure crypto bug.
removeExtraKeys() (which deletes .public-keys/ files not in the current
recipient list) was disabled by default as a band-aid:
// TODO(GH-2620): Temporarily disabled by default until we fix the key cleanup.
if cfg.GetGlobal("recipients.remove-extra-keys") == "true" { ... }But disabling it only hides one of the deletion paths. The deeper issue is that
a member who cannot resolve a recipient locally can still trigger a write that
regenerates the exported key set from an incomplete view, and then push it.
Fix direction:
1. Unify clone and setup onto one join code path (UC-3).
2. Never regenerate the full .public-keys/ set from a partial local view;
only add the member's own key and import others from the store.
3. Re-enable controlled, recipient-driven cleanup tied to explicit
recipients remove only (UC-5), never as a side effect of sync/clone.
2.3 #1430 — no key-refresh path; expired keys not updated
* core.autoimport and the import prompt only import keys that are missing
from the keyring; they never update an existing-but-expired key.
* core.exportkeys only exports keys that are missing from .public-keys/
(exportPublicKey returns early if the file exists and IsPubkeyUpdate(ctx)
is false).
* There is no command to push a refreshed local key into the store.
So when L extends an expired key, neither the store copy nor other members'
keyrings ever update automatically.
Fix direction: add a gopass recipients update command (UC-6) plus
"update if newer/expired" semantics for both export and import, gated to avoid
surprise overwrites. This dovetails with ADR A-13's expiry warnings.
---
3. Decision: a canonical-recipient model + unified join + key refresh
We will make canonical recipient identity the backbone, then fix the three
workflows on top of it. The work is staged so each stage is shippable and
testable on its own.
3.1 Guiding invariants (the contract)
For every store and every recipient R:
1. The .gpg-id entry for R equals the canonical ID (full fingerprint for
GPG; the recipient string for age).
2. .public-keys/<canonical-id> exists and contains R's key (when
core.exportkeys is on).
3. No member operation removes R from .gpg-id or .public-keys/ unless the
operator explicitly runs recipients remove R.
4. Sync only ever adds exported keys or updates an outdated one; it never
deletes.
---
4. Implementation plan (staged)
Stage 0 — Safety net (tests + diagnostics), no behavior change
Goal: lock current behavior and make the bugs observable before changing code.
* Add integration tests under tests/ that reproduce each issue:
* tests/team_join_test.go — clone-as-new-member must preserve all
.public-keys/ (red test for #2620).
* tests/recipients_email_test.go — recipients add <email> then import on
a second keyring (red test for #2762).
* tests/recipients_refresh_test.go — refresh an expired key (red test for
#1430).
* Add a gopass fsck/gopass doctor diagnostic that reports recipient/key
inconsistencies (see Stage 4). Read-only; safe to ship first.
Pseudocode for the diagnostic (no decryption, local only):
func (s *Store) DiagnoseRecipients(ctx) []Finding {
rs := s.GetRecipients(ctx, "")
for _, id := range rs.IDs() {
canonical := s.crypto.FindRecipients(ctx, id) // 0, 1, or many
switch {
case len(canonical) == 0 && !s.publicKeyFileExists(id):
finding(Error, "%s: no key in keyring and not in .public-keys", id)
case len(canonical) == 0:
finding(Warn, "%s: only available via .public-keys (run import)", id)
case canonical[0] != id:
finding(Warn, "%s stored non-canonically; canonical is %s", id, canonical[0])
}
// expiry check (A-13 R-4): compare .public-keys key vs keyring key
}
}Stage 1 — Canonicalize recipient identity (fixes #2762)
1a. Normalize on add. In AddRecipient, resolve the input to the canonical
ID before storing, mirroring Init:
func (s *Store) AddRecipient(ctx, id string) error {
canon, err := s.canonicalizeRecipient(ctx, id) // FindRecipients -> fingerprint
if err != nil { return err } // ask user if ambiguous (>1)
rs := s.GetRecipients(ctx, "")
if rs.Has(canon) { / re-encrypt/update path / }
rs.Add(canon)
...
}canonicalizeRecipient:
func (s *Store) canonicalizeRecipient(ctx, id string) (string, error) {
kl, err := s.crypto.FindRecipients(ctx, id)
switch {
case len(kl) == 1: return kl[0], nil
case len(kl) == 0:
// fall back to .public-keys: read key, get fingerprint, that's canonical
if pk, e := s.getPublicKey(ctx, id); e == nil {
return s.crypto.GetFingerprint(ctx, pk)
}
return "", fmt.Errorf("no key found for %q", id)
default:
// ambiguous: prompt the user to pick one
return askWhichKey(ctx, kl)
}
}1b. Canonical export filename. exportPublicKey must derive the filename
from the canonical ID, not the raw recipient string. Since 1a guarantees.gpg-id holds canonical IDs, the existing filepath.Join(keyDir, r) becomes
correct automatically. Add a guard/assertion + debug log if r is
non-canonical.
1c. Migration for existing stores. Providegopass recipients canonicalize (or fold into gopass fsck --recipients)
that, for a store the operator can decrypt:
for each id in .gpg-id:
canon = canonicalize(id)
if canon != id:
rename .public-keys/<id> -> .public-keys/<canon> (if present)
rewrite .gpg-id line id -> canon
commit "Canonicalize recipient IDs"Must be explicit and confirmed; it rewrites .gpg-id and therefore changesrecipients.hash.
Backward compatibility: lookups (getPublicKey, decodePublicKey,RemoveRecipient) keep their fuzzy fallback so old non-canonical stores still
work read-side until migrated. Only the write paths become strict.
Stage 2 — Unify and harden the join/clone flow (fixes #2620)
2a. Single join code path. Extract the post-clone/post-setup logic into one
function used by both clone.go and setup.go:
func joinTeam(ctx, sub *leaf.Store) error {
// 1. import every recipient key that the store already ships
sub.ImportMissingPublicKeys(ctx) // never deletes
// 2. can we decrypt?
if hasDecryptionKey(ctx, sub) {
out.OK("You can decrypt this store.")
return nil
}
// 3. no access yet: export ONLY our own key, additively
self := crypto.ListIdentities(ctx)[0]
sub.ExportSelfPublicKey(ctx, self) // adds .public-keys/<self>, no removal
out.Notice("Request access: ask an owner to run 'gopass recipients add %s'", self)
// 4. push the added key
}2b. Make UpdateExportedPublicKeys strictly additive on the sync/clone
paths. It must never call removeExtraKeys outside an explicitrecipients remove. The blanket recipients.remove-extra-keys global flag is
removed; cleanup moves to Stage 3 where it is recipient-scoped.
2c. Guard against partial-view writes. Before any path re-encrypts or
regenerates the exported key set, require that the operator can resolve all
current recipients (keyring or .public-keys/). If some recipients are
unresolved, gopass imports them from .public-keys/ first; if that fails, it
refuses to rewrite the key set and prints actionable guidance instead of
silently pushing a reduced set.
2d. Path independence. Add a test matrix covering root store at~/.password-store and at the new default location to ensure the join flow is
identical (covers jonmz's reproduction).
Stage 3 — Clean removal (completes #2620, supports UC-5)
RemoveRecipient becomes the only writer that deletes from .public-keys/:
func (s *Store) RemoveRecipient(ctx, id string) error {
canon := s.matchRecipient(ctx, id) // existing fuzzy match, returns canonical
rs.Remove(canon)
if rs.Len() == 0 { return errLastRecipient }
s.saveRecipients(ctx, rs, "Remove Recipient "+canon)
// recipient-scoped cleanup (NOT blanket):
s.storage.Delete(ctx, filepath.Join(keyDir, canon))
s.storage.Delete(ctx, filepath.Join(oldKeyDir, canon)) // legacy
return s.reencrypt(...)
}This restores key cleanup safely: only the explicitly removed recipient's file
is deleted, never an unrelated one. The disabled removeExtraKeys path and its
global flag are deleted.
Stage 4 — Key refresh and expiry handling (fixes #1430, ties to A-13)
4a. New command gopass recipients update [<id> ...]. Re-exports the
named recipients' (default: own) current public keys from the local keyring
into .public-keys/, overwriting stale copies, and commits:
func (s *Store) UpdateRecipientKeys(ctx, ids ...string) error {
ctx = WithPubkeyUpdate(ctx, true) // force overwrite in exportPublicKey
for _, id := range ids {
canon := s.canonicalizeRecipient(ctx, id)
s.exportPublicKey(ctx, exp, canon) // overwrites because IsPubkeyUpdate(ctx)
}
commit("Refreshed public keys")
}4b. "Update if newer/expired" on import. Extend recipientCheck so a key
already in the keyring is still re-imported when the .public-keys/ copy is
newer or the keyring copy is expired:
func (s *Store) recipientCheck(ctx, r string) bool {
kl := s.crypto.FindRecipients(ctx, r)
if len(kl) == 0 { return false } // missing -> import
storeKey := s.getPublicKey(ctx, r) // .public-keys copy
if keyringExpired(kl[0]) && !parsedKeyExpired(storeKey) {
return false // outdated -> import update
}
if newerThanKeyring(storeKey) { return false } // refreshed -> import update
return true
}Importing an update still respects core.autoimport / the interactive prompt;
the prompt text is adjusted to say "update" when a key already exists.
4c. Expiry warnings. Implement ADR A-13 R-1..R-4 (audit check, sync/fsck
warnings, recovery docs, store-vs-keyring drift detection) and have the warning
message point at gopass recipients update.
Stage 5 — Documentation and UX
* docs/commands/recipients.md: document add, remove, update,
canonicalize/migration, and the expiry recovery flow.
* docs/usecases/team-workflows.md (added in this change) is the reference.
* Improve the messages emitted on join ("request access"), on add ("confirm
this key", "imported from store"), and on sync (clearer key import/export
reporting).
---
5. Configuration summary
| Key | Default | Effect | Change |
| --- | --- | --- | --- |
| core.exportkeys | true (root) / false (substore) | export recipient keys to .public-keys/ | keep; document the substore default which surprises users (#2620) — consider defaulting true for substores too |
| core.autoimport | false | import (now also update) keys without prompt | semantics extended (Stage 4b) |
| recipients.check | false | validate .gpg-id against recipients.hash | keep |
| recipients.remove-extra-keys | false (global only) | blanket cleanup of .public-keys/ | remove (replaced by recipient-scoped removal, Stage 3) |
Open question: flip core.exportkeys to default true for substores. It makes
team substores work out of the box (most #2620 reporters used substores) at the
cost of slightly larger repos. Recommended: yes, with a release note.
---
6. Backwards compatibility & migration
* Read paths keep fuzzy matching, so existing non-canonical stores keep working.
* Write paths become strict (canonical). The first owner-side recipients add/
remove on an old store will write canonical IDs for the touched recipient;
full migration is via the explicit recipients canonicalize/fsck command.
* recipients.hash changes when .gpg-id is rewritten by migration;
gopass recipients ack already handles acknowledging a new hash.
* Removing the recipients.remove-extra-keys flag is safe because it defaulted
to off.
---
7. Testing strategy
* Unit tests in internal/store/leaf for canonicalization, additive export,
scoped removal, and the extended recipientCheck.
* Integration tests in tests/ (GPG-backed via gptest) for the full
lifecycle UC-1..UC-7, including the three regression scenarios and the
root-store-path matrix.
* make test, make codequality, and make test-integration must pass.
---
8. Rejected / deferred alternatives
* Store emails in .gpg-id and rely on per-machine resolution. This is the
status quo and the source of #2762; rejected.
* Re-enable blanket removeExtraKeys with smarter heuristics. Any blanket
cleanup driven by a partial local view risks #2620 again; rejected in favor
of recipient-scoped deletion on explicit removal.
* Auto-rotate secrets on member removal. Out of scope; revocation is
documented as non-retroactive. A future gopass audit --rotate-after-removal
could help and is deferred.
* A central team-membership manifest / server. Out of scope; gopass stays
decentralized and relies on git hosting for transport-level access control.
---
9. Sequencing / dependencies
flowchart TD
S0[Stage 0: tests + diagnostics] --> S1[Stage 1: canonical IDs #2762]
S1 --> S2[Stage 2: unified join #2620]
S2 --> S3[Stage 3: scoped removal]
S1 --> S4[Stage 4: key refresh #1430 + A-13]
S3 --> S5[Stage 5: docs/UX]
S4 --> S5Stage 1 is the keystone; Stages 2–4 depend on canonical identity to be robust.
---
Adr/A 15 Screenshot Build Tag
A-15: noscreenshot Build Tag for OTP Screen-Capture Feature
Status: accepted
Source: GitHub Issue #3415
---
Background
pkg/otp/screenshot_supported.go imports github.com/kbinani/screenshot to capture display
contents when the user runs gopass otp --snip. This allows gopass to locate an OTP QR code
that is visible on screen and store the decoded otpauth:// URL directly into a secret.
screenshot is a notable dependency for a password manager because it grants the binary the
ability to read the full screen. Users auditing binary capabilities or operating in
policy-restricted environments (e.g., enterprise security reviews, MDM policies) may need to
understand this surface or opt out of it entirely at compile time.
Audit findings
* Which function? pkg/otp.ParseScreen (implemented in screenshot_supported.go) is the
sole caller of the kbinani/screenshot API.
Caller? internal/action.(otpHandler).OTP in internal/action/otp.go, guarded by
if snip { … }.
* User-visible trigger? The --snip / -s flag on gopass otp. Screen capture
cannot be triggered without the user explicitly passing this flag.
* Affected packages? pkg/passkey does not use screenshot. The issue description
was slightly inaccurate on this point.
* Platform scope? Only compiled on (arm|arm64|amd64|386) && (linux|windows|(cgo &&. Other platforms already receive a no-op stub.
darwin)|freebsd|netbsd)
---
Decision
Implement the noscreenshot build tag (Option A below). It is low-risk, additive, and
allows enterprise / policy-aware users to produce a binary without the screen-capture surface
while keeping the default experience unchanged.
---
Options considered
A — Negative opt-out build tag noscreenshot ✅ (chosen)
Add && !noscreenshot to the existing build constraint in screenshot_supported.go and|| noscreenshot to the fallback screenshot_others.go.
go build -tags noscreenshot .Pros:
* No change to the default build; existing users and CI pipelines are unaffected.
* Simple: one tag, two build-constraint lines.
* Canonical Go pattern for feature opt-out (mirrors nomsgpack, noasm, etc.).
Cons:
* The tag name must be documented; there is no automated reminder to keep stubs in sync.
B — Separate module / package pkg/otp/screenshot
Move the screenshot logic into a sub-package and make ParseScreen a function variable
that callers inject.
Pros: clean separation.
Cons: more invasive refactor for a minor gain; deferred.
C — Runtime config flag otp.screenshot: false
A config option to disable the feature at runtime without recompiling.
Pros: no special build required.
Cons: the library is still linked; does not address the "linked capability" concern.
---
Consequences
* gopass otp --snip continues to work for all users who build without the tag.
* Builds with -tags noscreenshot will return "not supported on your platform" for
--snip, consistent with unsupported-platform behaviour.
* github.com/kbinani/screenshot is absent from the linked binary when the tag is set.
* docs/commands/otp.md documents the tag and the screen-capture scope.
---
Adr/README
Architecture Decision Records
Each record states one architectural decision, the context it was made in, the
options considered, and the outcome. Amend a record only to update its status.
Never reuse a number: supersede a decision by writing a new record.
Name records A-NN-<kebab-slug>.md, with NN zero-padded to two digits. Write
the H1 as # A-NN: <Title>, matching the file name. Open the record with aStatus: line — proposed, accepted, deferred, implemented,partially implemented, or superseded by A-NN — and a Source: line.
Update this index in the same commit that adds or supersedes a record.
Index
| ADR | Title | Status | Added |
|---|---|---|---|
| A-03 | Separate Storage and RCS interfaces | deferred | 2026-04-06 |
| A-04 | Fix grep match and error counters | open | 2026-04-06 |
| A-05 | Template engine uses text/template instead of html/template | deferred | 2026-04-06 |
| A-06 | Minimum password length enforcement | deferred | 2026-04-06 |
| A-07 | Hook system dead code and CVE-2023-24055 | deferred | 2026-04-06 |
| A-08 | Shred operation is ineffective on modern storage | accepted | 2026-04-06 |
| A-09 | Low severity and informational findings | accepted | 2026-04-06 |
| A-10 | Code quality findings | open | 2026-04-06 |
| A-11 | Integrate org.freedesktop.secrets D-Bus service | proposed | 2026-05-24 |
| A-12 | pkg/gopass API stability contract | accepted | 2026-05-24 |
| A-13 | Expired GPG key handling and recipient validity warnings | partially implemented | 2026-05-25 |
| A-14 | Effortless team workflows | implemented | 2026-06-06 |
| A-15 | noscreenshot build tag for OTP screen-capture feature | accepted | 2026-05-25 |
Status values are taken from each record's Status: line. Dates are the
authoring commit dates reported by git log --diff-filter=A --follow.
Reserved and renumbered records
A-01 and A-02 are reserved and have no records. Both numbers are cited from
the CHANGELOG.md unreleased section: "Split Action handler into focused
handler types (A-1)" and "Replace context-key config system with typed structs
(A-2)". No file was written for either. Do not reuse these numbers; the
changelog citations would then point at unrelated decisions.
A-15 was renumbered from A-13. Two records carried the number A-13.A-13-expired-gpg-key-handling.md keeps it: it is cited fromdocs/commands/recipients.md, docs/usecases/team-workflows.md, anddocs/adr/A-14-team-workflows.md. The screenshot record had no inbound
citations and was renumbered to A-15.
A-03 through A-09 were zero-padded. Their file names previously used a
single digit, which sorts them after A-10 in any lexical listing.
Unavailable sources
SECURITY_AUDIT_REPORT.md and CODE_QUALITY_REPORT.md are not present in the
working tree. Records A-03 through A-10 cite them in their Source: lines.
Both files were removed in commit 77894053 ("Clean up") and are recoverable
only from git history. The citations identify which finding each record answers,
for example "§ M-4", and are therefore retained.
---
Backends/Age
age crypto backend
The age backend is an experimental crypto backend based on age. It adds an
encrypted keyring on top (using age in scrypt password mode). It also has
(largely untested) support for specifying recipients as github users. This will
use their ssh public keys for age encryption.
It is well positioned to eventually replace gpg as the default crypto backend.
Getting started
WARNING: This backend is experimental and the on-disk format likely to change.
To start using the age backend initialize a new (sub) store with the --crypto=age flag:
$ gopass age identities add [AGE-... age1...]
<if you do not specify an age secret key, you'll be prompted for one>
$ gopass init --crypto ageor use the wizard that will help you create a new age key:
$ gopass setup --crypto ageThis will automatically create a new age keypair and initialize the new store.
Existing stores can be migrated using gopass convert --crypto age.
N.B. for a fully scripted or non-interactive setup, you can use the GOPASS_AGE_PASSWORD env variable
to set your identity file secret passphrase, and specify the age identity and recipients
that should be used for encrypting/decrypting passwords as follows:
$ gopass age identities add <AGE-...> <age1...>
$ GOPASS_AGE_PASSWORD=mypassword gopass init --crypto age <age1...>Notice the extra space in front of the command to skip most shell's history.
You'll need to set your name and username using
git directly if you're using it as storage backend (the default one).For test automation or other environments where pinentry is unavailable, setGOPASS_AGE_STDIN_PASSPHRASE to force gopass to read the passphrase from the terminal instead.
You can also specify the ssh directory by setting environment variable
$ GOPASS_SSH_DIR=/Downloads/new_ssh_dir gopass init --crypto age <age1...>Features
* Encryption using age library, can be decrypted using the age CLI
* Support for native age, ssh-ed25519 and ssh-rsa recipients
* Support for encrypted ssh private keys
* Support for using GitHub users' private keys, e.g. github:user as recipient
* Automatic downloading and caching of SSH keys from GitHub
* Encrypted keyring for age keypairs
* Support for age plugins
* Caching of passphrases via an agent
Agent
The age backend comes with an agent that can cache the passphrases for your age identities.
The agent is started automatically by gopass if it's not already running.
You can disable the agent by setting age.agent-enabled to false in your gopass config.
The agent performs the decryption and the passphrase never leaves the agent process.
The agent listens on a unix socket at $XDG_RUNTIME_DIR/gopass/gopass-age-agent.sock.
You can interact with the agent using the following commands:
- gopass age agent: starts the agent in the foreground.
- gopass age lock: locks the agent, clearing all cached passphrases.
Usage with a yubikey
To use with a Yubikey, age requires the usage of the age-plugin-yubikey plugin.
Assuming you have Rust installed:
$ cargo install age-plugin-yubikey
$ age-plugin-yubikey -i
<should be empty>
$ age-plugin-yubikey
✨ Let's get your YubiKey set up for age! ✨
<follow instructions to setup a PIV slot>
$ age-plugin-yubikey -i
<should display your PIV slot information now>
$ gopass age identities add
Enter the age identity starting in AGE-:
<paste the AGE-PLUGIN-YUBIKEY-... identity from the previous command>
Provide the corresponding age recipient starting in age1:
<paste the age1yubikey1... recipient from the previous command>If gopass tells you waiting on yubikey plugin... when decrypting secrets, it probably is waiting for you to touch
your Yubikey because you've set a Touch policy when setting up your PIV slot.
Roadmap
The future of this backend largely depends on what is happening in the age project itself.
Assuming age is supporting this, we'd like to:
* Finalize GitHub recipient support
* Add Hardware token support
* Make age the default gopass backend
---
Backends/Cryptfs
cryptfs storage backend
The cryptfs backend is an experimental storage backend PREVIEW. It hashes secret names and stores the mapping from names to actual file inside an age encrypted lookup table. The filesystem backing this storage backend is flexible, but by default uses gitfs.
WARNING: Do not use unless you want to contribute to the development of this backend!
---
Backends/Fossilfs
fossilfs storage backend
This is an EXPERIMENTAL storage backend that uses the Fossil SCM. It isn't well tested and only exists to provide an example how a non-git backend could look like.
---
Backends/Fs
fs storage backend
The simplest storage backend, often used for testing.
It stores data directly in the filesystem without any RCS support.
---
Backends/Gitfs
gitfs storage backend
This is the default storage backend. It stores the encrypted data directly in the filesystem. It uses an external git binary to provide history and remote sync operations.
gopass configures git to use persistent ssh connections. If you do not want
this set GIT_SSH_COMMAND to an empty string to override the built-in default.
---
Backends/Gpg
gpg crypto backend
The gpgcli backend is the default crypto backend based on the gpg CLI. It depends on the GPG installation to be working and having a properly initialized keyring.
Getting started
WARNING: This backend suffers from myriads of different configuration options, a poor scripting interface and not pure-Go libarary bindings being available.
To start using the gpgcli backend initialize a new (sub) store with the --crypto=gpgcli flag:
gopass init --crypto gpgcli
gopass recipients add 0xDEADBEEFFeatures
* Compatible with other password store implementations
* Support for all GPG features, like smart-cards or hardware tokens
Caveats
* Using long key sizes (e.g. 4096 bit or longer) can make many operations a lot slower
* Some GPG installations don't work well with concurrent operations
Roadmap
This backend is the single most annoying source of maintenance workload in this project.
We try to keep this backend working as good as possible but there are a lot of reasons
why we'd prefer eventually move beyond GPG.
GPG Critism
This section is a growing list of references why GPG is bad and why you should avoid it.
That might sound like an unusual thing to say for the authors of a tool whose main use case
relies on GPG but whenever we tried to move beyond GPG we got a lot of backlash. So I guess
first we need to try to make use understand why you shouldn't hold on to GPG and by then we'll
try to have a replacement ready for you.
* What's the matter with PGP
* The PGP Problem
* I'm giving up on PGP
* GPG and Me
---
Backends/Jjfs
jjfs storage backend
This is an EXPERIMENTAL storage backend that uses the JJ / Git. It isn't well tested and only exists to provide an example how a non-git backend could look like.
---
Commands/Audit
audit command
The audit command will decrypt all secrets and scan for weak passwords or other common flaws.
Synopsis
$ gopass auditExcludes
You can exclude certain secrets from the audit by adding a .gopass-audit-exclude file to the secret. The file should contain a list of RE2 patters to exclude, one per line. For example:
Lines starting with # are ignored. Trailing comments are not supported.
Exclude all secrets in the pin folder.
Note: These are RE2, not Glob patterns!
pin/.*
Literal matches are also valid RE2 patterns
test_folder/ignore_this
Gopass internally uses forward slashes as path separators, even on Windows. So no need to escape backslashes.
Exit codes
| Code | Meaning |
|-----:|---------|
| 0 | No issues found |
| 14 | One or more weak passwords or issues detected |
See docs/exit-codes.md for the full table.
Password strength backends
| Backend | Description |
|-------------------------------------------------|------------------------------------------------------------------------|
| crunchy | Crunchy password strength checker |
| name | Checks if password equals the name of the secret |
---
Commands/Cat
cat command
The cat command is used to pipe password in and out of STDIN and STDOUT
respectively. As it is intended to be used with binary data, it encodes the
data-stream to store it.
Synopsis
$ echo "test" | gopass cat test/new
$ gopass cat test/newModes of operation
* Create a new entry with data-stream from STDIN
* Change an existing entry to data-stream from STDIN
* Retrive encoded data from password-store and echo it to STDOUT
Cat is intended to work with binary data, so it accepts any kind of stream from
STDIN. It reads the binary-stream from STDIN and encodes it Base64 and saves it
in the password store encoded, with some metadata about the input-stream and the
used encoding (currently only Base64 supported).
Example
$ echo "234" | gopass cat test/new
$ gopass show -f test/new
Secret: test/new
content-disposition: attachment; filename="STDIN"
content-transfer-encoding: Base64
MjM0Cg==
$ gopass cat test/new
234
Differences to insert
In contrast to insert it handles any kind of data-stream from STDIN and
encodes it.
Drawback: you can not just simply read the password with gopass show.
Flags
This command has currently no supported flags except the gopass globals.
---
Commands/Clone
clone command
The clone command allows cloning and setting up a new password store
from a remote location, e.g. a remote git repo.
Synopsis
$ gopass clone [email protected]/store.git
$ gopass clone [email protected]/store.git sub/storeFlags
| Flag | Aliases | Description |
|------------|---------|-----------------------------------------------------------------|
| --path | | The path to clone the repo to. |
| --crypto | | Override the crypto backend to use if the auto-detection fails. |
---
Commands/Config
config command
The config command allows displaying and altering configuration options.
Note: To manage mounts use gopass mounts.
Synopsis
gopass config
gopass config generate.autoclip
gopass config generate.autoclip falseFlags
| Flag | Description |
|-----------|--------------------------------|
| --store | Only sync a specific sub store |
---
Commands/Convert
convert command
The convert command exists to migrate stores between different backend
implementations.
Note: This command exists to enable a possible migration path. If we agree
on a single set of backend implementations the multiple backend support
might go away and this command as well.
Warning: Converting between different RCS backends will loose part of the history. While we try to retain as much information as possible especially the commit timestamps will be set to the convert time.
Synopsis
$ gopass convert --store=foo --move=true --storage=gitfs --crypto=age
$ gopass convert --store=bar --move=false --storage=fs --crypto=plainFlags
Flag | Description
---- | -------------store | Substore to convert.--move | Remove backup after converting? (default: false)--storage | Target storage backend.--crypto | Target crypto backend.
---
Commands/Create
create command
The create command creates a new secret using a set of built-in or custom templates.
It implements a wizard that guides inexperienced users through the secret creating.
The main design goal of this command was to guide users through the creation of a secret
and asking for the necessary information to create a reasonable secret location.
Synopsis
gopass create
gopass create --store=fooModes of operation
* Create a new secret using a wizard
Templates
gopass create will look for files ending in .yml in the folder .gopass/create inside
the selected store (by default the root store).
On first run, gopass writes two built-in templates (website login and PIN code) to this
folder. You can modify them or add your own alongside them.
To add a new template create a YAML file in .gopass/create/ and commit it:
open the store directory
cd "$(gopass config mounts.path)"
mkdir -p .gopass/create
$EDITOR .gopass/create/aws.yml
git add .gopass/create/aws.yml && git commit -m "Add AWS credential template"Template Structure
Each template file is a YAML document with the following top-level fields:
| Field | Type | Required | Description |
|--------------|----------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | string | yes | Human-readable name shown in the wizard's selection menu (e.g., "Website login"). |
| priority | int | no | Sort order in the wizard menu. Lower numbers appear first. Default: 0. The two built-in templates use priorities 0 (website) and 1 (PIN). |
| prefix | string | yes | Directory inside the store where the new secret will be saved (e.g., "websites" → secret stored under websites/<name>). |
| name_from | []string | no | List of attribute names whose values are joined to form the secret's file name. If empty, the user is prompted for a path. Values are sanitised with CleanFilename. |
| welcome | string | no | Message printed at the start of the wizard for this template. Supports Unicode/emoji. |
| attributes | list | yes | Ordered list of attribute definitions (see Attribute Fields below). |
Example skeleton:
---
priority: 5
name: "AWS"
prefix: "aws"
name_from:
- "org"
- "user"
welcome: "🧪 Creating AWS credentials"
attributes:
- name: "org"
type: "string"
prompt: "Organization"
min: 1
- name: "user"
type: "string"
prompt: "User"
min: 1
- name: "password"
type: "password"
prompt: "Password"
charset: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%&*"
min: 10
strict: true
- name: "comment"
type: "string"
prompt: "Comments"Attribute Fields
Each entry in attributes supports the following fields:
| Field | Type | Applies to | Description |
|-----------------|--------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| name | string | all | Key under which the value is stored in the secret's YAML body. Also used as the human-readable label when prompt is omitted. |
| type | string | all | Controls the input behaviour. One of string, hostname, password, or multiline — see Attribute Types below. |
| prompt | string | all | Override the text shown to the user. Defaults to the name field with the first letter upper-cased. |
| min | int | string, hostname, password | Minimum acceptable length. Validation is skipped when 0 (default). For password with auto-generation the minimum is passed to the generator. |
| max | int | string, hostname, password | Maximum acceptable length. Validation is skipped when 0 (default). |
| charset | string | password | Explicit character set for generated passwords. When omitted, the standard mixed-class generator is used. Ignored when the user opts out of generation. |
| always_prompt | bool | password | When true, skip the "Generate Password?" prompt and always ask the user to type one in. Default: false. |
| strict | bool | password | When true (and charset is set), every character class detected in charset (upper, lower, digit, symbol) must appear at least once in the generated password. Equivalent to gopass generate --strict. Default: false. |
Attribute Types
string
Prompts for a single-line text value. The value is stored as-is under the attribute'sname key in the secret's YAML body.
- name: "username"
type: "string"
prompt: "Username"
min: 1
max: 64hostname
Like string, but additionally:
* Extracts the hostname component from the entered value (e.g., https://example.com/login → example.com).
* The extracted hostname is used as the name_from component if this attribute is listed there.
* Looks up password-change URLs via the built-in pwrules database and stores them as password-change-url if found.
- name: "url"
type: "hostname"
prompt: "Website URL"
min: 1password
Prompts the user "Generate Password?". If yes, generates a password using the standard
gopass generator (respecting charset and strict). If no, asks the user to type one
(with confirmation) and applies min/max length validation.
The password is stored as the first line of the secret (the gopass password field),
not as a YAML key.
- name: "password"
type: "password"
prompt: "Password"
charset: "0123456789" # digits only, e.g. for PIN codes
min: 4
max: 8
always_prompt: true # skip the "generate?" questionmultiline
Opens the user's $EDITOR (or the editor configured via gopass config core.editor)
with any existing gopass template for this attribute pre-filled. The full editor content
is written verbatim to the secret body.
Useful for SSH keys, certificates, or annotated notes.
- name: "notes"
type: "multiline"
prompt: "Additional notes"File Naming Convention
Template files must end in .yml or .yaml. gopass ignores files with other extensions.
There is no enforced naming convention, but the built-in templates follow the pattern<priority>-<prefix>.yml (e.g., 0-websites.yml, 1-pin.yml). Using the same convention
makes the order predictable in directory listings.
Flags
| Flag | Aliases | Description |
|-----------|---------|------------------------------------------------------------------|
| --store | -s | Select the store to use. Will be used to look up user templates. |
| --force | -f | For overwriting existing entries. |
| --print | -p | Print the password to STDOUT. |
---
Commands/Delete
delete command
The delete command is used to remove a single secret or a whole subtree.
Note: Recursive operations crossing mount points are intentionally not supported.
Synopsis
$ gopass delete entry
$ gopass rm -r path/to/folder
$ gopass rm -f entry
$ gopass delete entry keyModes of operation
* Delete a single secret
* Delete a single key from an existing secret
* Delete a directoy of secrets
Flags
| Flag | Aliases | Description |
|---------------|---------|---------------------------------------|
| --recursive | -r | Recursively delete files and folders. |
| --force | -f | Do not ask for confirmation. |
Exit codes
| Code | Meaning |
|-----:|---------|
| 0 | Secret deleted successfully |
| 10 | Secret not found |
See docs/exit-codes.md for the full table.
Details
* Removing a single key will need to decrypt the secret
---
Commands/Doctor
doctor command
The doctor command checks your gopass installation for common configuration
issues and reports the results. With the --recipients flag, it performs a
detailed recipient consistency diagnostic across all stores.
Synopsis
$ gopass doctor [--verbose]
$ gopass doctor --recipients [--verbose]Description
Runs a series of diagnostic checks on the gopass installation. Exits with a
non-zero status if any check fails, which makes it suitable for scripting.
Checks performed:
| Check | Description |
|---|---|
| GPG binary | Verifies gpg is in PATH when a store uses GPG encryption |
| age binary | Verifies age is in PATH when a store uses age encryption |
| git binary | Verifies git is in PATH when a store uses the gitfs backend |
| git identity | Checks that user.name and user.email are set in git config for each git-backed store |
| store permissions | Checks that each store directory exists and is not world-writable |
| recipient keys | Checks that all recipient keys are valid and not expired |
| git remote | Warns (but does not fail) if a git-backed store has no remote configured |
--recipients mode
When --recipients is passed, gopass doctor performs a detailed,
per-store, per-recipient consistency diagnostic:
- Non-canonical IDs — warns when a recipient is stored with an ambiguous
identifier (e.g. email address) instead of its full fingerprint. Suggests
running gopass recipients canonicalize.
- Missing keys — errors when a recipient is neither in the local keyring
nor in .public-keys/.
- .public-keys/ only — warns when a recipient's key is only available in
the store's .public-keys/ directory (not in the local keyring). Suggests
running gopass sync.
- Expired / unusable keys — detects when a key is present in the keyring
by fingerprint but not usable by direct ID lookup (expired). Recommends
gopass recipients update followed by gopass sync.
The diagnostic is read-only (no decryption needed) and safe to run at any time.
Flags
| Flag | Aliases | Description |
|---|---|---|
| --verbose | -v | Show passing checks in addition to warnings and errors |
| --recipients | | Run a detailed recipient consistency diagnostic across all stores |
Exit codes
| Code | Meaning |
|---|---|
| 0 | All checks passed |
| non-zero | One or more checks failed |
Examples
Run all checks, show only failures
$ gopass doctorRun all checks, show every result
$ gopass doctor --verboseRun detailed recipient diagnostic
$ gopass doctor --recipientsRun recipient diagnostic with full detail
$ gopass doctor --recipients --verbose---
Commands/Edit
edit command
The edit command loads a new or existing secret into your $EDITOR (default: vim)
and saves the resulting content in the password store. It will attempt to create secure
temporary directory (depending on the OS) and will warn if insecure editor configuration
(currently only vim) is detected.
Native gopass MIME secrets are syntax checked and invalid encodings are rejected.
Any other type of secret is accepted as is.
gopass will honor templates when creating a new entry.
Synopsis
$ gopass edit entry
$ gopass edit -e /bin/nano entry
$ EDITOR=/bin/nano gopass edit entryModes of operation
* Create a new secret
* Edit an existing secret
Flags
| Flag | Aliases | Description |
|------------|---------|---------------------------------------------------------------------------------------------------------------------------------------|
| --editor | -e | Specify the path to an editor. Must accept the filename as it's first argument. |
| --create | -c | Create a new secret. You can create a new secret with edit with or without -c, but -c will skip searching for existing matches. |
---
Commands/Env
env command
Security warning: Any mode that injects secrets as environment variables
(defaultand--exec) exposes those values to every process that can read
/proc/<pid>/environon Linux orps ewwon macOS for the entire lifetime of
the subprocess. If secret exposure via the process environment is a concern,
use--stdin(single secret) or--file(ramdisk-backed temp file) instead.
The env command runs a binary as a subprocess with a pre-populated environment.
The environment of the subprocess is populated with a set of environment variables corresponding
to the secret subtree specified on the command line.
Synopsis
$ gopass env [options] secret-or-prefix command [args...]Flags
| Flag | Description |
|------|-------------|
| --keep-case / -kc | Do not uppercase the environment variable name (default: names are uppercased) |
| --stdin | Pipe the secret's password to the subprocess's stdin instead of injecting it into the environment |
| --file | Write each secret to a ramdisk temporary file and export KEY_FILE=/path/to/file instead of KEY=value |
| --exec | Replace the current gopass process with the subprocess via exec(3) (Linux/macOS only; not supported on Windows) |
--stdin, --file, and --exec are mutually exclusive.
Modes
Default (env injection)
$ gopass env db/prod psql -U admin mydbEach secret key under the given prefix is exported as an uppercased environment variable
(DB_PASSWORD=secret). The subprocess runs as a child process of gopass.
Security caveat: The injected variables are visible in /proc/<pid>/environ on Linuxand via ps eww on macOS for as long as the subprocess is running. Any local processwith read access to /proc (including other user processes on a shared system) canobserve these values. Prefer--stdinor--filefor sensitive credentials.
--stdin
$ gopass env --stdin db/password gpg --passphrase-fd 0 --decrypt file.gpgThe secret's password is written to the subprocess's stdin. No environment variable is
set, so the secret is never visible in /proc/<pid>/environ or ps output.
Caveats:
- Only works with a single secret. Passing a directory/prefix is not supported.
- The subprocess must be designed to read credentials from stdin (e.g. via
--passphrase-fd 0,--password-stdin, or similar flags). Programs that do
not read from stdin at all will hang waiting for input.
- The password is written to stdin without a trailing newline. Most programs
(e.g. gpg --passphrase-fd 0) accept this, but a small number of programsrequire a newline-terminated passphrase. Wrap with printf '%s\n' or a shellheredoc in those cases.
- The subprocess's own stdin is replaced by the secret. If the subprocess also
needs interactive stdin input from the user, this mode is not suitable.
--file
$ gopass env --file db/prod psql -U admin mydbEach secret is written to a ramdisk-backed temporary file (on Linux via /dev/shm, on macOS
via a RAM disk). The environment variable is set to KEY_FILE=/path/to/tmpfile following the*_FILE convention used by Docker Compose and HashiCorp Vault. All temporary files are
removed automatically when the subprocess exits.
Caveats:
- On Windows there is no ramdisk support; temp files fall back to the regular OS
temporary directory, which resides on a persistent disk. The secret may be
recoverable from disk after deletion.
- Temp files are deleted (not shredded) on exit. On SSDs with wear leveling,
journaling filesystems, or copy-on-write filesystems (ZFS, Btrfs, APFS) the
original data may persist in reallocated blocks or journal entries.
- TheKEY_FILEvariable itself is visible in/proc/<pid>/environ, though it
exposes only the file path, not the secret value.
--exec
$ gopass env --exec db/prod psql -U admin mydbUses exec(3) (via syscall.Exec) to replace the current gopass process with the
subprocess. Because gopass disappears from the process table entirely, there is no lingering
parent process whose /proc/<pid>/environ can be observed.
Caveats:
- Not supported on Windows.
- The injected variables are still present in the subprocess's own /proc/<pid>/environ.--exec eliminates the gopass parent from the process table but does not preventthe subprocess from exposing the variables.
- Because the gopass process is replaced, any deferred cleanup (e.g. temp files from
a previous --file call in the same invocation) will not run after the subprocessexits.
Choosing a mode
| Scenario | Recommended mode |
|----------|------------------|
| Single secret for a program that reads stdin | --stdin |
| Multiple secrets or program does not support stdin | --file |
| Program requires env variables and secrets are low-sensitivity | default or --exec |
| Must avoid a lingering gopass process in ps output | --exec |
Security summary
| Mode | Secret in env? | Visible in /proc? | Ramdisk? |
|------|---------------|---------------------|----------|
| Default | Yes (KEY=value) | Yes (subprocess PID) | No |
| --exec | Yes (KEY=value) | Yes (subprocess PID) | No |
| --file | No (path only) | File path only | Yes (Linux/macOS) |
| --stdin | No | No | N/A |
---
Commands/Find
find command
The find command will attempt to do a simple substring match on the names of all secrets.
If there is a single match it will directly invoke show and display the result.
If there are multiple matches a selection will be shown.
Note: The find command will not fall back to a fuzzy search.
Synopsis
$ gopass find entry
$ gopass find -f entry
$ gopass find -c entryFlags
| Flag | Aliases | Description |
|------------|---------|---------------------------------------------------------------|
| --clip | -c | Copy the password into the clipboard. |
| --unsafe | -u | Display any unsafe content, even if safecontent is enabled. |
| --regex | -r | Interpret the pattern as a regular expression instead of a plain substring match. |
Exit codes
| Code | Meaning |
|-----:|---------|
| 0 | Matches found and displayed |
| 10 | No matching secret found |
See docs/exit-codes.md for the full table.
---
Commands/Fsck
fsck command
gopass can check integrity of it's password stores with the fsck command.
It will ensure proper file and directory permissions as well as proper
recipient coverage (on supported crypto backends, only).
Synopsis
$ gopass fsckModes of operation
* Check the entire password store, incl. all mounts
* Check only the specified mount
Flags
Flag | Aliases | Description
---- | ------- | -------------decrypt | | Decrypt and reencrypt all secrets.
Exit codes
| Code | Meaning |
|-----:|---------|
| 0 | Store integrity OK |
| 15 | One or more integrity errors found |
See docs/exit-codes.md for the full table.
---
Commands/Fscopy
fscopy command
The fscopy command is used to copy a file from your filesystem into your
password store, while keeping it in clear in your local filesystem after
having stored it in your encrypted store.
Synopsis
$ gopass fscopy ~/test/file data/test/file-entry
$ gopass fscopy data/test/file-entry ~/fileModes of operation
This command either reads a file from the filesystem and writes the
encoded and encrypted version in the store or it decrypts and decodes
a secret and writes the result to a file. Either source or destination
must be a file and the other one a secret.
If you want the source to be removed use 'gopass fsmove'.
fscopy is intended to work with raw files.
Example
$ gopass fscopy ~/test/file data/test/file-entry
$ gopass cat data/test/file-entrySee also the docs for the cat action.
Flags
This command has currently no supported flags except the gopass globals.
---
Commands/Fsmove
fsmove command
The fsmove command is used to move a file from your filesystem into your
password store, erasing it from your local filesystem after having stored it in your encrypted store.
Synopsis
$ gopass fsmove ~/test/file data/test/file-entry
$ gopass fsmove data/test/file-entry ~/fileModes of operation
This command either reads a file from the filesystem and writes the
encoded and encrypted version in the store or it decrypts and decodes
a secret and writes the result to a file. Either source or destination
must be a file and the other one a secret. The source will be wiped
from disk or from the store after it has been copied successfully
and validated. If you don't want the source to be removed use
'gopass fscopy'.
fsmove is intended to work with raw files.
Example
$ gopass fsmove ~/test/file data/test/file-entry
$ gopass cat data/test/file-entrySee also the docs for the cat action.
Flags
This command has currently no supported flags except the gopass globals.
---
Commands/Generate
generate command
The generate command is used to generate a new password and store it into the password store.
Note: If you only want generate a password without storing it in the store, use the pwgen command.
Synopsis
gopass generate entry [length]
gopass generate entry key [length]Modes of operation
* Generate a new entry with a new password, e.g. a new login. Setting the Password field, gopass generate entry [chars]
* Re-generating a new password and setting it in the Password field of an existing entry
* Generate a new password and setting it to a new key of an existing secret, e.g. gopass generate entry key [chars]
* Re-generate a new password for an existing key in an existing entry
Flags
| Flag | Aliases | Description |
|---------------|---------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| --clip | -c | Copy the generated password into the clipboard. Default: Value of autoclip |
| --print | -p | Print the generated password to the terminal. Default: false. |
| --force | -f | Force overwriting an existing entry. |
| --edit | -e | Generate a password and open the entry for editing in $EDITOR. |
| --generator | -g | Choose of of the available password generators, desribed below. Default: cryptic |
| --symbols | -s | Include symbols in the generated password (default: false) |
| --strict | | Ensure each requested character class is actually included. Without this option all requested classes can be included, but not necessarily are. (default: false) |
| --xkcd-sep | --sep, --xkcdsep | Word separator for multi-word generators. |
| --xkcd-lang | --lang, --xkcdlang | Language for word-based generators. |
| --xkcd-capitalize | --xkcdcapitalize | Capitalize the first letter of each word when using the xkcd generator. Equivalent to setting pwgen.xkcd-capitalize = true in config. |
| --xkcd-numbers | --xkcdnumbers | Append a random number to each word when using the xkcd generator. Equivalent to setting pwgen.xkcd-numbers = true in config. |
Password Generators
Use --generator to select one of the available password generators:
| Generator | Description |
|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| cryptic | The default generator yields cryptic passwords that should work with most sites. Use --symbols and --strict if the site has specific requirements. Please note that we auto-detect the correct rules for some sites. The length argument specifies the number of characters. |
| xkcd | Use an XKCD#936 style password. Use --xkcd-lang and --xkcd-sep to refine its behaviour. The length argument specifies the number of words. |
| memorable | Generate a memorable password. The length argument specifies the minimum lenght of characters. Please note that the password might be longer if not all necessary rules were satisfied by the minimum length solution. |
| external | Use the external generator from $GOPASS_EXTERNAL_PWGEN |
Exit codes
| Code | Meaning |
|-----:|---------|
| 0 | Password generated and stored successfully |
| 12 | Generated secret could not be encrypted and saved |
See docs/exit-codes.md for the full table.
Relevant configuration options
* autoclip only applies to generate. If set the generated password is automatically copied to the clipboard - unless --clip is explicitly set to --clip=false
* safecontent will suppress printing of the password, unless -p is set. The password will not be copied, unless -c or the autoclip option are set.
Templates
When creating a new entry gopass will look for the most specific template
by going up in the secret path looking for a file called .pass-template.
If any such file is found it will be used to pre-populate the generated
secret.
---
Commands/Gopass
gopass command
Calling gopass without any command argument is a common entry point and
has two different modes.
Synopsis
$ gopass
$ gopass entry
$ gopass -c entryModes of operation
* Invoked without any arguments gopass will start an interactive REPL shell. This includes zero-setup command completion and passphrase caching (for non-GPG backends).
* Invoked with one argument it will perform a (fuzzy) search and display a list of matches or the secret directly (if exactly one match).
* Invoked with two arguments it will do search and if there is a match display the named key.
Flags
Note: DO NOT use in scripts! Use gopass show instead.
| Flag | Aliases | Description |
|------------|---------|----------------------------------------------------------------------------------------------------------------------------|
| --clip | -c | Copy the password value into the clipboard and don't show the content. |
| --unsafe | -u | Display unsafe content (e.g. the password) even when the safecontent option is set. No-op when safecontent is false. |
| --yes | | Assume yes on all yes/no questions or use the default on all others. |
---
Commands/Grep
grep command
The grep command works like the Unix grep tool. It decrypts all secrets
and performs a substring or regexp match on the given pattern.
Synopsis
$ gopass grep foobarModes of operations
* Search for the given pattern in all secrets
Flags
None.
Flag | Aliases | Description
---- | ------- | -------------regexp | | Parse the pattern as a RE2 regular expression.
---
Commands/History
history command
The gopass history command will show all revisions of a given secret.
Synopsis
$ gopass history entryModes of operation
* Display all revisions of the given secret.
Flags
None.
---
Commands/Init
init command
The init command is used to initialize a new password store.
If no recipients are specified a useable existing private key is used.
The init command must be used to initilize new mounts. gopass mounts add only supports adding existing mounts.
Note: We do not support adding recipients using init. Please use gopass recipients add for that!
Synopsis
$ gopass init
$ gopass init --crypto [age|gpg] --storage=[fs|gitfs]Flags
| Flag | Aliases | Description |
|-------------|---------|-------------------------------------------------------------------------------------------------------------|
| --path | -p | Initialize the (sub) store in this location. |
| --store | -s | Mount the newly initialized sub-store at this mount point |
| --crypto | | Select the crypto backend. Choose one of: gpgcli, age, xc (deprecated) or plain. Default: gpgcli |
| --storage | | Select the storage and RCS backend. Choose one of: gitfs, fs. Default: gitfs |
See backends.md for more information on the available backends.
---
Commands/Insert
insert command
The insert command is used to manually set (insert, or change) a password in the store. It applies to either new or existing secrets.
Synopsis
$ gopass insert entry
$ gopass insert entry keyModes of operation
* Create a new entry with a user-supplied password, e.g. a new site with a user-generated password or one picked from gopass pwgen: gopass insert entry
* Change an existing entry to a user-supplied password
* Create and change any field of a new or existing secret: gopass insert entry key
* Read data from STDIN and insert (or append) to a secret
Insert is similar in effect to gopass edit with the advantage of not displaying any content of the secret when changing a key.
Exit codes
| Code | Meaning |
|-----:|---------|
| 0 | Secret inserted successfully |
| 11 | Existing secret could not be read for append or key-insert |
| 12 | Secret could not be encrypted and saved |
See docs/exit-codes.md for the full table.
Note: insert will not change anything but the Password field (using the insert entry invocation) or the specified key (using the insert entry key invocation).
Flags
| Flag | Aliases | Description |
|---------------|---------|------------------------------------------------------------------------------------------------------------------------|
| --echo | -e | Display the secret while typing (default: false) |
| --multiline | -m | Insert using $EDITOR (default: false). This identical to running gopass edit entry. All other flags are ignored. |
| --force | -f | Overwrite any existing value and do not prompt. (default: false) |
| --append | -a | Append to any existing data. Only applies if reading from STDIN. (default: false) |
---
Commands/Link
link command
The link (or ln) command is used to create a symlink from one secret in a
store to a target in the same store.
Note: Symlinks across different stores / mounts are currently not supported!
Note: audit and list do not recognize symlinks, yet. They will treat
symlinks as regular (different) entries.
Synopsis
$ gopass ln foo/bar bar/baz
$ gopass show foo/bar
$ gopass show bar/bazModes of operations
* Create a symlink from an existing secret to a new name, the target must not exist, yet
Note: Use gopass rm to remove a symlink.
Flags
None.
---
Commands/List
list command
The list command is used to list all the entries in the password store or at a given prefix.
Synopsis
gopass ls
gopass ls path/to/entries- List all the entries in the password store including the one in mounted stores: gopass list
- List all the entries in a given folder showing their relative path from the root: gopass list path/to/entries
Note: list will not change anything, nor encrypt or decrypt anything.
Flags
| Flag | Aliases | Description |
|------------------|------------|-----------------------------------------------------|
| --limit value | -l value | Max tree depth (default: -1) |
| --flat | -f | Print a flat list of secrets (default: false) |
| --folders | -d | Print a flat list of folders (default: false) |
| --strip-prefix | -s | Strip prefix from filtered entries (default: false) |
The --flat and --folders flags provide a plaintext list of the entries located at
the given prefix (default prefix being the root /). They are notably used to produce the
completion results.
The --flat one will list all entries, one per line, using its full path.
The --folders one will display all the folders, one per line, recursively per level.
For instance an entry folder/sub/entry would cause it to list both:
$ gopass list --folders
folder
folder/subwhereas gopass list --flat would have just displayed one line: folder/sub/entry.
The --strip-prefix flag is meant to be used along with --flat or --folders.
It will list the relative path from the current prefix, removing the said prefix,
instead of listing the relative paths from the root.
For instance on entry folder/sub/entry, running gopass ls -f -s folder would display
only sub/entry instead of folder/sub/entry.
The --limit flag starts counting its depth from the root store, which means that
a depth of 0 only lists the items in the root gopass store:
$ gopass list -l 0
gopass
├── bar/
├── foo/
└── test (/home/user/.local/share/gopass/stores/substore1)A value of 1 would list all the items in the root, plus their sub-items but no more:
$ gopass list -l 1
gopass
├── bar/
│ └── bar
├── foo/
│ ├── bar
│ └── foo
└── test (/home/user/.local/share/gopass/stores/substore1)
└── fooA negative value lists all the items without any depth limit.
$ gopass list -l -1
gopass
├── bar/
│ └── bar
├── foo/
│ ├── bar/
│ │ ├── bar/
│ │ │ └── bar
│ │ └── baz
│ └── foo
└── test (/home/user/.local/share/gopass/stores/substore1)
└── fooThe flags can be used together: gopass -l 1 -d will list only the folders up to a depth of 1:
$ gopass list -l 1 -d
bar/
foo/
foo/bar/
test/
test/foo/Shadowing
It is possible to have a path that is both an entry and a folder. In that case the list command
will display the folder with a marker of (shadowed), it can still be accessed usinggopass show path/to/it, while the content of the folder can be listed using gopass list path/to/it.
It should also be noted that the mount command can completely "shadow" an entry in a password store,
simply by having the same name and this entry and its subentries will not be visible
using ls anymore until the substore is unmounted.
The entries shadowed by a mount will not show up in a search and cannot be accessed at all without unmounting.
For instance in our example above, maybe there is an entry test/zaz in the root store,
but since the substore is mounted as test/, it only displays the content of the substore.
Unmounting it reveals its shadowed entries:
$ gopass list test
test/
└── foo
$ gopass mounts rm test
$ gopass list test
test/
└── zaz---
Commands/Mounts
mounts commands
The mounts commands allow managing mounted substores. This is one of the
distinctive core features of gopass and we aim making working with substores
as seamless as possible.
Instead of support for encrypting different parts of a store for different
recipients we instead encourage users to mount different stores - each
encrypted to a uniform set of recipients - into a semless virtual tree structure.
This feature is modeled after standard POSIX mount semantics.
Synopsis
$ gopass mounts
$ gopass mounts add mount/point /path/to/store
$ gopass mounts remove mount/pointModes of operation
* Add a new mount
* List existing mounts
* Remove an existing mount
Creating new mounts
You can also create new mounts using init even if your store is already initialized:
gopass init --store mynewsubstore pgpkeyidentitfier(You can also specify a specific local path using --path, just make sure to keep your PGP key identifier, e.g. its email or fingerprint, as the last argument.)
---
Commands/Move
move command
Note: The implementations for copy and move are exactly the same. The only difference is that move will remove the source after a successful copy.
The move command works like the Unix mv or rsync binaries. It allows moving either single entries or whole folders around. Moving across mounts is supported.
If the source is a directory, the source directory is re-created at the destination if no trailing slash is found. Otherwise the contained secrets are placed into the destination directory (similar to what rsync does).
Please note that move will always decrypt the source and re-encrypt at the destination.
Moving a secret onto itself is a no-op.
Synopsis
Overwrite new/leaf
$ gopass move path/to/leaf new/leaf
Move the content of path/to/somedir to new/dir/somedir
$ gopass move path/to/somedirdir new/dir
Does nothing
$ gopass move entry entryModes of operation
* Move a single secret from source to destination
* Move a folder of secrets, possibly with sub folders, from source to destination
Flags
| Flag | Aliases | Description |
|-----------|---------|------------------------------------------------|
| --force | -f | Overwrite existing destination without asking. |
Details
* To simplify the implementation and support multiple backends a copy or move operation will always decrypt and re-encrypt all affected secrets. Even if moving encrypted files around might be possible.
* You can move a secret to another secret, i.e. overwrite the destination. But gopass won't let you move a directory over a file. In that case you have to delete the destination first.
---
Commands/Otp
otp command
The otp command generates TOTP tokens from an OTP URL (otpauth://).
The command tries to parse the password and the totp fields as an OTP URI.
Note: HTOP is supported, but requires a counter field to keep track of it.
Note: If show.safecontent is enabled, OTP URIs are hidden from the show command,
see the docs for show to learn more about it.
Screen capture dependency
The --snip mode requires the github.com/kbinani/screenshot library to capture the
contents of the display(s) so that gopass can locate and decode an OTP QR code. Screen
capture is only performed when the user explicitly passes --snip (-s); it is never
triggered automatically.
This capability is compiled in by default on supported platforms
(arm, arm64, amd64, 386 on Linux, Windows, FreeBSD, and NetBSD; CGo Darwin).
Users who prefer to omit this surface (e.g. in enterprise or policy-restricted environments)
can build gopass without it:
go build -tags noscreenshot .When built with noscreenshot, the --snip flag will return an error on all platforms
and the github.com/kbinani/screenshot package will not be linked into the binary.
Modes of operation
* Generate the current TOTP token from a valid OTP URL
* Snip the screen to add a TOTP QR code as an OTP field to an entry.
Flags
| Flag | Aliases | Description |
|--------------|---------|--------------------------------------------------------------------------|
| --clip | -c | Copy the time-based token into the clipboard. |
| --alsoclip | -C | Copy the time-based token into the clipboard and show it. |
| --qr | -q | Write QR code to file. |
| --chained | -p | chain the token to the password |
| --password | -o | Only display the token. For use in scripts. |
| --snip | -s | Try and find a QR code in the screen content to add as OTP to the entry. |
Supported formats
Your secret needs to either contain a otpauth, hotp or a totp field.
When using the OTP code directly you can simply add it to a secret usinggopass insert your/entry totp.
The otp command also tries to parse the body of your secret to try and find a line starting
by otpauth:// in case you're not using the key-value format for your secret.
Finally, if your secret contains nothing but a password on the first line, the otp command
will try and use that password to generate an OTP code. This allows use-cases where you
store your password in a given entry and your OTP code in another dedicated entry.
The otpauth URIs are typically communicated through a QR code which can be read on Linux using
the gopass otp -s your/entry flag. It should also work if they are added usinggopass insert your/entry otpauth, but won't work if you add them under the totp
or hotp keys.
Steam OTP is supported, but requires using the otpauth URI input to specify the
encoder, e.g. otpauth://totp/username%20steam:username?secret=qlt6vmy6svfx4bt4rpmisaiyol6hihca&period=30&digits=5&issuer=username%20steam&encoder=steam.
---
Commands/Process
process command
The process command extends the gopass templating to support user-supplied
template files that will be processed. These templates can access the users
credentials with the template functions documented below. That way users can
store their full configuration files publicly accessible and have any of the
recipients automatically populate it to generate a complete configuration file
on the fly.
gopass process writes the result to STDOUT. You'll likely want to redirect
it to a file.
Synopsis
$ gopass process <TEMPLATE> > <OUTPUT>Flags
None.
Examples
The templates are processed using Go's text/template package.
A set of helpful template functions is added to the template. See below for a list.
Populate a MySQL configuration
$ cat /etc/mysql/my.cnf.tpl
[client]
host=127.0.0.1
port=3306
user={{ getval "server/local/mysql" "username" }}
password={{ getpw "server/local/mysql" }}
$ gopass process /etc/mysql/my.cnf.tpl
[client]
host=127.0.0.1
port=3306
user=admin
password=hunter2Template functions
Function | Example | Description
-------- | ------- | -----------md5sum | {{ getpw "foo/bar" \| md5sum }} | Calculate the hex md5sum of the input.sha1sum | {{ getpw "foo/bar" \| sha1sum }} | Calculate the hex sha1sum of the input.md5crypt | {{ getpw "foo/bar" \| md5crypt }} | Calculate the md5crypt of the input.ssha | {{ getpw "foo/bar" \| ssha }} | Calculate the salted SHA-1 of the input.ssha256 | {{ getpw "foo/bar" \| ssha256 }} | Calculate the salted SHA-256 of the input.ssha512 | {{ getpw "foo/bar" \| ssha512 }} | Calculate the salted SHA-512 of the input.get | {{ get "foo/bar" }} | Insert the full secret.getpw | {{ getpw "foo/bar" }} | Insert the value of the password field from the given secret.getval | {{ getval "foo/bar" "baz" }} | Insert the value of the named field from the given secret.argon2i | {{ getpw "foo/bar" \| argon2i }} | Calculate the Argon2i hash of the input.argon2id | {{ getpw "foo/bar" \| argon2id }} | Calculate the Argon2id hash of the input.bcrypt | {{ getpw "foo/bar" \| bcrypt }} | Calculate the Bcrypt hash of the input.blake3 | {{ getpw "foo/bar" \| blake3 }} | Calculate the BLAKE-3 hash of the input.
---
Commands/Pwgen
pwgen command
The pwgen command implements a subset of the features of the Unix/Linuxpwgen command line tool. It aims to eventually support most of the pwgen
flags and mirror it's behaviour. It is mainly implemented as a curtosy for
Windows users.
Modes of operation
* Generate a few dozen random passwords with the chosen length
Usage
gopass pwgen [optional length]Synopsis
gopass pwgen
gopass pwgen 24Flags
Flag | Aliases | Description
---- | ------- | -------------no-numerals | -0 | Do not include numerals in the generated passwords.--one-per-line | -1 | Print one password per line.--xkcd | -x | Use multiple random english words combined to a password.--xkcd-sep | --sep, --xkcdsep | Word separator for multi-word passwords.--xkcd-lang | --lang, --xkcdlang | Language to generate password from. Currently only supports english (en, default).--xkcd-capitalize | --xkcdcapitalize | Capitalize the first letter of each word in the generated xkcd password.--xkcd-numbers | --xkcdnumbers | Add a random number to the end of the generated xkcd password.--memorable | -m | Use the memorable (word-based) password generator. The length is a minimum (output may be longer). Incompatible with --no-numerals.--memorable-capitalize | --memorablecapitalize | Capitalize (some) words in the generated memorable password. Implies --memorable.
Notes
* With --memorable, the requested length is a minimum — the generated password is usually longer because whole words are concatenated.
* --memorable always includes digits (one per word), so it is incompatible with --no-numerals; the command errors out instead of silently ignoring it.
* --memorable ignores --ambiguous. --memorable and --xkcd are mutually exclusive (combining them is an error).
---
Commands/Recipients
recipients commands
The set of recipients commands allow managing public keys that are able to
decrypt a given password store. These commands are one of the more uniquegopass features and we aim to make working with teams as seamless as possible.
For the full team workflow reference, see
docs/usecases/team-workflows.md.
Synopsis
$ gopass recipients
$ gopass recipients add [--store=<store>] <recipient-id>...
$ gopass recipients remove [--store=<store>] <recipient-id>...
$ gopass recipients update [--store=<store>] [<recipient-id>...]
$ gopass recipients canonicalize [--store=<store>]
$ gopass recipients ack [--store=<store>]Subcommands
recipients list (default — no subcommand)
Lists all existing recipients for every mounted store.
recipients add (aliases: authorize)
Adds one or more recipients to a store and re-encrypts all secrets so the new
recipients can decrypt them.
Recipient identifiers may be email addresses, short key IDs, or full
fingerprints. gopass normalizes every identifier to its canonical form (full
GPG fingerprint) before storing it in .gpg-id, ensuring the entry and the.public-keys/<id> filename always match (see
ADR A-14).
Example:
$ gopass recipients add --store team-a [email protected]
Resolved '[email protected]' to canonical key ID '0x1A2B3C4D5E6F'
√ Added 1 recipients
You need to run 'gopass sync' to push these changesFlags: --store (store to operate on), --force (skip confirmation).
recipients remove (aliases: rm, deauthorize)
Removes a recipient from a store and re-encrypts all secrets. After removal,
the removed recipient can no longer decrypt new changes — but they can still
decrypt old revisions from the git history. Always rotate secrets after
removing a recipient.
Removal also performs recipient-scoped cleanup: the removed recipient's.public-keys/<id> and legacy .gpg-keys/<id> files are deleted. No other
recipient's files are affected.
Flags: --store, --force.
recipients update (aliases: refresh)
Re-exports the named recipients' public keys from the local keyring into.public-keys/, overwriting stale copies. Use this after extending an expired
key or adding new subkeys. If no IDs are given, your own key is updated.
Example:
$ gopass recipients update --store team-a
Refreshing public keys in store "team-a" ...
Updated public key for '0x1A2B3C4D5E6F'.
Done. You may want to run 'gopass sync' to push the updated keys.Flags: --store.
recipients canonicalize (aliases: canon)
Rewrites the .gpg-id file of a store so that every recipient ID is in its
canonical (full-fingerprint) form and renames the corresponding.public-keys/ files to match. Safe migration — no re-encryption required.
Run this once on existing stores that use non-canonical IDs (email addresses
or short key IDs). After running, use gopass sync to publish the changes.
Flags: --store.
recipients ack (aliases: acknowledge)
Updates recipients.hash after manually validating changes to the recipients
list. This is part of the experimental recipients hashing feature (see below).
Flags: --store.
Common flags
| Flag | Description |
|------|-------------|
| --store | Store to operate on. |
| --force | Skip confirmation prompts (supported on add and remove). |
Important Remarks
WARNING: Removing a recipient can only ever work for new or changed secrets.
When a recipient is removed they will still be able to access anything that
they used to have access to. As a logical consequence one should change
all secrets when removing a recipient.
Recipients hashing
This is an experimental feature that will hash the content of each mount's
recipients file (only the top most file) to display a warning when this is
changed by anyone else (local changes update it without warning). This
can happen either when a teammate modifies that file or when an attacker
tries to modify the recipients file in the central storage to get themselves
added to any newly modified secrets.
Key refresh and expiry recovery
When a GPG key expires, other team members will see warnings during sync.
The key owner should extend the key locally and then run:
$ gopass recipients update --store <team-store>
$ gopass sync --store <team-store>Other members will pick up the refreshed key on their next gopass sync.
You can check the health of your recipient keys at any time with:
$ gopass doctor --recipientsSee also ADR A-13 for details
on expired key handling.
---
Commands/Show
show command
The show command is the most important and most frequently used command.
It allows displaying and copying the content of the secrets managed by gopass.
Synopsis
$ gopass show entry
$ gopass show entry key
$ gopass show entry --qr
$ gopass show entry --passwordModes of operation
* Show the whole entry: gopass show entry
* Show a specific key of the given entry: gopass show entry key (only works for key-value or YAML secrets)
Flags
Flag | Aliases | Description
---- | ------- | -------------clip | -c | Copy the password value into the clipboard and don't show the content.--alsoclip | -C | Copy the password value into the clipboard and show the content.--qr | | Encode the password field as a QR code and print it. Note: When combining with -c/-C the unencoded password is copied. Not the QR code.--qrbody | | Encode the entire body (all lines after the first) as a QR code and print it.--unsafe | -u | Display unsafe content (e.g. the password) even when the safecontent option is set. No-op when safecontent is false.--safe | -s | Hide unsafe content (e.g. the password) even when the safecontent option is false. Overrides the config value for this invocation.--password | -o | Display only the password. For use in scripts. Takes precedence over other flags.--revision | -r | Display a specific revision of the entry. Use an exact version identifier from gopass history or the special -<N> syntax. Does not work with native (e.g. git) refs.--noparsing | -n | Do not parse the content, disable YAML and Key-Value functions.--nofuzzysearch | | Do not start fuzzy search if the requested entry is not found.--nosync | | Disable auto-sync for this invocation.--chars | | Display selected characters from the password.
Details
This section describes the expected behaviour of the show command with respect to different combinations of flags and
config options.
Note: This section describes the expected behaviour, not necessarily the observed behaviour.
If you notice any discrepancies please file a bug and we will try to fix it.
Note: The parser ensures every parsed secret contains a terminating newline, even if the stored content did not. When displaying via gopass show the trailing newline is suppressed so that copying output does not include a spurious newline character. When piping output to another command (gopass show entry | …), the trailing newline is preserved to make the output compatible with standard Unix text-processing tools.
* When no flag is set the show command will display the full content of the secret and will parse it to support key-value lookup and YAML entries.
If the safecontent option is set to true any secret fields (current default is only password) are replaced with a random number of '*' characters (length: 5-10).
Using the --unsafe flag will reveal these fields even if safecontent is enabled. --password takes precedence of safecontent=true as well and displays only the password.
* The --noparsing flag will disable all parsing of the output, this can help debugging YAML secrets for example, where key: 0123 actually parses into octal for 83.
* The --clip flag will copy the value of the Password field to the clipboard and doesn't display any part of the secret.
* The --alsoclip option will copy the value of the Password field but also display the secret content depending on the safecontent setting, i.e. obstructing the Password field if safecontent is true or just displaying it if not.
The --qr flags operates complementary to other flags. It will additionally format the value of the Password entry as a QR code and display it. Other than that it will honor the other options, e.g. gopass show --qr will display the QR code and* the whole secret content below. One special case is the -o flag, this flag doesn't make a lot of sense in combination, so if both --qr and -o are given only the QR code will be displayed.
* When an entry is not found, gopass show can start an interactive fuzzy search by default. This can be disabled globally with show.fuzzysearch=false or for one invocation via --nofuzzysearch.
* Since gopass plans to supports different RCS backends we do not support arbitrary git refs as arguments to the --revision flag. Using those might work, but this is explicitly not supported and bug reports will be closed as wont-fix. There are two issues with using arbitrary git refs is that (a) this doesn't work with non-git RCS backends and (b) git versions a whole repository, not single files. So the revision HEAD^
might not have any changes for a given entry. Thus we only support specifc revisions obtained from gopass history or our custom syntax -N where N is an integer identifying a specific commit before HEAD (cf. HEAD~N).
Exit codes
| Code | Meaning |
|-----:|---------|
| 0 | Secret displayed successfully |
| 10 | Secret not found |
| 11 | Secret could not be decrypted |
See docs/exit-codes.md for the full table.
Parsing and secrets
Secrets are stored on disk as provided, but are parsed upon display to provide extra features such as the ability
to show the value of a key using: gopass show entry key.
The secrets are split into 3 categories:
- the plain type, which is just a plain secret without key-value capabilities
this is a plain secret
using multiple lines and that's it
gets parsed to the same value
- the key-value type, which allows to query the value of a specific key. This does not preserve ordering.
this is a KV secret
where: the first line is the password
and: the keys are separated from their value by : and maybe we have a body text
below it
will be parsed into (with
safecontent enabled):and: the keys are separated from their value by :
where: the first line is the password
and maybe we have a body text
below it
- the YAML type which implements YAML support, which means that secrets are parsed as per YAML standard.
s3cret
---
invoice: 0123
date : 2001-01-23
bill-to: &id001
given : Bob
family : Doe
ship-to: *id001will be parsed into (with
safecontent enabled):bill-to: map[family:Doe given:Bob]
date: 2001-01-23 00:00:00 +0000 UTC
invoice: 83
ship-to: map[family:Doe given:Bob]Note how the
0123 is interpreted as octal for 83. If you want to store a string made of digits such as a numericalusername, it should be enclosed in string delimiters:
username: "0123" will always be parsed as the string 0123and not as octal.
By default, safecontent will remove the first line (the password), every line starting with otpauth:// in the body, and every YAML values where the key is one of the following: hotp, otpauth, password, totp.
Both the key-value and the YAML format support so-called "unsafe-keys", which is a key-value that allows you to specify keys that should be hidden when using gopass show with gopass config safecontent set to true.
E.g:
supersecret
---
age: 27
secret: The rabbit outran the tortoise
name: John Smith
unsafe-keys: age,secretwill display (with safecontent enabled):
age: *
name: John Smith
secret: *
unsafe-keys: age,secretunless it is called with
gopass show -n that would disable parsing of the body, but still hide the password, or gopass show -f that would show everything that was hidden, including the password.You can read more about secrets formats in its documentation.
Notice that if the option parsing is disabled in the config, then all secrets are handled as plain secrets.
---
Commands/Sync
sync command
The sync command is the preferred way to manually synchronize changes between
your local stores and any configured remotes.
You can also cd into a git-based store and manually perform git operations,
or use the gopass git command to automatically run a command in the correct
directory.
Note: gopass sync only supports one remote per store.
Flags
| Flag | Description |
|-----------|--------------------------------|
| --store | Only sync a specific sub store |
---
Commands/Templates
templates commands
The template support is one of the more unique gopass features. It allows
password stores to define templates that will automatically apply to any new
secret create at or below the template path. For example this can be useful
to generate a new email password and its salted hash at the same time. Or a
PostgreSQL password with the custom salted hash. This is certainly a feature
that's not used very often, but if used correctly it can greatly reduce the
toil of some common operations.
This uses Go's text/template package.
Synopsis
gopass templates
gopass templates show template
gopass templates edit template
gopass templates remove templateFlags
None.
Examples
Compute the salted hash for the password
Password: {{ .Content }}
SSHA256: {{ .Content | ssha256 }}Compute the SQL statements to create a new PostgreSQL user
{{ .Content }}
---
sql: |
CREATE ROLE {{ .Name }} LOGIN PASSWORD '{{ .Content }}';
GRANT {{ .Name }} TO {{ .Name }};
ALTER USER {{ .Name }} SET search_path = '{{ .Name }}';Template functions
Function | Example | Description
-------- | ------- | -----------md5sum | {{ .Content \| md5sum }} | Calculate the hex md5sum of the input.sha1sum | {{ .Content \| sha1sum }} | Calculate the hex sha1sum of the input.md5crypt | {{ .Content \| md5crypt }} | Calculate the md5crypt of the input.ssha | {{ .Content \| ssha }} | Calculate the salted SHA-1 of the input.ssha256 | {{ .Content \| ssha256 }} | Calculate the salted SHA-256 of the input.ssha512 | {{ .Content \| ssha512 }} | Calculate the salted SHA-512 of the input.get | {{ get "foo/bar" }} | Insert the full secret.getpw | {{ getpw "foo/bar" }} | Insert the value of the password field from the given secret.getval | {{ getval "foo/bar" "baz" }} | Insert the value of the named field from the given secret.argon2i | {{ .Content \| argon2i }} | Calculate the Argon2i hash of the input.argon2id | {{ .Content \| argon2id }} | Calculate the Argon2id hash of the input.bcrypt | {{ .Content \| bcrypt }} | Calculate the Bcrypt hash of the input.blake3 | {{ .Content \| blake3 }} | Calculate the BLAKE-3 hash of the input.
Template variables
Note: These examples assume being evaluated for the secret foo/bar/baz and
the generated password VerySecure.
Name | Example | Description
---- | ------- | -----------Dir | foo/bar | The directory containing the secret.DirName | bar | The directory name containing the secret.Path | foo/bar/baz | The path or full name of the secret.Name | baz | The last element of the path or short name of the secret.Content | VerySecure | The generated password.
---