Skip to content

feat: fetch secrets on demand via <NAME>_COMMAND or the OS keychain #2011

Description

@newhoggy

Problem

#2006 added <NAME>_FILE, which keeps secrets out of env listings, /proc/<pid>/environ, shell history, child processes and settings.json. That stops accidental leaks. It does not stop a process running as the same user from deliberately reading a secret. The secret is still plaintext on disk, and its path is in the environment or settings.json, so the file is easy to find.

That gap matters here in particular. This workflow runs many concurrent agent sessions (Claude Code, Codex, pi) as the user, and the claude-cli backend's --claude-cli-allow-tools escape hatch runs a tool-capable nested session driven by untrusted prompt content (diffs, commit messages, Jira text). A prompt-injection payload in any of these can cat a secret file as easily as it can read an environment variable.

Proposal

Let a secret be fetched on demand from a store that doesn't keep it as plaintext on disk. It would be one more source in utils::secret_env, next to <NAME> and <NAME>_FILE. The call sites would not change, because they already go through secret_var/secret_var_any (STYLE-0030).

Option A: <NAME>_COMMAND (credential helper)

<NAME>_COMMAND names a command whose stdout is the secret. This follows git credential helpers and AWS credential_process. Examples:

DATADOG_API_KEY_COMMAND='op read op://Private/datadog/api-key'
ATLASSIAN_API_TOKEN_COMMAND='security find-generic-password -s omni-dev-atlassian -w'
  • It is store-agnostic: 1Password, macOS security, pass, Vault, secret-tool, and so on.
  • It can require biometric approval per use, for example through 1Password's CLI integration with Touch ID.
  • Open questions:
    • How to split the command: shell-words versus sh -c. The ADR must decide. Shell interpretation is a larger surface area.
    • A timeout, and whether stdin is closed.
    • How to treat a non-zero exit and an empty result.
    • Whether stderr is passed through to the user or captured.
    • Caching: once per process, or per call. Consider the daemon: the Snowflake service and the browser bridge are long-lived.
    • Conflict rules with NAME and NAME_FILE within a layer. This should extend feat: support <NAME>_FILE for every secret environment variable #2006's per-layer pair to a triple.
    • Whether a _COMMAND value in settings.json is acceptable. Anything that can write settings.json could then run arbitrary commands. Perhaps restrict _COMMAND to the process environment only, or warn.
    • The claude-cli scrub must also remove *_COMMAND for secret names.

Option B: native OS keychain

Read secrets directly from the macOS Keychain or the Linux Secret Service (for example with the keyring crate), under a fixed service name such as omni-dev and an account named after the variable. The auth login commands could write there instead of to settings.json.

  • It is simpler for users, and nothing about it lives in the environment.
  • It adds a dependency and platform-specific behaviour. On macOS the ACL is per binary, and omni-dev installs unsigned, so every upgrade may trigger a new keychain prompt. The Accessibility grant (ADR-0058) has the same upgrade problem.
  • A headless daemon has no keychain on Linux without a Secret Service session.

A is the more general design, and B can be built on top of A (security find-generic-password …). Recommend deciding between them in an ADR that extends ADR-0089.

Acceptance

  • An ADR choosing A, B or both, and settling the open questions above.
  • The resolver supports the chosen sources, with MapEnv tests and no mutation of the process environment. For A, use a fake command.
  • The grep guards and the registry are unchanged. New sources apply to every registered secret automatically.
  • The claude-cli scrub covers the new variable names.
  • The operator docs and the CHANGELOG are updated.

Follow-up to #2006 / #2007.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    adrArchitecture Decision RecordenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions