Skip to content

Remember which keyring backend was found, instead of searching every run - #37

Merged
Korijn merged 1 commit into
masterfrom
claude/keycmd-keyring-instructions-dixlfa
Aug 5, 2026
Merged

Remember which keyring backend was found, instead of searching every run#37
Korijn merged 1 commit into
masterfrom
claude/keycmd-keyring-instructions-dixlfa

Conversation

@Korijn

@Korijn Korijn commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Replaces the first version of this PR, which printed a hint telling the user to set PYTHON_KEYRING_BACKEND themselves. keycmd does it itself now, and the branch was rebuilt from master — there is nothing left of the hint machinery.

Left to itself, keyring works out which backend to use by loading every backend that every installed package registers and keeping the best of them. That search runs again on every invocation and is the single most expensive thing a keycmd run does: measured on the machine this was written on it is the difference between 0.156s and 0.085s, and it grows with the number of packages installed and with any backend that takes its time deciding it is not viable.

The answer, though, is the same every time until the packages on the machine change. So backend.py writes it down the first time a run needs a credential and loads that backend by name afterwards — the shortcut PYTHON_KEYRING_BACKEND buys, without anyone having to know the variable exists. Nothing to read, nothing to set; the second run is simply faster than the first.

keycmd: keyring backend: keyrings.alt.file.PlaintextKeyring (found in 0.12s)   # the first run
keycmd: keyring backend: keyrings.alt.file.PlaintextKeyring (remembered)       # every run after

The note, and how far it is trusted

One line, in the folder the platform keeps files a program can afford to lose: %LOCALAPPDATA% on windows, ~/Library/Caches on macOS, $XDG_CACHE_HOME on linux. Deleting it costs one slow run. It is treated as untrusted input throughout:

  • is_backend_name keeps anything that is not a dotted class name from reaching an import, so a file that has been truncated or scribbled in is worth no more than a fresh search.
  • A name that no longer loads sends the run back to searching, and is replaced. load_keyring asks the class for its priority on the way, which is how keyring itself decides a backend is viable, so a backend that was uninstalled and one whose daemon stopped are both caught by the same except.
  • backend_name looks through the chainer, which is not a backend but the search wearing one's clothes — writing that down would leave the search in place — and names the backend it would have reached first, so the credentials keep coming from where they already did.
  • PYTHON_KEYRING_BACKEND outranks the note and is never written over.
  • A search that found nothing is not an answer, and is not remembered.
  • The write goes through a temporary file and an os.replace, so a second keycmd running at the same moment reads either the old name or the new one. A cache folder that cannot be written is a slow run, not a failed one.

The two commands

What keycmd cannot notice by itself is a backend that still loads but is no longer the one you want — you installed a better one, or removed a package and want the runner-up. So both steps are available on purpose:

❯ keycmd --detect-backend
keycmd: remembered keyring backend keyring.backends.SecretService.Keyring, found in 0.12s

❯ keycmd --reset-backend
keycmd: forgot the remembered keyring backend

Errors that used to be tracebacks

Two ways of ending up with no backend reached the user as a traceback, and both are now errors that say what to do — logs.error grew the hint lines they need:

❯ keycmd 'npm install'
keycmd: error: keyring has no backend to read credentials from
keycmd: hint: install one for this platform, or name one you have with PYTHON_KEYRING_BACKEND
keycmd: hint: see https://github.com/jaraco/keyring#third-party-backends

Inside a distribution that error also points at the WSL section of the README, which is what the situation almost always is. A PYTHON_KEYRING_BACKEND that cannot be loaded is reported with the value it was given.

Testing

156 passed / 16 skipped with no keyring available, 169 / 3 with one. tests/test_backend.py covers the cache paths of all three platforms, the name check, remembering and forgetting, an unwritable cache, a scribbled note, a stale note, and every branch of load_backend; tests/test_cli.py covers the two flags and the run that quietly writes the note. Two things worth knowing for future tests, and now in CLAUDE.md: the autouse cache_home fixture points CACHE_HOME at tmp_path so a run never touches the note the machine is using, and a test about remembering has to delenv PYTHON_KEYRING_BACKEND first, since the README suggests running the suite with it set and it outranks everything here.

keyring stays out of the import path of a run that looks up no credential, and test_cli_import_stays_lean still passes. The version is untouched, since that moves in its own commits.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CRYugGcVh3wvC5a5pzwKU3

Left to itself, keyring works out which backend to use by loading every
backend that every installed package registers and keeping the best of
them. That search runs again on every invocation and is the single most
expensive thing a keycmd run does: measured here it is the difference
between 0.156s and 0.085s, and it grows with the number of packages
installed and with any backend that takes its time deciding it is not
viable.

The answer, though, is the same every time until the packages on the
machine change. So the new backend.py writes it down the first time a
run needs a credential and loads that backend by name afterwards, which
is the shortcut PYTHON_KEYRING_BACKEND buys without anyone having to
know the variable exists. Nothing to read, nothing to set: the second
run is simply faster than the first.

The note goes where the platform keeps files a program can afford to
lose -- %LOCALAPPDATA% on windows, ~/Library/Caches on macOS,
$XDG_CACHE_HOME on linux -- and holds one line, and is trusted only as
far as it can be checked:

- is_backend_name keeps anything that is not a dotted class name from
  reaching an import, so a file that has been truncated or scribbled in
  is worth no more than a search
- a name that no longer loads sends the run back to searching and is
  replaced. load_keyring asks the class for its priority on the way,
  which is how keyring itself decides a backend is viable, so an
  uninstalled backend and one whose daemon stopped are both caught
- backend_name looks through the chainer, which is not a backend but the
  search wearing one's clothes; writing that down would leave the search
  in place, so the backend it would have reached first is written instead
- PYTHON_KEYRING_BACKEND outranks the note and is never written over
- a search that found nothing is not an answer, and is not remembered

What keycmd cannot notice by itself is a backend that still loads but is
no longer the one you want, so the two steps are also available on
purpose:

  keycmd --detect-backend   # search now, and remember what turns up
  keycmd --reset-backend    # forget it, so the next run searches again

Two ways of ending up with no backend used to reach the user as a
traceback and are now errors that say what to do: keyring settling on
fail.Keyring, which raises on the first lookup and which inside a distro
points at the WSL section of the README, and a PYTHON_KEYRING_BACKEND
that cannot be loaded. logs.error grew the hint lines those need.

keyring stays out of the import path of a run that looks up no
credential: backend.py reaches for it inside the functions that need it,
the way creds.py used to, and its own name only appears at module level
under TYPE_CHECKING.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRYugGcVh3wvC5a5pzwKU3
@Korijn
Korijn force-pushed the claude/keycmd-keyring-instructions-dixlfa branch from efe2453 to 728e94e Compare August 5, 2026 11:08
@Korijn Korijn changed the title Tell users how to name the keyring backend, and stop looking for one Remember which keyring backend was found, instead of searching every run Aug 5, 2026
@Korijn
Korijn merged commit 679eb6d into master Aug 5, 2026
9 checks passed
@Korijn
Korijn deleted the claude/keycmd-keyring-instructions-dixlfa branch August 5, 2026 11:25
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.

2 participants