fix(config): refuse plaintext credentials and redact them in config list - #46
Conversation
`cu config set api_token <token>` wrote the token to ~/.config/cu/config.yaml in plaintext — and then never used it, since auth reads the OS keyring only. The operation had no upside: it leaked a secret to disk and left `cu auth status` reporting "Not authenticated". This was an asymmetry introduced with the .cu.yml credential guard: project files were filtered, but Set staged every key including credentials. `cu config set` now refuses credential keys and points at `cu auth login`, and config.Set no longer stages them as a backstop — they still apply in-process, so nothing that relies on a runtime override breaks. `cu config list` redacts credential values, since that output is pasted into issues and terminals far more casually than an explicit `config get <key>`, which is left alone as a deliberate act. A token already in a config file is preserved rather than silently deleted; refusing to add more is the fix, and list redacts what is already there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014aqbmccWm1tqttmBUCR5rv ClickUp: 86dxbeqyt
Review — approve, no blockersVerified every claim in the description against a built binary with a throwaway Also confirmed the premise independently on this branch — Two calls I think are right and worth keeping: Preserving a pre-existing token rather than scrubbing it. Deleting user data during an unrelated The redaction string not claiming the value lives in the keyring. For a legacy entry that would be a lie, and it's the exact case where the text is displayed. Non-blocking: one asymmetry is left
It's latent, not live: the only caller is On your
|
Summary
cu config set api_token <token>wrote the token to~/.config/cu/config.yamlin plaintext — and then never used it, because authentication reads the OS keyring only:The operation had no upside at all: it leaked a secret to disk and left the user unauthenticated. Nothing in the codebase reads
api_tokenback —internal/configdeclares the field, and that is the only reference.This is an asymmetry I introduced in #43. That PR added a credential filter for project
.cu.ymlfiles butSetstaged every key, so the global config had no equivalent guard.Changes
cu config setrefuses credential keys and points atcu auth login, exiting non-zero.config.Setno longer stages them, as a backstop. They still apply in-process, so anything relying on a runtime override is unaffected — but they can never reach disk.cu config listredacts credential values. That output gets pasted into issues and terminals far more casually than an explicitconfig get <key>, which is left alone as a deliberate act by the user. Happy to redactgettoo if you'd rather be uniform.config.IsCredentialKeyis exported so both layers share one definition.A token already in a config file is preserved, not silently deleted. Removing a user's data on the next unrelated
config setwould be a worse surprise than leaving it; refusing to add more is the fix, andlistredacts what is already there. The redaction text deliberately does not claim the value lives in the keyring, because for a legacy entry it does not.Tests
TestCredentialKeysAreNeverStagedcovers the key predicate, thatSetapplies in-process while never writing to disk, and that a pre-existing plaintext token survives an unrelated save.Checklist
./scripts/ci.shpasses locally — excepterrcheck, which reports the same pre-existing findings onmain, none in files this PR touches