Built & maintained byRuntime

12 · Best practice

Wire agents to env vars, not secrets

Agent prompts never hold raw credentials. Env var references only. Agent confirms its environment before any write action.

A prompt containing a raw API key is a secret waiting to leak. It lives in conversation history, in any tool that processes the transcript, and in logs you don't control. The fix is the same rule that applies to source code: reference secrets by name, never by value.

The second risk is quieter: an agent that infers its environment from context will eventually infer wrong. Prod data gets touched on what everyone assumed was a dev run. Make the environment explicit and confirmed before any write action happens.

The rules

  • Secrets by reference only. Prompts and generated code use $API_KEY, not the key itself. The value stays in the shell or a vault.
  • Explicit environment. Dev, staging, and prod are named directly in the prompt or confirmed at session start — never inferred from URL patterns or config filenames.
  • Halt on missing secrets. If a required env var is absent, the agent surfaces a clear error and stops. No defaults, no substitutions.
  • Confirm before writing. The agent's first output in any session states the environment name and lists which secrets it has access to.

How to do it

1

Add required env vars to .env.example with empty values and a comment. Keep actual values in .env (gitignored). The agent reads from the shell — never from a prompt.

# .env.example
DATABASE_URL=          # postgres://... (dev uses localhost, prod uses RDS)
STRIPE_SECRET_KEY=     # sk_live_... or sk_test_...
DEPLOY_ENV=            # dev | staging | prod
2

Open every agent session with an explicit environment check. Wire it into your skill or CLAUDE.md so it runs automatically.

Before taking any write action, confirm:
1. The value of $DEPLOY_ENV
2. Which of the following secrets are present in the shell (answer yes/no only — do not print values): DATABASE_URL, STRIPE_SECRET_KEY
If DEPLOY_ENV is unset or any required secret is missing, halt and list what's missing.
3

If the agent must construct a URL or connection string, build it from env vars — never hardcode the hostname.

# Right
db_url = os.environ["DATABASE_URL"]

# Wrong
db_url = "postgres://user:pass@prod-db.internal/app"
4

Test the guard. Run the agent with DEPLOY_ENV unset and confirm it halts with a clear error instead of defaulting to anything.

The session-start confirmation pattern

The agent's first output should look like this — not a paragraph, just a two-line check:

Environment: dev
Secrets present: DATABASE_URL ✓  STRIPE_SECRET_KEY ✓  DEPLOY_ENV ✓

If a secret is missing:

Environment: unset
Missing required secrets: DEPLOY_ENV, DATABASE_URL
Halting. Set these in your shell and restart the session.

This makes environment mismatches visible before any damage is done. A teammate cloning the repo gets the same check — they'll see exactly what to set up without asking you.

Anti-patterns

  • Pasting a token into a prompt "just this once." It's in the transcript now.
  • Letting the agent infer dev vs. prod from the database hostname. Inferences break.
  • Defaulting to a safe-looking value when a secret is missing. Halt instead — silent fallbacks hide misconfiguration.
  • Skipping the session-start confirmation because "I know what environment I'm in." Your teammate doesn't.
  • Storing DEPLOY_ENV=prod in a committed .env.example. The example file names keys, not values.

Related

Get started

Clone the repo. Open it in Claude Code.

Two minutes from clone to your first PRD. Fill in three files and ship.

git clone https://github.com/runtm-ai/claudecode-for-pms.git
Open on GitHub

MIT licensed. Star the repo if it saves you an afternoon.