Skip to content

Warn about the keyring fallback on the first credential read, not only on login - #664

Merged
jeremy merged 3 commits into
mainfrom
keyring-fallback-warn-on-read
Aug 28, 2026
Merged

Warn about the keyring fallback on the first credential read, not only on login#664
jeremy merged 3 commits into
mainfrom
keyring-fallback-warn-on-read

Conversation

@jeremy

@jeremy jeremy commented Aug 28, 2026

Copy link
Copy Markdown
Member

The failure

The credential store printed its warning: system keyring unavailable, credentials stored in plaintext at … only on Save. Every other command merely reads, so a process whose keyring probe failed served whatever an earlier fallback had left in credentials.json — on one machine today, tokens that expired months ago and profiles that no longer existed — with no word about why. Under the concurrent-probe race fixed in basecamp/cli#70, 19 of 20 parallel basecamp auth status -j returned that stale file silently, and basecamp me --profile clawdito said "Not authenticated" 19/20.

The change

Store.Load runs the same once-per-process warnFallback as Save, so the first read after a fallen-back probe says so on stderr. The inner store becomes a small credStore interface so the test can stand in a fallen-back store without failing a real keyring probe; swapNewCredStore's signature follows.

Manager.SetUserEmail — the one credential read a BASECAMP_TOKEN session could reach (basecamp people me stores the fetched email) — now returns without touching the store when the env token is set, matching AccessToken() / AccountID() precedence. The email names the env token's user, not whoever the stored credentials belong to, so writing it there mislabeled them; and the load ran the keyring probe, which with this PR would have warned a CI host with a locked keychain about plaintext credentials it neither stored nor read. TestSetUserEmailSkipsStoreOnEnvToken fails with the guard removed.

TestLoadWarnsOnceWhenKeyringFellBack asserts the warning appears on the first of two reads and that a later write stays quiet. Checked that it fails with only the Load call removed (an earlier draft passed for the wrong reason — the Save in the same capture window warned — which is why the test is split into a read phase and a write phase).

Verified end to end against basecamp/cli#70 with a temporary local replace (not committed): a bounded probe that fails yields

Load error: credentials not found for profile:clawdito: system keyring unavailable (keyring probe timed out after 1ns: context deadline exceeded), fell back to …/credentials.json
stderr:     warning: system keyring unavailable (keyring probe timed out after 1ns: context deadline exceeded), credentials stored in plaintext at …/credentials.json

This PR works against the currently pinned credstore too — the warning just lacks the reason until the bump.

Noise

Hosts with no keyring at all (Linux without a secret service) will now see the warning on every invocation, not only at login. That is the honest state — their probe fails every run — and hosts that mean to use file storage set BASECAMP_NO_KEYRING=1, which skips the probe and never warns. CI and agents on BASECAMP_TOKEN never read or write the store: AccessToken, IsAuthenticated, AccountID, AuthorizationEndpoint, auth status, and doctor short-circuit on the env token, and SetUserEmail now does too, so the warning cannot fire there.

Declined

  • Print from credstore. A library shouldn't write to stderr; credstore's FallbackWarning() carries the reason and its docstring asks callers to surface it on reads.
  • Warn on Delete / MigrateToKeyring too. Both are preceded by a read or a login in every path that reaches them.
  • Give SetUserIdentity the same BASECAMP_TOKEN guard. Its callers are the login flows (auth login, profile add, the setup wizard), which have just saved fresh credentials and fetch the profile to label them; skipping the write there would leave a login done under an exported token unlabeled. Whether that fetch should use the fresh token rather than the env one is a separate, pre-existing question.
  • Guard GetUserEmail / GetOAuthType too. Neither is reached from a token session — AuthorizationEndpoint consults GetOAuthType only after its env-token branch — and both are display-only reads whose stored answer some callers may want alongside an env token.
  • Gate the call in people.go instead of the setter. The precedence belongs where AccessToken and AccountID already keep it, so every caller gets it.

Follow-up

Bump credstore once basecamp/cli#70 merges: go get github.com/basecamp/cli@main && go mod tidy.

…login

The credential store printed its "system keyring unavailable, credentials
stored in plaintext" warning only on Save. Every other command merely
reads, so a process whose keyring probe failed served whatever an earlier
fallback had left in credentials.json — on one machine today, tokens that
expired months ago and profiles that no longer existed — with no word
about why. The failing probe was invisible until the next login.

Store.Load now runs the same once-per-process warning as Save, so the
first read after a fallen-back probe says so on stderr, and once credstore
is bumped past basecamp/cli#70 the warning and the Load error also name
the probe failure itself. Hosts that mean to use file storage set
BASECAMP_NO_KEYRING, which skips the probe and never warns.

The wrapper's inner store is now the credStore interface rather than the
concrete *credstore.Store, so the test can stand in a fallen-back store
without failing a real keyring probe.
Copilot AI balanced review requested due to automatic review settings August 28, 2026 20:38
@jeremy

jeremy commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

@codex review

@github-actions github-actions Bot added tests Tests (unit and e2e) auth OAuth authentication labels Aug 28, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Surfaces credstore keyring-fallback warnings on the first credential read, preventing silent use of stale plaintext credentials.

Changes:

  • Warns once on the first credential read or write.
  • Adds an interface seam and regression test for fallback behavior.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
internal/auth/keyring.go Adds read-time fallback warnings and a testable store interface.
internal/auth/keyring_test.go Verifies warnings occur once across reads and writes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/auth/keyring.go
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: a9b8dcb3c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI review requested due to automatic review settings August 28, 2026 20:41

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

`basecamp people me` stores the fetched email through SetUserEmail, the
one read path with no BASECAMP_TOKEN guard: IsAuthenticated and
AuthorizationEndpoint short-circuit on the env token, then SetUserEmail
loads the stored credentials anyway. That was wrong twice over. The email
names the env token's user, not whoever the stored credentials belong to,
so writing it there mislabeled them. And the load ran the keyring probe —
now that a fallback read warns, a CI host with a locked keychain and only
BASECAMP_TOKEN warned about plaintext credentials it neither stored nor
read.

BASECAMP_TOKEN wins, matching AccessToken() and AccountID(): SetUserEmail
returns without touching the store.
Copilot AI review requested due to automatic review settings August 28, 2026 21:01
@jeremy

jeremy commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Since 6208afd (one commit, 84aaed3e is head):

  • Manager.SetUserEmail now returns without touching the store when BASECAMP_TOKEN is set, matching AccessToken() / AccountID() precedence. It was the one credential read a token session could reach — basecamp people me stores the fetched email — and it both mislabeled any stored credentials with the env token's user and ran the keyring probe, which with this PR would have warned a CI host with a locked keychain about plaintext credentials it neither stored nor read. TestSetUserEmailSkipsStoreOnEnvToken fails with the guard removed.
  • PR body: the "never touch the store" claim is now stated per path (AccessToken, IsAuthenticated, AccountID, AuthorizationEndpoint, auth status, doctor, and now SetUserEmail all short-circuit on the env token); declined guarding SetUserIdentity (login flows label freshly saved credentials) and GetUserEmail/GetOAuthType (not reached from a token session), and gating at the people.go call site instead of the setter.
  • Resolved the keyring.go:49 wording thread, addressed by 6208afd.

go test ./... is green except the PTY-dependent interactivity tests in internal/{appctx,cli,commands,stdinarg,tui/resolve}, which fail identically on origin/main in this agent session (no PTY allocation) and are unrelated; internal/auth and golangci-lint run ./internal/auth/... pass.

@codex review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

Reviewed commit: 84aaed3e29

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@jeremy
jeremy merged commit af381b3 into main Aug 28, 2026
26 checks passed
@jeremy
jeremy deleted the keyring-fallback-warn-on-read branch August 28, 2026 23:02
jeremy added a commit that referenced this pull request Aug 29, 2026
Pulls in basecamp/cli#70: the keyring availability probe now writes a
per-probe keychain item (service credstore.probe.<name>, account
__probe__.<pid>.<n>), so concurrent CLI processes no longer share one
probe entry and race each other's read-back. With the previous pin,
20 parallel invocations against the real macOS keychain returned 19
file-store fallbacks reading stale or missing credentials; with this
pin all 20 read the keyring. FallbackWarning() now carries the probe
failure reason, so the fallback warning added in #664 names its cause.

vendorHash recomputed and the Nix build verified via make
update-nix-hash.
jeremy added a commit that referenced this pull request Aug 29, 2026
Pulls in basecamp/cli#70: the keyring availability probe now writes a
per-probe keychain item (service credstore.probe.<name>, account
__probe__.<pid>.<n>), so concurrent CLI processes no longer share one
probe entry and race each other's read-back. With the previous pin,
20 parallel invocations against the real macOS keychain returned 19
file-store fallbacks reading stale or missing credentials; with this
pin all 20 read the keyring. FallbackWarning() now carries the probe
failure reason, so the fallback warning added in #664 names its cause.

vendorHash recomputed and the Nix build verified via make
update-nix-hash.
@robzolkos robzolkos added the bug Something isn't working label Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auth OAuth authentication bug Something isn't working tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants