Conversation
Lets `pass run` resolve `se://` references directly from the local OS keychain (the same store used by `pass set`/`pass get`) instead of going through the secrets-engine daemon. Useful when Docker Desktop is not running or when symmetric round-tripping is desired. Implemented as a `secrets.Resolver` adapter over `store.Store.Filter`, so `resolveEnv`/`resolveRef` semantics (pattern parsing, single-match enforcement, ErrNotFound) stay identical between daemon and keychain modes. Also moves the `run` long description into an embedded markdown file (`run_long.md`), matching the existing pattern for examples.
There was a problem hiding this comment.
Assessment: 🟡 NEEDS ATTENTION
⚠️ Note: Automated verification was inconclusive (verifier returned no verdicts). Findings below are unverified hypotheses from static analysis — please evaluate them as the PR author.
Summary: The --os-keychain flag and storeResolver adapter look sound for the normal production path (where Root() always supplies a real store via NewPassPlugin). Two potential issues are flagged for author review.
| return err | ||
| var r secrets.Resolver | ||
| if opts.osKeychain { | ||
| r = &storeResolver{s: s} |
There was a problem hiding this comment.
[HIGH] Missing nil guard: --os-keychain with nil store panics at runtime
RunCommand now accepts a store.Store parameter. When --os-keychain is set, &storeResolver{s: s} is constructed and r.s.Filter(ctx, p) is immediately called (line 155). If s is nil, this is a nil-interface dereference → runtime panic.
In the normal production path (Root() → RunCommand(s)), s is always a real store from NewPassPlugin, so this is not reachable there. However, the tests already call RunCommand(nil) — if any future test adds --os-keychain it will panic rather than fail gracefully. A defensive early return would harden this:
if opts.osKeychain && s == nil {
return fmt.Errorf("--os-keychain requires a configured store (s is nil)")
}Consider adding this guard at line 79, before constructing storeResolver.
| } | ||
|
|
||
| func (r *storeResolver) GetSecrets(ctx context.Context, p secrets.Pattern) ([]secrets.Envelope, error) { | ||
| m, err := r.s.Filter(ctx, p) |
There was a problem hiding this comment.
[MEDIUM] store.Filter may signal 'not found' inconsistently between error vs empty map
In storeResolver.GetSecrets, if store.Filter returns store.ErrCredentialNotFound (= secrets.ErrNotFound) as an error, it is propagated directly. But if Filter returns an empty map with nil error, the returned slice is empty and the caller (resolveRef) detects ErrNotFound from the empty result.
Both paths ultimately trigger ErrNotFound in the caller (via errors.Is wrapping), so correctness is preserved. However, the error message path differs: the error-return path will say "resolving $KEY: secret not found" while the empty-slice path generates a separate ErrNotFound. If store.Filter implementations diverge on this contract, the UX of --os-keychain will be inconsistent.
Consider normalising in GetSecrets:
if errors.Is(err, store.ErrCredentialNotFound) {
return nil, nil // let the caller's empty-slice ErrNotFound path handle it
}
Lets
pass runresolvese://references directly from the local OS keychain (the same store used bypass set/pass get) instead of going through the secrets-engine daemon. Useful when Docker Desktop is not running or when symmetric round-tripping is desired.Implemented as a
secrets.Resolveradapter overstore.Store.Filter, soresolveEnv/resolveRefsemantics (pattern parsing, single-match enforcement, ErrNotFound) stay identical between daemon and keychain modes.Also moves the
runlong description into an embedded markdown file (run_long.md), matching the existing pattern for examples.