Skip to content

fix(config): redact credentials in config get and refuse them in .cu.yml - #50

Merged
timimsms merged 1 commit into
mainfrom
fix/credential-surface
Aug 29, 2026
Merged

timimsms merged 1 commit into
mainfrom
fix/credential-surface

Conversation

@timimsms

Copy link
Copy Markdown
Owner

Summary

Completes the credential surface started in #46, which left two gaps I flagged in review there.

config get <key> still printed the token in full. The case for leaving it alone was that naming the key is a deliberate act — but that does not match what redaction actually defends against, which is incidental disclosure: pasted terminal output, a screen-share, a script whose stdout lands in a CI log. get is the spelling most likely to be captured by one. And it was never a route to a secret cu uses, since authentication reads the keyring, so redacting costs nothing real.

$ cu config get api_token            # stdout — what a pipe or a log captures
<redacted — cu authenticates via the system keyring, not this file>

$ cu config get api_token 2>&1 >/dev/null    # stderr — what a person sees
"api_token" is not printed. cu authenticates via the system keyring; this value is an unused plaintext leftover.
To read or remove it, edit /home/you/.config/cu/config.yaml directly.

Splitting the streams is the point: piped output stays clean, while an interactive user is told exactly where the value lives and how to remove it. GlobalConfigPath is exported so that path is the one Save really writes rather than a guess.

SaveProjectConfig wrote its settings map verbatim. Init already refuses to read a credential back out of .cu.yml, so writing one there would strand a plaintext secret on disk that cu never uses — the exact state the refusal exists to prevent, reached from the other direction. The only caller passes default_list, so this was latent rather than live.

Both directions now share one stripCredentials helper. It copies rather than deleting in place, so a caller that reuses its settings map does not silently lose keys — covered by its own test.

Behavior deliberately unchanged

A pre-existing plaintext token is still preserved, not scrubbed, exactly as #46 decided. Redaction hides it from casual output; it does not delete the user's data, and get now names the file so removing it by hand is obvious.

Tests

Five tests across both packages: the get and list redaction paths, the .cu.yml write guard, the caller-map-not-mutated property, and that GlobalConfigPath names the file Save actually writes. I verified the two behavioral ones fail without the change rather than passing vacuously — reverting the get redaction and the SaveProjectConfig guard each produces a failure naming the leaked value.

Note on provenance

These four files were briefly swept into #48 by a concurrent commit in the same working tree; that PR's message describes only the search/export work, so the credential change was riding along undisclosed. This PR carries them on a clean main base. #48 should have them removed so each gets reviewed on its own terms.

Checklist

  • go build, go vet, gofmt, and the full test suite pass on a clean origin/main base
  • Behavior verified end-to-end against a built binary with a seeded legacy token
  • Commit messages use conventional prefixes
  • No command help text changed, so no doc regeneration needed

Completes the credential surface started in #46, which left two gaps.

`config get <key>` still printed a token in full. The argument for leaving it
alone was that naming the key is a deliberate act, but that does not match what
redaction defends against: incidental disclosure — pasted terminal output, a
screen-share, a script whose stdout lands in a CI log. `get` is the spelling
most likely to be captured by one. It was never a way to reach a secret cu
uses, since authentication reads the keyring, so nothing is lost by redacting
it. The value goes to stdout redacted; the pointer to the real file goes to
stderr, so a person is told where to look while piped output stays clean.

`SaveProjectConfig` wrote its settings map verbatim. Init already refuses to
read a credential back out of .cu.yml, so writing one there would strand a
plaintext secret on disk that cu never uses — the exact state the refusal
exists to prevent, reached from the other direction. Both directions now share
one `stripCredentials` helper, which copies rather than deleting in place so a
caller reusing its map does not silently lose keys.

Tests cover both, and both were confirmed to fail without the change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ZEGsLHBQ2GzP6v48i4hXz
timimsms added a commit that referenced this pull request Aug 29, 2026
These four files were swept into the previous commit by a broad `git add` while
another change was in progress in the same working tree. They are unrelated to
the search/export work this branch is about, and that commit's message does not
mention them — so a change to what `cu config get` prints, and a new write guard
on .cu.yml, were riding along undisclosed on a PR titled as a performance fix.

They now live on their own in #50, against a clean main, where the security
surface gets reviewed on its own terms. Nothing here depended on them: no code
on this branch references GlobalConfigPath, stripCredentials, RedactedValue or
IsCredentialKey, and the suite passes without them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ZEGsLHBQ2GzP6v48i4hXz
@timimsms
timimsms merged commit 7e06108 into main Aug 29, 2026
16 checks passed
@timimsms
timimsms deleted the fix/credential-surface branch August 29, 2026 22:53
timimsms added a commit that referenced this pull request Aug 30, 2026
Brings in #50 and #51. One conflict, in the export flag block, where this
branch's --include-closed/--subtasks met #51 moving the destination file off
-o/--output onto --file/-F. Both sides kept: the new filters stay, and -o is
left to the global format flag as #51 intends.

Resolved as a merge rather than a rebase because another session owns this
branch; force-pushing rewritten history over it is the one operation that could
destroy work in progress.

Docs regenerated for the combined flag set — the auto-merged page still
advertised -o as the output file, which is no longer true.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ZEGsLHBQ2GzP6v48i4hXz
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.

1 participant