Repository navigation
Resolve API keys from macOS Keychain - #82
Merged
Merged
Conversation
Adds internal/keychain, a small wrapper around the `security` CLI for looking up, setting and deleting generic-password items by account/ service - the macOS Keychain mechanism issue #67 asks to support, so orcli.json can name a credential instead of containing it. Every call goes through exec.CommandContext with an argument array, never a shell. The one invariant the package is built around: a secret value can never reach a returned error. Lookup separates stdout (the secret) from stderr (the diagnostic) at the type level - commandErr's own signature only accepts stderr - and Set/Delete never see the secret's bytes again once they reach argv. Fully testable on Linux CI (which has no `security` binary and will never run macOS itself) via an injectable subprocess seam and a `goos` variable standing in for runtime.GOOS. Config changes (internal/config/keychain.go): - New `api_key_keychain` field names an entry in a new `keychain.items` block, each item being an {account, service} reference. - Precedence is deliberate and documented: an inline `api_key`, when present, always wins over `api_key_keychain`, so migrating means removing the stale field rather than just adding the new one beside it. - WriteKeychainItem/DeleteKeychainItem edit only the "keychain" member, the same byte-level approach every other writer in this package uses, despite this being the first writer for a block that isn't a pure raw-bytes passthrough like cloudflare/notion/github/apiary. cmd/orcli: - `orcli keychain set NAME ACCOUNT SERVICE` prompts for the secret with echo suppressed (golang.org/x/term.ReadPassword) when stdin is a real terminal, falling back to a plain line read for piped input and tests. - `orcli keychain list` shows names/accounts/services - never secret values - and marks which one api_key_keychain currently points at. - `orcli keychain delete NAME` removes both the config reference and the Keychain item, refusing an unconfigured name outright. - Startup (run's own ErrNoAPIKey case) now tries api_key_keychain before falling back to writing the empty-api_key stub, and reports a configured-but-failing reference as a startup error naming it, rather than silently treating it as no credential at all. orcli.json.example and README.md document the shape, the precedence rule, and the one exposure this approach cannot avoid while driving the `security` CLI rather than the Security framework's own API: the secret is briefly visible to another local process's own view of the process list while `security add-generic-password` runs. `make test` passes clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Nxr4mW9rMNzn9kThjdJpZt
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.
Fixes #67.
What
Adds
internal/keychain, a small wrapper around thesecurityCLI for looking up, setting and deleting generic-password items by account/service — the macOS Keychain mechanism issue #67 asks for, soorcli.jsoncan name a credential instead of containing it.Design decisions the issue left open
securityCLI (find-generic-password/add-generic-password/delete-generic-password), not the Security framework's native API — the native API has no stable cgo-free binding, and adding cgo to a client that otherwise has none is a bigger cost than the one exposure the CLI carries (see below). The issue's own acceptance criteria already describe-a/-smatching, which is this CLI's own flags.api_keyandapi_key_keychainare configured: the inline key always wins. Migrating means removingapi_keyfrom the file, not just addingapi_key_keychainbeside it — a staleapi_keysilently continuing to be used with no error is exactly the failure mode this feature exists to avoid elsewhere.Security properties (the issue's own acceptance criteria)
exec.CommandContextwith an argument array, never a shell — a configured account/service name reaches argv as a single element, never interpolated into a command line.Lookupseparates stdout (the secret) from stderr (the diagnostic) at the type level — the internalcommandErrhelper's signature only accepts stderr, so stdout isn't available to pass in by mistake. Tested explicitly (TestLookup_FailureNeverLeaksStdout,TestSet_FailureNeverLeaksTheSecret).orcli keychain setreads the secret with echo suppressed (golang.org/x/term.ReadPassword) when stdin is a real terminal; the interactive chat session is deliberately not used for this, since it renders input into the transcript.security add-generic-passwordas one argv element — the one exposure this approach cannot avoid while driving this CLI rather than the Security framework's own API, visible only to another local process inspecting the process list at that moment, and documented as such in the README.orcli keychain delete NAMEremoves only the named item, from both the config file and the Keychain.Testing
Fully testable on Linux CI (which has no
securitybinary and will never run macOS) via an injectable subprocess seam (runininternal/keychain) and agoosvariable standing in forruntime.GOOS. New tests acrossinternal/keychain,internal/config, andcmd/orclicover: lookup success/not-found/failure, the stdout/secret-leak guard,Set/Deleteargument shape, config precedence (inline vs. keychain, missing item, no keychain block, lookup-failure propagation), the writer (WriteKeychainItem/DeleteKeychainItem, including that a refused write leaves existing items untouched), theorcli keychain set/list/deletecommands end to end, and the startup path (run's ownErrNoAPIKeycase resolving viaapi_key_keychain, and reporting — not silently swallowing — a configured-but-failing reference).make testpasses clean.🤖 Generated with Claude Code
https://claude.ai/code/session_01Nxr4mW9rMNzn9kThjdJpZt
Generated by Claude Code