Skip to content

feat(credstore): recognize the pass(1) backend - #26

Merged
rianjs merged 5 commits into
mainfrom
feat/24-expand-backend-set
May 27, 2026
Merged

rianjs merged 5 commits into
mainfrom
feat/24-expand-backend-set

Conversation

@rianjs

@rianjs rianjs commented May 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds BackendPass to the recognized backend set so consumers wired against --backend / <SVC>_KEYRING_BACKEND / keyring.backend can pick pass(1) as their keyring.
  • Extracts buildKeyringConfig from openOSBackend and sets cfg.PassPrefix = service for the pass backend — critical because ByteNess's pass backend ignores ServiceName and would otherwise let two CLIs collide in ~/.password-store on identical key names.
  • Docs sweep in working-with-secrets.md: §1.4 explicit-opt-in paragraph + §1.10 "what credstore exposes" rewrite. KeePassXC works today via Secret Service (no separate backend); 1Password is explicitly deferred because all three of ByteNess's op* openers depend on the still-pre-1.0 onepassword-sdk-go.

Why

  • The original ticket credstore: expand recognized backend set (pass, keepassxc, 1password, …) #24 called pass "the canonical headless-Linux answer" and noted it pairs with the existing loginctl enable-linger guidance. cli-common#23 deliberately kept parseBackend at the original five names to keep that diff tight; this is the follow-up that puts the new name into circulation.
  • Downstream CLIs already wire --backend <whatever> through to credstore.BindBackendFlag as an opaque string (atlassian-cli#386, google-readonly#140, newrelic-cli#105, slack-chat-api#170). After this PR releases as v0.2.1, they pick up --backend pass automatically the next time they bump cli-common — no per-CLI PR required.

Architectural notes

  • PassPrefix = service is the load-bearing line. The pass backend's item path is filepath.Join(PassDir, PassPrefix, key)+".gpg" (byteness/keyring/pass.go:129). Without PassPrefix every consumer would share a flat namespace and cfl + slck storing keys with identical names would collide silently. TestBuildKeyringConfig_PassSetsPrefixToService pins this with two distinct services.
  • buildKeyringConfig extraction. Moving the per-backend wiring into a pure helper makes the prefix assertion testable without opening a real keyring. The existing BackendFile branch moves into the helper unchanged.
  • Lockstep test. TestAllBackends_MatchesConstants is the drift guard: it forces a constant addition to land in allBackends AND in the expected-slice literal. Adding pass exercised it.
  • Exhaustive linter. Adding BackendPass triggered the exhaustive linter on the switch in buildKeyringConfig. Added a default-style case enumerating the no-extra-wiring backends rather than a bare default: so a future addition forces the author to think about whether the new backend needs construction extras.
  • No new dependencies. ByteNess already pulled in by v0.2.0; the pass opener is !windows and has no SDK imports.

Out of scope (explicitly deferred)

  • op / op-connect / op-desktop — all SDK-backed via github.com/1password/onepassword-sdk-go (pre-1.0). Gated on the SDK going GA; will file a follow-up.
  • kwallet, keyctl, passage — real but unrequested. Will file a single tracking issue for "expose remaining ByteNess backends on user ask".
  • Exposing PassDir / PassCmd config options — PASSWORD_STORE_DIR env var already covers the override case; adding Options fields without a user ask would be premature configurability.

Test plan

  • go test ./... -race — all packages pass
  • go vet ./... — clean
  • golangci-lint run — clean (after adding the exhaustive switch case)
  • TestBuildKeyringConfig_PassSetsPrefixToService covers the prefix scoping with two distinct services + the file-backend negative case
  • TestAllBackends_MatchesConstants exercises the lockstep drift guard
  • TestOpenOSBackend_PassOnWindows_FailsGracefully skipped on macOS; runs on Windows CI
  • Manual: pass --version, pass init <gpg-key>, then a downstream CLI built against this commit with --backend pass

Closes #24

rianjs added 2 commits May 27, 2026 06:59
Adds BackendPass to the recognized backend set so consumers wired
against the --backend flag/env/config knobs can pick pass(1) as their
keyring. pass shells out to the `pass` CLI binary with no Go SDK
dependency; the canonical headless-Linux answer noted in the original
ticket.

ByteNess's pass backend ignores Config.ServiceName and stores items at
filepath.Join(PassDir, PassPrefix, key)+".gpg", so without a per-service
PassPrefix every cli-common consumer would share a flat ~/.password-store
namespace and collide on identical key names ("api_token", etc.).
openOSBackend now sets PassPrefix = service for the pass backend,
mirroring the existing per-service carve-out for the file backend.

The keyring.Config construction is extracted into a small
buildKeyringConfig helper so the per-service-prefix wiring is unit
testable without opening a real backend.

Out of scope, deferred to follow-ups: op / op-connect / op-desktop are
all SDK-backed via github.com/1password/onepassword-sdk-go (still
pre-1.0); kwallet, keyctl, passage are real but unrequested. Documented
in §1.10.

Closes #24
The architect's PR review flagged that ByteNess's keyring.Open loops
through AllowedBackends and 'continue's past any opener that returns an
error — so the actionable 'The pass program is not available' message
from byteness/keyring/pass.go init() is swallowed, and the user sees
the generic 'Specified keyring backend not available' wrapped by our
own error string.

Adds preflightOSBackend with a single arm for BackendPass that does an
exec.LookPath before calling keyring.Open. The error names the install
hint (apt/brew) and the `pass init <gpg-key-id>` follow-up step so the
user has a path forward without reading source.

Also tightens the BackendMemory comment in the buildKeyringConfig
exhaustive switch: memory short-circuits in store.Open and never
reaches openOSBackend; the arm exists only to satisfy the exhaustive
linter rather than 'no extras needed because ByteNess uses ServiceName.'
@rianjs

rianjs commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

One meaningful gap: the preflight's actionable error message isn't pinned.

  • preflightOSBackend(BackendPass) is the load-bearing new behavior — its whole reason for existing (per the doc comment) is that ByteNess swallows the pass opener's error, so users would otherwise see a generic "Specified keyring backend not available". The PR ships a specific, install-hint-bearing message ("the pass(1) CLI is not on $PATH; install it (e.g. apt install pass / brew install pass) and run pass init <gpg-key-id> before selecting --backend pass") but no test asserts any substring of it. A future refactor could regress this to a bare err and CI would stay green.
  • Relatedly, no test exercises preflightOSBackend directly when pass is absent from $PATH, and nothing pins the openOSBackend wrapping format (credstore: opening %s backend for service %q: %w) or asserts errors.Is / errors.As-ability of the preflight failure. A PATH="" (or t.Setenv("PATH","")) -based test on non-Windows would close this cleanly.
  • Minor: TestOpenOSBackend_PassOnWindows_FailsGracefully is Skip-on-non-Windows, so on the Mac/Linux CI matrix it provides zero coverage — fine as a Windows-runner pin, just worth noting it doesn't substitute for the above.

Non-gaps (coverage is fine):

  • Constant + allBackends lockstep, parseBackend("pass"), and BackendFlagUsage() "pass" mention are all pinned.
  • buildKeyringConfig is well-covered for the load-bearing pass case (two services, distinct prefixes) and the file case (PassPrefix empty, FileDir set). The keychain/wincred/secret-service arms are intentionally no-ops, so skipping per-arm ServiceName assertions is reasonable.

TDD assessment, sub-agent run.

… contract

The TDD assessment on #26 caught a coverage gap: the actionable
preflight error message and the assertion that non-pass backends are
unaffected by the preflight were both unpinned. The existing
TestOpenOSBackend_PassOnWindows_FailsGracefully only runs on Windows,
so Mac/Linux CI had no preflight coverage at all.

Adds:
- TestPreflightOSBackend_PassNotOnPath: forces an empty PATH for the
  test (t.Setenv restores it) and asserts the error mentions "pass",
  "PATH", "install", and "pass init" so a future edit that drops the
  actionable text fails the suite.
- TestPreflightOSBackend_NonPassBackendsSkipPreflight: iterates every
  non-pass backend in the recognized set and asserts the preflight is a
  no-op even with an empty PATH, so a future change cannot accidentally
  gate an OS keyring on a CLI binary it does not need.

@monit-reviewer monit-reviewer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR Review

Reviewed commit: 62027f9

Summary

Reviewer Findings
documentation:docs-reviewer 1
harness-engineering:harness-architecture-reviewer 1
harness-engineering:harness-enforcement-reviewer 1
harness-engineering:harness-knowledge-reviewer 3
documentation:docs-reviewer (1 findings)

💡 Suggestion - docs/working-with-secrets.md:161

Component attribution is incorrect: the phrase 'cli-common scopes each consumer's entries' attributes the scoping action to the wrong component. Since credstore sets PassPrefix, the sentence should reference credstore specifically, e.g. 'credstore scopes each consumer's entries under a per-service subtree of the password-store (by setting PassPrefix = <service>)'.

harness-engineering:harness-architecture-reviewer (1 findings)

💡 Suggestion - credstore/osbackend_test.go:401

TestOpenOSBackend_PassOnWindows_FailsGracefully has an inaccurate comment claiming it pins the ByteNess 'not-registered' path (pass.go is !windows). In practice, preflightOSBackend's exec.LookPath check fires first on any system where pass is not on PATH — which includes virtually every Windows system. The test assertions remain correct (error contains 'pass'), but the comment misrepresents which code path is being tested. Update to reflect that the error originates from the preflight LookPath check, and note that the ByteNess registration path is not independently exercised by this test.

harness-engineering:harness-enforcement-reviewer (1 findings)

💡 Suggestion - credstore/osbackend.go:60

preflightOSBackend's BackendPass branch performs exec.LookPath on all platforms without a runtime.GOOS == "windows" short-circuit. On Windows systems with a WSL bridge script or Git-for-Windows shim that places 'pass' on PATH, the preflight succeeds but keyring.Open subsequently returns a generic ByteNess 'backend not available' error rather than a clear 'pass is unsupported on Windows' message. TestOpenOSBackend_PassOnWindows_FailsGracefully only exercises the no-pass-in-PATH path, leaving this edge case uncovered.

harness-engineering:harness-knowledge-reviewer (3 findings)

⚠️ Should Fix - credstore/osbackend.go:54

The preflight error message for BackendPass hardcodes apt/brew install hints ('install it (e.g. apt install pass / brew install pass)') without guarding on runtime.GOOS. On Windows — where the byteness/keyring pass backend is excluded via !windows build tags — a user hitting this path sees Linux/macOS-specific install instructions that are not actionable. Consider adding an explicit runtime.GOOS == "windows" guard that either returns 'pass backend is not supported on Windows' or omits the platform-specific hints.

💡 Suggestion - credstore/select_test.go:102

There is a test asserting BackendMemory is never auto-selected (TestSelectBackendMemoryNeverAuto) but no equivalent test asserting BackendPass is never auto-selected. BackendPass is now in allBackends, and on systems where pass is installed, a future selectBackend refactor could silently enable pass auto-selection. A TestSelectBackendPassNeverAuto test mirroring the Memory pattern would pin the 'pass is opt-in only' contract.

💡 Suggestion - credstore/osbackend.go:52

The preflightOSBackend switch is not covered by the exhaustive linter in the same way as buildKeyringConfig's switch. A developer adding a new Backend constant may add it to the combined no-op case without considering whether it needs a preflight check, and no linter would flag the omission. Consider adding a doc comment explicitly noting that new backends must be evaluated for preflight requirements, or splitting the combined case to mirror the pattern used in buildKeyringConfig.

5 info-level observations excluded. Run with --verbose to include.

1 PR discussion thread considered.


Completed in 6m 01s | $1.17 | sonnet | daemon 0.2.120 | Glorfindel
Field Value
Model sonnet
Reviewers hybrid-synthesis, documentation:docs-reviewer, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer, harness-engineering:harness-self-documenting-code-reviewer, security:security-code-auditor
Engine claude · sonnet
Reviewed by pr-review-daemon · monit-pr-reviewer
Duration 6m 01s wall · 15m 48s compute (Reviewers: 3m 45s · Synthesis: 2m 14s)
Cost $1.17
Tokens 161.4k in / 63.7k out
Turns 11

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost
hybrid-synthesis sonnet 19.3k 9.1k 7.5k 11.8k (1h) $0.19
documentation:docs-reviewer haiku 5.0k 9.4k 0 5.0k (1h) $0.06
harness-engineering:harness-architecture-reviewer sonnet 30.6k 11.1k 15.1k 15.5k (1h) $0.24
harness-engineering:harness-enforcement-reviewer sonnet 30.3k 7.0k 15.1k 15.2k (1h) $0.17
harness-engineering:harness-knowledge-reviewer sonnet 31.1k 13.1k 15.1k 16.0k (1h) $0.27
harness-engineering:harness-self-documenting-code-reviewer sonnet 28.7k 8.2k 13.8k 14.8k (1h) $0.19
security:security-code-auditor haiku 16.5k 5.7k 0 16.5k (1h) $0.06

Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.

Comment thread docs/working-with-secrets.md Outdated
- **Secret Service is present but locked, returns an auth failure, or otherwise rejects the request.** **Fail closed** with an actionable error that names the backend, the operation that failed, and how to unlock or grant access (`gnome-keyring-daemon`, `seahorse`, `kwalletmanager`, etc.). Do *not* silently fall back to the file backend. A user with a working desktop keyring that happens to be locked must not have their secrets silently downgraded to an encrypted file in a different location — that is a stealth security regression and a likely source of "where did my credentials go?" support tickets.
- **Ambiguous failure → fail closed.** If the wrapper cannot confidently distinguish "unavailable" from "denied/locked" — D-Bus answered but returned an opaque error, the backend timed out, the response shape was unexpected — treat the case as denied/locked and fail closed. The fallback path is opt-in only when we are sure no working keyring is present.
- **Explicit user opt-in to the file backend** is supported via a config flag (working name `keyring.backend: file` in `config.yml`) or an env var (`<SERVICE>_KEYRING_BACKEND=file`). With that set, the CLI uses the file backend unconditionally and never attempts Secret Service. This is the supported path for users who genuinely prefer the file backend. The backend-selector env var is non-secret runtime configuration (it controls *where* the CLI looks, not *what* it finds); it is not the runtime-env-var exception described below.
- **Explicit user opt-in to the `pass` backend** is supported via the same knobs (`--backend pass`, `<SERVICE>_KEYRING_BACKEND=pass`, or `keyring.backend: pass`). `pass` is the canonical headless-Linux answer for users who already run a `pass` password-store; it requires the `pass` CLI binary on `$PATH` and an initialized password-store (`pass init <gpg-key-id>`). cli-common scopes each consumer's entries under a per-service subtree of the password-store (the credstore sets `PassPrefix = <service>`), so two CLIs storing the same key name do not collide. KeePassXC users do not need a separate backend — KeePassXC integrates with Secret Service over D-Bus, so `--backend secret-service` (the default) already covers it. The `pass` backend is `!windows`: requesting it on Windows fails closed with a "no such backend" error rather than silently falling back.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (documentation:docs-reviewer): Component attribution is incorrect: the phrase 'cli-common scopes each consumer's entries' attributes the scoping action to the wrong component. Since credstore sets PassPrefix, the sentence should reference credstore specifically, e.g. 'credstore scopes each consumer's entries under a per-service subtree of the password-store (by setting PassPrefix = <service>)'.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — §1.4 now says 'credstore scopes each consumer's entries... (by setting PassPrefix = <service>)'.

Comment thread credstore/osbackend_test.go Outdated
// TestOpenOSBackend_PassOnWindows_FailsGracefully pins the Windows
// behavior: byteness/keyring's pass.go is `//go:build !windows`, so a
// user reading our docs and trying `--backend pass` on Windows will hit
// the not-registered path inside ByteNess. We want a sensible error

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-architecture-reviewer): TestOpenOSBackend_PassOnWindows_FailsGracefully has an inaccurate comment claiming it pins the ByteNess 'not-registered' path (pass.go is !windows). In practice, preflightOSBackend's exec.LookPath check fires first on any system where pass is not on PATH — which includes virtually every Windows system. The test assertions remain correct (error contains 'pass'), but the comment misrepresents which code path is being tested. Update to reflect that the error originates from the preflight LookPath check, and note that the ByteNess registration path is not independently exercised by this test.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — comment now reflects that the preflight's runtime.GOOS guard fires first (the ByteNess !windows registration is not independently exercised by this test).

Comment thread credstore/osbackend.go
// user sees a generic "Specified keyring backend not available". Catch
// the common cases here so users get actionable messages.
func preflightOSBackend(kind Backend) error {
switch kind {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium (harness-engineering:harness-knowledge-reviewer): The preflight error message for BackendPass hardcodes apt/brew install hints ('install it (e.g. apt install pass / brew install pass)') without guarding on runtime.GOOS. On Windows — where the byteness/keyring pass backend is excluded via !windows build tags — a user hitting this path sees Linux/macOS-specific install instructions that are not actionable. Consider adding an explicit runtime.GOOS == "windows" guard that either returns 'pass backend is not supported on Windows' or omits the platform-specific hints.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added a runtime.GOOS == "windows" short-circuit ahead of the LookPath. Windows users now get a clear 'not supported on Windows' message with no Linux/macOS install hints. Mooted both this and the Windows-shimmed-pass concern below.

Comment thread credstore/osbackend.go
return fmt.Errorf("the pass(1) CLI is not on $PATH; install it (e.g. `apt install pass` / `brew install pass`) and run `pass init <gpg-key-id>` before selecting --backend pass")
}
case BackendKeychain, BackendWinCred, BackendSecretService, BackendFile, BackendMemory:
// No preflight check — ByteNess's openers (or store.Open's

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-enforcement-reviewer): preflightOSBackend's BackendPass branch performs exec.LookPath on all platforms without a runtime.GOOS == "windows" short-circuit. On Windows systems with a WSL bridge script or Git-for-Windows shim that places 'pass' on PATH, the preflight succeeds but keyring.Open subsequently returns a generic ByteNess 'backend not available' error rather than a clear 'pass is unsupported on Windows' message. TestOpenOSBackend_PassOnWindows_FailsGracefully only exercises the no-pass-in-PATH path, leaving this edge case uncovered.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by the same runtime.GOOS guard in the sibling thread — preflight now fails closed on Windows before LookPath, so a shimmed pass on PATH cannot pass preflight only to die inside ByteNess.

