Skip to main content
  • Status: accepted · Date: 2026-07-19 · Deciders: Davor Runje

Context

The skills reach external services that key-gate their useful rate limits — Semantic Scholar (S2_API_KEY), OpenAlex (OPENALEX_MAILTO, polite pool), private rclone mirror remotes — but keys are read ad hoc (os.environ inside _lit_client) with no unified place to store, add, or check them, and no guidance on obtaining them. Keyless Semantic Scholar throttling is the root cause behind ADR-noted rate-limit work (#41); “get a key” is the easy ceiling-raiser, but today it is undiscoverable and unmanaged. For a tool run across many repos, that invites keys pasted into committed config or lost, and secrets leaking through shell history, argv, or over-broad exposure to child processes. This is an integrity tool, so the design must also be honest about what it does not do: convenience storage is not encryption.

Decision drivers

  • One controlled, documented way to add/check keys — no rediscovery, no hand-edited files.
  • Never leak a secret through shell history, argv, or a process list.
  • Least privilege: a child process (rclone, the engineering backend) should see only the secrets its job needs, not the whole store.
  • Prefer no secret on disk during an operation; when a file is unavoidable, make it short-lived and 0600.
  • CI/interop: an injected environment variable must always win over the store.
  • Honest posture: disclose plaintext-at-rest; do not imply “secure”.
  • Light dependency footprint (ADR-0024) — avoid a heavy secrets dependency in v1.

Considered options

  1. A globally-loaded .env file (dotenv-style), read on every CLI start.
  2. A CLI-managed JSON store + a keys command group, read in-process, with stdin-based writes and scoped in-memory env for child processes. (chosen)
  3. The OS secure store now (Keychain / libsecret / Credential Manager).

Decision

Option 2.
  • Store. A CLI-owned JSON file .honest-scholar/keys.json — gitignored, created 0600 — holding each key plus light metadata (service, how-to-obtain link). JSON (not .env) because the CLI is the only writer/reader, so a structured, metadata-carrying format beats line-parsing; and a global .env auto-loaded by unrelated tools is a foot-gun.
  • Command group. honest-scholar keys set|list|check|unset|path. set reads the value from stdin / a hidden prompt (never argv), and accepts a JSON blob on stdin to set many at once or to run a one-shot without persisting. list/check report presence, never values, and which source each key came from.
  • Read precedence. os.environ > the JSON store > unset. A present env var always wins (CI/secrets injection), then the store, then the service degrades (per the rate-limit handling).
  • In-process use. Our own CLI commands read the store into memory and use it directly — no .env, no temp file is written for our own work (the common path).
  • Child processes. Secrets are handed to a child via a scoped in-memory env= dict containing only what that job needs — not a file. rclone in particular takes RCLONE_CONFIG_<REMOTE>_<…> env vars, so no config file is needed. A temporary .env/file is a documented last resort only for a tool that can only read a file, created under TMPDIR/tmpfs at 0600 and removed in a finally.
  • Honesty. The store is plaintext at rest; docs say so explicitly — gitignore + 0600 limit exposure but are not encryption. research-init scaffolds guidance (not secrets); doctor reports key presence, never values.

Consequences

  • Adding/checking a key is one command; nothing lands in shell history or a committed file; env vars still override for CI.
  • Child processes get least-privilege secret exposure with no disk artifact on the default path.
  • The store gives no at-rest encryption — an accepted, disclosed limitation. Real at-rest protection (OS keychain) is deferred to #49 as an optional backend behind the same keys interface.
  • Implementation stays light-dep (stdlib JSON + a small loader); no Pydantic, no dotenv dependency.

Rejected alternatives

  • Global .env — auto-loaded by unrelated tools (surprising), no scoping to child processes, weaker metadata, and encourages a broad always-present secret file. The CLI-owned store with explicit env provisioning is more controlled.
  • OS secure store now — the right at-rest answer, but heavy/platform-specific and needs a graceful headless/CI fallback; deferred to #49 as an optional extra.
  • Secret via argv (keys set NAME VALUE) — leaks to the process list and shell history; rejected in favor of stdin/hidden-prompt.
honest-scholar/honest_scholar/cli.py (_lit_client, the keys group); resources/ensure-tooling.md; #42 (implementation), #49 (OS-keychain backend follow-up), #41 (rate-limit handling — the “get a key” complement); ADR-0024 (light-dependency / optional-extra posture).