How this SDK (and its sibling repos cli_subprocess_core and
agent_session_manager) handle environment-driven configuration and
credentials, and the rules that keep secrets out of logs and builds.
The architecture
- All env reads are runtime.
config/runtime.exssnapshots the OS environment into Application config once at boot; every read goes through the internal <code>ClaudeAgentSDK.Env</code> wrapper over that snapshot. Nothing reads env at compile time, so a release built on CI cannot bake in a build-machine value. - Variable names live in one registry —
ClaudeAgentSDK.Config.Env— not in string literals scattered through the code. - The snapshot is allowlisted.
runtime.exscopies onlyConfig.Env.all_known_vars/0plus theCLAUDE_/ANTHROPIC_namespaces (Config.Env.snapshot/1). A whole-System.get_env()copy would spread every unrelated secret in the parent environment (database URLs, CI tokens) into inspectable Application config. When adding a new env read, add the name toall_known_vars/0(or use aCLAUDE_-prefixed name, which passes automatically). - CLI subprocesses are isolated. The child env is built from the
Config.Env.passthrough_vars/0allowlist (+ per-call overrides) and passed withclear_env?: true— theclaudeCLI sees only what the SDK hands it, not the whole node environment.
The rules
- Read config at runtime, never compile-time. Env-driven config —
especially secrets — belongs in
config/runtime.exsor inside a function, never in a module attribute or compile-timeconfig.exs. - Redact secrets from
inspect. Any struct holding a token, key, or password must carry@derive {Inspect, except: [:the_secret_field]}(or a customInspectimpl). Structs get inspected in OTP crash reports,Loggermetadata, and error tuples. In this SDK:AuthManager(:token) andOptions(:anthropic_auth_token,:env) are redacted. - Log names, not values. Emit
env_keys: Map.keys(env); never log an env map's values or a raw token. Return offending keys in error tuples, not the full env (seeCliSubprocessCore.Commandenv validation). - Allowlist, don't snapshot-all — for the runtime snapshot and for subprocess environments alike.
- Fail fast on required secrets. Prefer raising at the launch
boundary over letting a silent
nilsurface as a confusing failure later. - Never commit secrets.
.envis gitignored in all three repos (*.env.exampletemplates are fine); no literal keys in source or compile-time config. Token files are written0600(created with restrictive permissions before content is written, then renamed into place). - Tests run with no secrets.
mix testpasses with an empty environment; live paths are gated behind env flags (LIVE_TESTS,CLAUDE_EXAMPLES_BACKEND).
Guardrails
scripts/secrets_guard.sh runs in mix ci across all three repos and
fails on:
- a
config/runtime.exsthat snapshots the whole env instead of an allowlist, and - a
defstructinlib/declaring a secret-named field (token/api_key/secret/password/auth_token/credential/bearer) in a module without Inspect redaction.
Reviewed false positives (for example key-name registries, which hold
names rather than values) carry a same-line # secret-safe: annotation.