Comment thread credstore/select_test.go
@@ -102,3 +102,18 @@ func TestSelectBackendMemoryNeverAuto(t *testing.T) {
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-knowledge-reviewer): There is a test asserting BackendMemory is never auto-selected (TestSelectBackendMemoryNeverAuto) but no equivalent test asserting BackendPass is never auto-selected. BackendPass is now in allBackends, and on systems where pass is installed, a future selectBackend refactor could silently enable pass auto-selection. A TestSelectBackendPassNeverAuto test mirroring the Memory pattern would pin the 'pass is opt-in only' contract.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added TestSelectBackendPassNeverAuto mirroring the BackendMemory pattern. Asserts explicit pass selection works (SourceExplicit) and that auto selection on darwin/windows/linux never returns pass.

Comment thread credstore/osbackend.go
// and `continue`s past any opener that returns an error, so a useful
// "pass program is not available" from the pass opener is lost and the
// user sees a generic "Specified keyring backend not available". Catch
// the common cases here so users get actionable messages.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-knowledge-reviewer): The preflightOSBackend switch is not covered by the exhaustive linter in the same way as buildKeyringConfig's switch. A developer adding a new Backend constant may add it to the combined no-op case without considering whether it needs a preflight check, and no linter would flag the omission. Consider adding a doc comment explicitly noting that new backends must be evaluated for preflight requirements, or splitting the combined case to mirror the pattern used in buildKeyringConfig.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a doc comment to preflightOSBackend explicitly directing future authors of new Backend constants to evaluate whether a preflight is warranted. The exhaustive linter does cover this switch (an unhandled constant fails the build), but the doc hint helps an author think about preflight semantics rather than just adding their new case to the no-op arm.

