Skip to content

macOS Keychain: refreshed token persists but logs misleading persist warning with -25299 #718

Description

@toruvieI

Summary

On macOS Keychain, a token refresh can successfully persist the refreshed access token/expiry, but gog still logs:

time=2026-06-09T17:00:43.727+02:00 level=WARN msg="persist refreshed token metadata failed" [email protected] client=default err="store token: set token: Failed to update item in keychain: The specified item already exists in the keychain. (-25299)"

This warning appears misleading: immediately after the warning, exporting the token shows the refreshed expiry was stored, and subsequent auth-using commands do not warn again until the next refresh window.

Environment

  • OS: macOS / Darwin arm64
  • gog: v0.21.0 (2c84d89 2026-06-01T21:54:29Z)
  • Keyring backend: macOS Keychain/default backend
  • Account/client: default OAuth client, Gmail/Calendar scopes among others

I also checked the relevant code path in v0.21.0, v0.22.0, v0.23.0, and main; the warning path appears unchanged.

Repro / observed behavior

Before expiry, commands do not warn. Token export showed:

access_exp=2026-06-09T15:00:14Z

At/after expiry, the first auth-using command refreshes and logs the warning:

gog gmail list 'is:inbox is:unread'

Output starts with:

time=2026-06-09T17:00:43.727+02:00 level=WARN msg="persist refreshed token metadata failed" [email protected] client=default err="store token: set token: Failed to update item in keychain: The specified item already exists in the keychain. (-25299)"

But immediately afterwards, repeated auth-using commands succeed without the warning:

gog gmail list 'is:inbox is:unread'
gog gmail list 'is:inbox is:unread'

and exporting the token shows that the new access token expiry was persisted:

access_exp=2026-06-09T16:00:42Z

So the runtime behavior looks like:

  1. stored access token expires;
  2. first command refreshes successfully;
  3. SetToken returns -25299 from macOS Keychain and client_auth.go logs persist refreshed token metadata failed;
  4. nevertheless the refreshed expiry is visible in the stored token;
  5. subsequent commands do not refresh/warn until the next expiry.

Why this seems wrong/misleading

The warning sounds like the refreshed token metadata was not persisted at all, which would imply every new gog process after expiry should refresh and warn again. That is not what happens: the refreshed expiry is visible in gog auth tokens export, and subsequent commands use it.

Looking at internal/secrets/store.go, SetToken writes multiple entries/aliases: primary token, legacy token for the default client, and subject-token alias when a subject is present. A plausible failure mode is that the primary token write succeeds, but a later alias write returns errSecDuplicateItem (-25299). If so, the current warning is too broad: it reports the whole refresh persist as failed, even though the primary token appears to have persisted.

Expected behavior

One of:

  • handle macOS errSecDuplicateItem during refresh alias writes by updating/replacing/retrying the specific item; or
  • make the warning more precise, e.g. distinguish primary token persistence from legacy/subject alias persistence; or
  • if -25299 is benign after read-back verification confirms the primary token was updated, downgrade/suppress the warning.

Relevant code path

internal/googleapi/client_auth.go:

if err := p.store.SetToken(p.client, persistEmail, updated); err != nil {
    slog.Warn("persist refreshed token metadata failed", "email", persistEmail, "client", p.client, "err", err)
    return t, nil
}

internal/secrets/store.go writes primary/legacy/subject token entries via verifiedSet(...). The observed error does not identify which of those writes failed, only:

store token: set token: Failed to update item in keychain: The specified item already exists in the keychain. (-25299)

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal priority bug or improvement with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:auth-providerThis issue is about auth, provider routing, model choice, or SecretRef resolution.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions