cli-tools config: store API keys instead of requiring an exported one - #14
Merged
Conversation
generate-names needed OPENAI_API_KEY or ANTHROPIC_API_KEY in the environment, which in practice means writing the key into a shell profile — a plaintext copy in a file that is backed up, synced, and read by every process you start, to configure one command that runs occasionally. So there is now a store: ~/.config/cli-tools/credentials.json, written 0600 inside a 0700 directory, with a command to manage it. cli-tools config what is set, and where each key came from cli-tools config set openai prompts with echo off cli-tools config unset openai Nothing prints a whole key. `config` shows a masked preview and a length — enough to tell two keys apart, not enough to use one — and --json carries the same previews rather than the values. The environment still wins over the file, because a one-off `KEY=… command` and a CI-injected key both have to keep working. That precedence is invisible at the moment it bites: you store a key, the old one keeps being used, and nothing says why. So `config` reports the *source* of each key rather than only whether one exists, and states plainly when a stored value is being shadowed. It is the same lesson as the `list` fix in #12 — presence is not the useful question, provenance is. A value may be piped for scripts, or passed inline; inline warns, because it lands in shell history and in `ps`. Deliberately not a .env: nothing loads it into an environment wholesale, nothing syncs it, and it is not how a key travels between machines. A secret a deployed service needs still belongs on that service. resolveCredentials returns an environment-shaped record, so resolveProvider consumes it unchanged — the wiring is one line, and a test covers it. 167 tests pass (was 164 plus 3 new wiring cases), typecheck clean. Verified end to end in a sandbox: set/unset/re-set, 0600 on a file that already existed with a looser mode, masked output, and no key in --json. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| }); | ||
|
|
||
| it('honours an explicit override', () => { | ||
| expect(credentialsPath({ CLI_TOOLS_CREDENTIALS: '/tmp/k.json' } as NodeJS.ProcessEnv)).toBe( |
|
|
||
| it('honours an explicit override', () => { | ||
| expect(credentialsPath({ CLI_TOOLS_CREDENTIALS: '/tmp/k.json' } as NodeJS.ProcessEnv)).toBe( | ||
| '/tmp/k.json', |
|
|
||
| it('never puts a whole key in the state', async () => { | ||
| const env = await sandbox(); | ||
| saveStored({ OPENAI_API_KEY: 'sk-secret-value-abcdefgh' }, env); |
ThreatCrush Security Scan6 finding(s) MEDIUM: 1 | LOW: 5
Snippets are redacted; ThreatCrush never prints matched credential material. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the setup command for API keys.
generate-namesneededOPENAI_API_KEYorANTHROPIC_API_KEYin the environment, which in practice means writing the key into a shell profile — a plaintext copy in a file that is backed up, synced, and read by every process you start, to configure one command that runs occasionally.Keys land in
~/.config/cli-tools/credentials.json, written0600inside a0700directory.$CLI_TOOLS_CREDENTIALSoverrides the path.Nothing prints a whole key
configshows a masked preview and a length — enough to tell two keys apart, not enough to use one — and--jsoncarries the same previews rather than the values:The environment still wins, and now says so
A one-off
KEY=… commandand a CI-injected key both have to keep working, so the environment overrides the file. That precedence is invisible exactly when it bites: you store a key, the old one keeps being used, and nothing explains why.So
configreports the source of each key rather than only whether one exists, and states plainly when a stored value is being shadowed:Same lesson as the
listfix in #12 — presence is not the useful question, provenance is.Input paths
… | cli-tools config set openaicli-tools config set openai sk-…psWhat this is not
A machine-local credential store, like
~/.aws/credentialsorgh auth. Not a.env: nothing loads it into an environment wholesale, nothing syncs it, and it is not how a key travels between machines. A secret a deployed service needs still belongs on that service.Checks
167 tests pass (164 + 3 new wiring cases), typecheck clean.
Verified end to end in a sandbox: set / unset / unset-again,
0600enforced on a file that already existed with a looser mode, masked output, no key in--json, and the error paths for an unknown key and an unknown verb.resolveCredentialsreturns an environment-shaped record soresolveProviderconsumes it unchanged — one-line wiring, covered by a test rather than by a manual run that would have cost an API call.🤖 Generated with Claude Code