…nt fixes

Daemon review on #26 caught six items:

- (Medium) preflight error hardcoded apt/brew hints regardless of OS. A
  Windows user with a `pass` shim on PATH (Git-for-Windows, WSL bridge)
  would otherwise pass preflight and then hit the generic ByteNess
  "backend not available" from keyring.Open. Added a runtime.GOOS ==
  "windows" guard ahead of the LookPath that returns a clear
  "not supported on Windows" message with no Linux/macOS install hints.

- (Low) TestSelectBackendPassNeverAuto mirrors the BackendMemory contract
  so a future selectBackend refactor cannot silently start auto-picking
  pass on systems that happen to have it installed.

- (Low) Doc attribution: "cli-common scopes" → "credstore scopes" in
  §1.4. The PassPrefix wiring lives in credstore/osbackend.go, not the
  umbrella module.

- (Low) Windows test comment claimed it pinned the ByteNess
  not-registered path; in practice the preflight's runtime.GOOS guard
  fires first. Comment now reflects that.

- (Low) Doc comment on preflightOSBackend telling future authors of new
  Backend constants to evaluate whether a preflight is warranted.

@monit-reviewer monit-reviewer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR Review

Reviewed commit: a9acb4a | Previous: 62027f9 (incremental)

Summary

Reviewer Findings
harness-engineering:harness-architecture-reviewer 2
harness-engineering:harness-architecture-reviewer (2 findings)

