CLI Agent Config: Environment Variables and Secrets
Where should your agent's API key actually live? Good cli agent environment config splits secrets from settings, layers sources by precedence, and keeps keys out of logs, git, and model context.
import os
def load_config():
return {
"api_key": os.environ["ANTHROPIC_API_KEY"], # secret: from env only
"model": os.environ.get("AGENT_MODEL", "claude-sonnet-5"),
"timeout": int(os.environ.get("AGENT_TIMEOUT", "30")),
}Where should your agent's API key actually live? It's the question every CLI agent builder hits on day one, and the wrong answer — hardcoding it, or committing a .env file "just for now" — is how keys end up leaked on GitHub within the hour. Good cli agent environment config is the boring infrastructure that decides whether your tool is safe to share, safe to run in CI, and safe to hand a teammate. This is how to get it right without overcomplicating it.
The stakes are higher for agents than for most tools, because an agent's config includes credentials that can spend real money and touch real systems. Sloppy config isn't just untidy — it's a security incident waiting for a git push.
What Is cli agent environment config?
Config is everything your agent needs to run that isn't code: the API key, the default model, feature flags, tool settings, and paths. The discipline of cli agent environment config is deciding where each of those lives, how they're loaded, and which source wins when two disagree. Get the layering right and the same agent runs cleanly on your laptop, in a teammate's shell, and in an unattended CI job.
The foundational split is between secrets and settings. Settings — default model, verbosity, timeout — are harmless and can live in a committed config file. Secrets — API keys, tokens, passwords — must never touch version control and belong in the environment or a secret store. Conflating the two is the root of most config mistakes.
[object Object], os
,[object Object], ,[object Object],():
,[object Object], {
,[object Object],: os.environ[,[object Object],], ,[object Object],
,[object Object],: os.environ.get(,[object Object],, ,[object Object],),
,[object Object],: ,[object Object],(os.environ.get(,[object Object],, ,[object Object],)),
}What this does: Pulls the secret strictly from the environment (failing loudly if absent) while giving non-secret settings sensible defaults. The API key has no default and no fallback file — the only way in is the environment, which keeps it out of your codebase.
Why It Matters
Because the cost of getting it wrong is asymmetric and immediate. A leaked settings file is a shrug; a leaked API key is someone else spending your money and impersonating your app until you notice. Config discipline is cheap insurance against an expensive, embarrassing failure.
Three people feel this differently, and all three need it solved.
A freelance developer shipping an open-source CLI agent needs config that lets users bring their own key safely — if the tool encourages hardcoding, every user who forks it risks committing their secret.
A platform engineer running the agent in CI needs it to read config from environment variables the pipeline injects, with no interactive prompts and no files on disk, because CI has no keyboard and no business storing secrets in the repo.
A security-conscious team lead needs assurance that secrets never land in logs, transcripts, or error messages — an agent that echoes its API key into a stack trace has leaked it to every logging system downstream.
The common requirement: secrets flow through a controlled path and never leak sideways.
How Should You Layer Config Sources?
With a clear precedence order, because config comes from several places and they will disagree. The standard, least-surprising order from highest priority to lowest: command-line flags, then environment variables, then a config file, then built-in defaults. A flag the user typed right now should always beat a default from a file they forgot about.
[object Object], ,[object Object],(,[object Object],):
,[object Object],
,[object Object], key ,[object Object], cli_args ,[object Object], cli_args[key] ,[object Object], ,[object Object], ,[object Object],:
,[object Object], cli_args[key]
env_name = ,[object Object],
,[object Object], env_name ,[object Object], os.environ:
,[object Object], os.environ[env_name]
,[object Object], key ,[object Object], file_config:
,[object Object], file_config[key]
,[object Object], defaultWhat this does: Resolves each setting by checking sources in priority order, returning the first that provides a value. The explicit chain means behavior is predictable — you can always answer "why is the model set to X" by walking the four sources top to bottom.
This precedence is a usability feature, not just plumbing. A user can set a permanent default in their config file, override it per-project with an environment variable, and override that for a single run with a flag — each layer doing exactly what layers should do.
⚡ Pro tip: Add a --show-config command that prints the resolved value of every setting and which source it came from. When someone's agent behaves unexpectedly, "model: claude-sonnet-5 (from config file)" ends the debugging in one line instead of a guessing game across four sources.
⚡ Pro tip: For real secrets, prefer the OS keychain over a .env file. Libraries like keyring (Python) store credentials in the system's encrypted secret store — macOS Keychain, Windows Credential Manager, or the Linux Secret Service — so the key is never sitting in plaintext on disk at all.
Where Should the Config File Live?
Settings that aren't secrets belong in a config file, and where that file lives is a decision worth making deliberately. The convention that surprises users least is the two-level pattern: a global config in the user's home directory for personal defaults, and an optional project config in the working directory that overrides it for a specific repo.
[object Object], pathlib, json
,[object Object], ,[object Object],():
global_cfg = pathlib.Path.home() / ,[object Object], / ,[object Object], / ,[object Object],
project_cfg = pathlib.Path.cwd() / ,[object Object],
merged = {}
,[object Object], p ,[object Object], (global_cfg, project_cfg): ,[object Object],
,[object Object], p.exists():
merged.update(json.loads(p.read_text()))
,[object Object], mergedWhat this does: Loads a global config from the standard ~/.config/agent/ location, then layers a project-local .agentrc.json on top if present, so a repo can pin its own model or timeout without changing the user's personal defaults. Project settings win, matching how developers expect per-project config to behave.
Following the platform's config conventions — ~/.config on Linux per the XDG spec, the equivalent on macOS and Windows — means your tool's files land where users and their backup tools expect, instead of littering home directories with dotfiles. A project-level .agentrc.json that teams commit to their repo also lets a whole team share the same agent settings, so everyone's tool behaves consistently without each person configuring it by hand.
⚡ Pro tip: Ship an agent init command that writes a commented starter config to the right location. Users discover available settings by reading the generated file rather than hunting through docs, and you control the defaults they start from. A good starter config is the fastest documentation you'll ever write.
How Do You Keep Secrets Out of Logs and Context?
This is the part most config guides skip entirely, and it's where agents leak. Your agent logs tool calls, prints errors, and — critically — sends context to a model. Any of those paths can accidentally carry a secret. Redaction has to be deliberate.
[object Object], re
SECRET_PATTERNS = [
re.,[object Object],(,[object Object],), ,[object Object],
re.,[object Object],(,[object Object],),
]
,[object Object], ,[object Object],(,[object Object],):
,[object Object], pat ,[object Object], SECRET_PATTERNS:
text = pat.sub(,[object Object],, text)
,[object Object], textWhat this does: Scrubs secret-shaped strings out of any text before it's logged or displayed. Running tool output and error messages through redact() means a config file or environment dump that sneaks a key into output gets masked before it reaches a logging system or the model's context.
The model context is the sneaky one. If a tool reads a file that happens to contain a credential and returns it as a tool result, that secret is now in the conversation you might persist or send onward. Redacting tool results before they enter the message history closes a leak most people never realize they have.
⚠️ Common mistake: Committing a .env file with real secrets "temporarily." There is no temporary — git remembers, and a pushed key is compromised the moment it's public, even if you delete it seconds later. Add .env to .gitignore before you create it, ship a .env.example with blank values, and treat any key that ever touched a commit as burned.
Common Mistakes
Beyond the committed .env, a few config errors recur. Hardcoding the key as a fallback "so it works out of the box" defeats every protection above — there's no safe default for a secret. Reading config once at import time and caching it means --show-config and runtime overrides drift out of sync; resolve config in one place and pass it down. And giving unclear errors when a key is missing sends users hunting; a message like "ANTHROPIC_API_KEY not set — export it or add it to your keychain" turns a cryptic crash into a fixable instruction.
One more that catches teams: storing config in the wrong scope. A setting that belongs per-project — which model this repo uses — placed in global config affects every project at once, and a personal preference placed in committed project config imposes itself on the whole team. Match each setting's scope to where the decision actually belongs, and the same layered precedence that resolves conflicts also keeps responsibilities clear.
⚡ Pro tip: Validate config at startup and fail fast with a helpful message. Check that the key exists, the model name is plausible, and numeric settings parse — then exit with a clear explanation if not. A tool that dies immediately with "timeout must be a number, got 'thirty'" beats one that crashes cryptically ten seconds into a task.
Conclusion
Solid cli agent environment config comes down to a few disciplined habits: split secrets from settings, layer sources with clear precedence, keep secrets in the environment or a keychain and never in git, and redact them from every output path including the model context. None of it is glamorous, and all of it is what separates a tool you can safely share from one that leaks a key on its first push.
The config schema, the precedence rules, and the redaction patterns are reusable across every agent you build — and so are the system-prompt notes about how the agent should handle sensitive values. Keeping those alongside your prompts in a library like PromptABCD means the next agent starts with safe config defaults baked in, instead of relearning the secrets-versus-settings lesson the hard way.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