⚠️ Should Fix - credstore/osbackend_test.go:406

TestPreflightOSBackend_PassNotOnPath is missing a Windows skip guard and will fail on Windows CI. On Windows, preflightOSBackend(BackendPass) now returns the 'not supported on Windows' message before reaching the exec.LookPath branch, so the assertions for 'PATH', 'install', and 'pass init' — substrings that only appear in the POSIX message — will all fail. Add if runtime.GOOS == "windows" { t.Skip("PATH-empty preflight test is POSIX-only; see TestOpenOSBackend_PassOnWindows_FailsGracefully") } at the top, mirroring the pattern already used in TestOpenOSBackend_PassOnWindows_FailsGracefully.

💡 Suggestion - credstore/osbackend.go:104

cfg.PassPrefix is set to the raw service string with no validation. The pass backend stores items at filepath.Join(PassDir, PassPrefix, key)+".gpg", so a service name containing path separators (e.g. "../sibling") would silently escape the intended password-store subtree. Service names are supplied by downstream library callers rather than end-users, so the practical risk is low, but a short guard (e.g. rejecting names containing "/" or "\") would prevent a surprising filesystem layout if a downstream CLI ever passes a directory-style service name.

4 info-level observations excluded. Run with --verbose to include.

7 PR discussion threads considered.


Completed in 5m 51s | $1.33 | sonnet | daemon 0.2.120 | Glorfindel
Field Value
Model sonnet
Mode Re-review · Cycle 2 · Session resumed
Reviewers hybrid-synthesis, documentation:docs-reviewer, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer, harness-engineering:harness-self-documenting-code-reviewer, security:security-code-auditor
Engine claude · sonnet
Reviewed by pr-review-daemon · monit-pr-reviewer
Duration 5m 51s wall · 15m 34s compute (Reviewers: 4m 05s · Synthesis: 1m 28s)
Cost $1.33
Tokens 204.0k in / 63.2k out
Turns 11

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost
hybrid-synthesis sonnet 32.6k 5.0k 7.5k 25.2k (1h) $0.17
documentation:docs-reviewer haiku 5.0k 6.2k 0 5.0k (1h) $0.04
harness-engineering:harness-architecture-reviewer sonnet 33.3k 9.9k 15.7k 17.6k (1h) $0.23
harness-engineering:harness-enforcement-reviewer sonnet 35.7k 12.3k 15.7k 19.9k (1h) $0.27
harness-engineering:harness-knowledge-reviewer sonnet 32.7k 9.0k 15.7k 17.0k (1h) $0.21
harness-engineering:harness-self-documenting-code-reviewer sonnet 33.9k 15.6k 14.5k 19.5k (1h) $0.32
security:security-code-auditor haiku 17.2k 4.5k 0 17.1k (1h) $0.05
discussion-summarizer — 13.6k 736 7.5k 6.1k (1h) $0.04

Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.

// not from ByteNess's !windows opener registration — the preflight
// fires first so the platform constraint is the user-visible reason.
// Skipped on non-Windows.
func TestOpenOSBackend_PassOnWindows_FailsGracefully(t *testing.T) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium (harness-engineering:harness-architecture-reviewer): TestPreflightOSBackend_PassNotOnPath is missing a Windows skip guard and will fail on Windows CI. On Windows, preflightOSBackend(BackendPass) now returns the 'not supported on Windows' message before reaching the exec.LookPath branch, so the assertions for 'PATH', 'install', and 'pass init' — substrings that only appear in the POSIX message — will all fail. Add if runtime.GOOS == "windows" { t.Skip("PATH-empty preflight test is POSIX-only; see TestOpenOSBackend_PassOnWindows_FailsGracefully") } at the top, mirroring the pattern already used in TestOpenOSBackend_PassOnWindows_FailsGracefully.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — added runtime.GOOS == "windows" skip at the top, mirroring TestOpenOSBackend_PassOnWindows_FailsGracefully. The Windows branch is independently covered by that test.

Comment thread credstore/osbackend.go
cfg.FilePasswordFunc = pwFunc
case BackendPass:
// ByteNess's pass backend ignores ServiceName — items are
// stored at filepath.Join(PassDir, PassPrefix, key) + ".gpg"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-architecture-reviewer): cfg.PassPrefix is set to the raw service string with no validation. The pass backend stores items at filepath.Join(PassDir, PassPrefix, key)+".gpg", so a service name containing path separators (e.g. "../sibling") would silently escape the intended password-store subtree. Service names are supplied by downstream library callers rather than end-users, so the practical risk is low, but a short guard (e.g. rejecting names containing "/" or "\") would prevent a surprising filesystem layout if a downstream CLI ever passes a directory-style service name.

Reply to this thread when addressed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Service names are already validated upstream by validSegment in credstore/ref.go, which restricts segments to [A-Za-z0-9_-] (no ., no /, no \\). store.Open enforces it on every entry path, so a "../sibling" service would be rejected long before reaching buildKeyringConfig. No additional guard needed.

Daemon review on #26 (run 2) caught that TestPreflightOSBackend_PassNotOnPath
would fail on Windows CI: the Windows guard added in the previous commit
returns the 'not supported on Windows' message before reaching LookPath,
so the POSIX-specific substring assertions ('PATH', 'install',
'pass init') no longer match. Adds runtime.GOOS skip mirroring the
existing TestOpenOSBackend_PassOnWindows_FailsGracefully pattern.

@monit-reviewer monit-reviewer left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR Review

Reviewed commit: ca022c2 | Previous: a9acb4a (incremental)

Approved with 4 non-blocking suggestions below. Address at your discretion.

Summary

Reviewer Findings
documentation:docs-reviewer 1
harness-engineering:harness-architecture-reviewer 2
harness-engineering:harness-enforcement-reviewer 1
documentation:docs-reviewer (1 findings)

💡 Suggestion - docs/working-with-secrets.md:160

The new pass backend paragraph lists three configuration mechanisms (--backend pass, <SERVICE>_KEYRING_BACKEND=pass, keyring.backend: pass), but the immediately preceding file backend paragraph documents only two (env var and config file) with no mention of --backend file. This PR-introduced asymmetry will confuse readers comparing the two sections. Either update the file backend paragraph to document --backend file, or add a brief note explaining why the CLI flag applies to both backends equally.

harness-engineering:harness-architecture-reviewer (2 findings)

💡 Suggestion - credstore/osbackend_test.go:386

TestPreflightOSBackend_PassNotOnPath covers the 'pass absent from PATH' failure path, and TestOpenOSBackend_PassOnWindows_FailsGracefully covers the Windows guard, but there is no test for the success path: preflightOSBackend(BackendPass) returning nil when pass IS on PATH on a non-Windows host. If the LookPath error-check sense were accidentally inverted, or the early-return logic rearranged, the positive path would silently break and only manual testing would catch it.

💡 Suggestion - credstore/select_test.go:127

TestSelectBackendPassNeverAuto uses continue when selectBackend returns an error, silently skipping the 'pass was not auto-selected' assertion for that platform. The comment frames this as expected for 'unknown' GOOS values, but all three enumerated platforms (darwin, windows, linux) are known. A future selectBackend change that returns an error for linux before evaluating the backend would make the loop a no-op for that platform without failing the test. Assert b != BackendPass in the error branch as well, or log a t.Logf noting the skip so coverage gaps are visible.

harness-engineering:harness-enforcement-reviewer (1 findings)

💡 Suggestion - credstore/osbackend.go:48

preflightOSBackend's switch has a BackendMemory arm that silently does nothing. If store.Open's short-circuit for BackendMemory is ever removed or bypassed, the preflight passes, buildKeyringConfig's BackendMemory arm also does nothing, and execution reaches keyring.Open with AllowedBackends={memory} — which is unlikely to work at the OS keyring layer. The equivalent risk in buildKeyringConfig carries a comment, but preflightOSBackend has no corresponding note explaining why silently passing is correct here.

4 info-level observations excluded. Run with --verbose to include.

9 PR discussion threads considered.


Completed in 7m 33s | $1.17 | sonnet | daemon 0.2.120 | Glorfindel
Field Value
Model sonnet
Mode Re-review · Cycle 3 · Session resumed
Reviewers hybrid-synthesis, documentation:docs-reviewer, harness-engineering:harness-architecture-reviewer, harness-engineering:harness-enforcement-reviewer, harness-engineering:harness-knowledge-reviewer, harness-engineering:harness-self-documenting-code-reviewer, security:security-code-auditor
Engine claude · sonnet
Reviewed by pr-review-daemon · monit-pr-reviewer
Duration 7m 33s wall · 14m 25s compute (Reviewers: 4m 36s · Synthesis: 2m 33s)
Cost $1.17
Tokens 186.6k in / 55.1k out
Turns 10

Per-workstream usage

Workstream Model In Out Cache read Cache create Cost
hybrid-synthesis sonnet 43.4k 8.1k 7.5k 35.9k (1h) $0.26
documentation:docs-reviewer haiku 5.0k 9.4k 5.0k 0 $0.05
harness-engineering:harness-architecture-reviewer sonnet 31.2k 7.9k 15.8k 15.4k (1h) $0.19
harness-engineering:harness-enforcement-reviewer sonnet 27.8k 2.2k 15.8k 12.0k (1h) $0.09
harness-engineering:harness-knowledge-reviewer sonnet 13.6k 7.1k 2.2k 11.4k (1h) $0.16
harness-engineering:harness-self-documenting-code-reviewer sonnet 34.2k 16.8k 14.5k 19.7k (1h) $0.34
security:security-code-auditor haiku 17.2k 2.7k 0 17.2k (1h) $0.04
discussion-summarizer — 14.1k 951 7.5k 6.7k (1h) $0.04

Re-reviews only run when @monit-reviewer is re-requested as a reviewer — push as many commits as you need, then re-request when ready. PRs targeting branches other than main, master are skipped, even when @monit-reviewer is re-requested.

@@ -158,6 +158,7 @@ The shared credstore package selects backends in this fixed order:
- **Secret Service is present but locked, returns an auth failure, or otherwise rejects the request.** **Fail closed** with an actionable error that names the backend, the operation that failed, and how to unlock or grant access (`gnome-keyring-daemon`, `seahorse`, `kwalletmanager`, etc.). Do *not* silently fall back to the file backend. A user with a working desktop keyring that happens to be locked must not have their secrets silently downgraded to an encrypted file in a different location — that is a stealth security regression and a likely source of "where did my credentials go?" support tickets.
- **Ambiguous failure → fail closed.** If the wrapper cannot confidently distinguish "unavailable" from "denied/locked" — D-Bus answered but returned an opaque error, the backend timed out, the response shape was unexpected — treat the case as denied/locked and fail closed. The fallback path is opt-in only when we are sure no working keyring is present.
- **Explicit user opt-in to the file backend** is supported via a config flag (working name `keyring.backend: file` in `config.yml`) or an env var (`<SERVICE>_KEYRING_BACKEND=file`). With that set, the CLI uses the file backend unconditionally and never attempts Secret Service. This is the supported path for users who genuinely prefer the file backend. The backend-selector env var is non-secret runtime configuration (it controls *where* the CLI looks, not *what* it finds); it is not the runtime-env-var exception described below.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (documentation:docs-reviewer): The new pass backend paragraph lists three configuration mechanisms (--backend pass, <SERVICE>_KEYRING_BACKEND=pass, keyring.backend: pass), but the immediately preceding file backend paragraph documents only two (env var and config file) with no mention of --backend file. This PR-introduced asymmetry will confuse readers comparing the two sections. Either update the file backend paragraph to document --backend file, or add a brief note explaining why the CLI flag applies to both backends equally.

Reply to this thread when addressed.

return ""
}
cfg, err := buildKeyringConfig(BackendFile, "atlassian-cli", &Options{}, getenv)
if err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-architecture-reviewer): TestPreflightOSBackend_PassNotOnPath covers the 'pass absent from PATH' failure path, and TestOpenOSBackend_PassOnWindows_FailsGracefully covers the Windows guard, but there is no test for the success path: preflightOSBackend(BackendPass) returning nil when pass IS on PATH on a non-Windows host. If the LookPath error-check sense were accidentally inverted, or the early-return logic rearranged, the positive path would silently break and only manual testing would catch it.

Reply to this thread when addressed.

Comment thread credstore/select_test.go
// on systems that happen to have the binary installed would be a stealth
// security regression (silent backend change), so pin the contract.
func TestSelectBackendPassNeverAuto(t *testing.T) {
if b, src, err := selectBackend("svc", &Options{Backend: BackendPass}, envFrom(nil), "linux"); err != nil || b != BackendPass || src != SourceExplicit {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-architecture-reviewer): TestSelectBackendPassNeverAuto uses continue when selectBackend returns an error, silently skipping the 'pass was not auto-selected' assertion for that platform. The comment frames this as expected for 'unknown' GOOS values, but all three enumerated platforms (darwin, windows, linux) are known. A future selectBackend change that returns an error for linux before evaluating the backend would make the loop a no-op for that platform without failing the test. Assert b != BackendPass in the error branch as well, or log a t.Logf noting the skip so coverage gaps are visible.

Reply to this thread when addressed.

Comment thread credstore/osbackend.go
}

// preflightOSBackend runs cheap, actionable pre-construction checks for
// backends whose ByteNess opener returns a useful error message that

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Low (harness-engineering:harness-enforcement-reviewer): preflightOSBackend's switch has a BackendMemory arm that silently does nothing. If store.Open's short-circuit for BackendMemory is ever removed or bypassed, the preflight passes, buildKeyringConfig's BackendMemory arm also does nothing, and execution reaches keyring.Open with AllowedBackends={memory} — which is unlikely to work at the OS keyring layer. The equivalent risk in buildKeyringConfig carries a comment, but preflightOSBackend has no corresponding note explaining why silently passing is correct here.

Reply to this thread when addressed.

@rianjs

rianjs commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

No architectural drift found in the post-daemon diff. The new preflightOSBackend guard preserves the intended contract: pass remains opt-in, gets service-scoped via PassPrefix, fails clearly on Windows, and gives an actionable missing-pass install/init hint without polluting the pure buildKeyringConfig tests.

The added never-auto test and corrected BackendMemory switch comment are aligned with the converged plan. gh pr diff is still blocked from this sandbox, so I reviewed the current PR diff via the GitHub connector.

@rianjs
rianjs merged commit b8babda into main May 27, 2026
4 checks passed
@rianjs
rianjs deleted the feat/24-expand-backend-set branch May 27, 2026 11:35
rianjs added a commit to open-cli-collective/atlassian-cli that referenced this pull request May 27, 2026
Picks up open-cli-collective/cli-common#26: the pass(1) backend is now
recognized by credstore. No code changes required in any downstream
module — the existing --backend flag wiring passes the new name through
to credstore.BindBackendFlag as an opaque string.

After this lands, users can run e.g.:

  jtk issues list --backend pass
  cfl pages list --backend pass

(both require the pass(1) CLI on $PATH and a `pass init`-initialized
password-store).
rianjs added a commit to open-cli-collective/newrelic-cli that referenced this pull request May 27, 2026
Picks up open-cli-collective/cli-common#26: the `pass(1)` backend is now
in credstore's recognized backend set. No code changes — the existing
`--backend` wiring passes `pass` through to credstore as an opaque
string.

After merge: `nrq --backend pass <cmd>` works for users with the
`pass(1)` CLI on `$PATH` and an initialized password-store.
rianjs added a commit to open-cli-collective/slack-chat-api that referenced this pull request May 27, 2026
Picks up open-cli-collective/cli-common#26: the `pass(1)` backend is now
in credstore's recognized backend set. No code changes — the existing
`--backend` wiring passes `pass` through to credstore as an opaque
string.

After merge: `slck --backend pass <cmd>` works for users with the
`pass(1)` CLI on `$PATH` and an initialized password-store.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

credstore: expand recognized backend set (pass, keepassxc, 1password, …)

2 participants