Claude API Environment Variables: Security Best Practices
If you're searching for how to handle Claude API keys safely, the short answer is: never hardcode them, never commit them, store them in environment variables scoped to the narrowest possible context, and rotate them on a schedule plus whenever exposure is suspected. The rest of this guide covers exactly how to do that across local development, CI/CD pipelines, and production deployments without breaking your workflow.
Claude API keys (and similarly, keys for any wrapper service like SubToAPI) are bearer credentials — whoever holds the string has full access to whatever that key is scoped to. Treating them with the same care as a database password or a signing secret is not optional once you move past a weekend prototype.
Why Environment Variables (Not Config Files)
Environment variables are the standard mechanism for injecting secrets into a running process without writing them to disk in a readable file that might get committed, backed up, or synced to a laptop. Compared to the alternatives:
- Hardcoded strings in source: get committed, show up in
git logforever even after deletion, get copy-pasted into Slack and docs. - Plaintext config files (
config.json,settings.yaml): often excluded from.gitignoretoo late, after the first commit. - Environment variables loaded at runtime: live only in process memory, don't persist in your repo, and can be scoped per-environment (dev, staging, prod) trivially.
The tradeoff is that environment variables are visible to anything that can read the process environment — child processes, crash dumps, and some logging middleware. That's manageable with the practices below, but it's worth knowing the limitation.
Local Development
Use a .env file that is explicitly listed in .gitignore before the first commit, not after:
# .gitignore
.env
.env.*
!.env.example
Keep a .env.example with placeholder values checked into the repo so teammates know what variables are expected:
# .env.example
ANTHROPIC_API_KEY=sk-ant-xxxxxxxx
SUBTOAPI_KEY=sub_live_xxxxxxxx
Load variables with a library like dotenv rather than exporting them in your shell history, where they can leak via history files or terminal multiplexer logs:
require('dotenv').config();
const apiKey = process.env.SUBTOAPI_KEY;
if (!apiKey) {
throw new Error('SUBTOAPI_KEY is not set');
}
Fail fast if a required variable is missing — don't let the app silently fall back to undefined and produce a confusing 401 three layers deep.
CI/CD Pipelines
CI systems (GitHub Actions, GitLab CI, CircleCI) all provide encrypted secret storage separate from your repo and separate from build logs. Use it instead of pasting keys into .yml files:
# GitHub Actions example
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run integration tests
env:
SUBTOAPI_KEY: ${{ secrets.SUBTOAPI_KEY }}
run: npm test
A few rules specific to CI:
- Mask output. Most CI providers automatically redact known secret values from logs, but only if the secret is registered through their secrets UI, not if it's embedded in a script argument.
- Scope secrets per job/environment. A key used for production deploys shouldn't also be available to a PR-triggered test job from a fork, where a malicious PR could exfiltrate it.
- Never echo secrets for debugging.
echo $SUBTOAPI_KEYin a debug step is a common way keys end up in build logs that get archived for 90 days.
Production Deployments
In production, environment variables should come from your platform's secret manager rather than a .env file sitting on the server:
- Cloud platforms: AWS Secrets Manager, Google Secret Manager, Azure Key Vault — inject secrets as environment variables at container/instance startup.
- PaaS: Vercel, Render, Fly.io, Heroku all have a secrets/env dashboard that keeps values encrypted at rest and out of your deploy logs.
- Containers: avoid baking secrets into Docker images with
ENVorARG— both are visible viadocker history. Use runtime injection (--env-file, Kubernetes Secrets, or a sidecar) instead.
# Bad: baked into the image layer
docker build --build-arg ANTHROPIC_API_KEY=sk-ant-xxxx .
# Better: injected at runtime
docker run --env-file .env.production myapp
Key Scoping and Rotation
Not all keys need the same level of access. If you're building an application on top of Claude, consider issuing separate keys per environment and per team member rather than sharing one master key across your whole organization. This limits blast radius if one key leaks — you revoke that single key instead of rotating everything downstream.
SubToAPI applies this directly: each application gets its own sub_live_... key from the dashboard, usage is tracked per key, and you can revoke an individual key without regenerating credentials for your whole team. That makes it easier to scope access by project, by environment, or by developer, and to set up team seats where each member works under their own key rather than a shared secret passed around in a chat thread. See /docs/quickstart for how key issuance works end to end.
Rotation practices worth adopting regardless of provider:
- Rotate keys on a fixed schedule (quarterly is reasonable for most teams).
- Rotate immediately if a key appears in a log, a public repo, a bug report, or a support ticket.
- Keep the old key active for a short overlap window when rotating in production, so in-flight requests don't fail mid-deploy.
- Audit which keys exist and who/what uses each one — unused keys are the easiest ones to forget and the easiest ones to leak.
Logging and Error Handling
A surprising number of leaks come from well-intentioned debug logging. Two concrete habits prevent this:
- Never log full request headers in production. Redact
Authorizationand any custom auth header before logging. - When catching errors from an API client, don't log the entire request config object — it often includes the key used to construct the client.
try {
const response = await fetch('https://api.subtoapi.app/v1/messages', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.SUBTOAPI_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
} catch (err) {
// Log the error message, not the request config
console.error('Request failed:', err.message);
}
For full request/response shapes, see /docs/messages, and for streaming-specific error handling, /docs/streaming.
Checklist
.envin.gitignorefrom commit one- Secrets stored in your CI provider's encrypted secrets store, not in YAML
- No secrets baked into Docker images or committed config files
- Separate keys per environment and per developer where possible
- Scheduled rotation plus immediate rotation on suspected exposure
- Redacted logging for anything touching
Authorizationheaders
questions
Do I need a different Claude API key for each environment? Yes. Using the same key in dev, staging, and production means a leak in a lower-security environment (like a local laptop) compromises production access too. Separate keys let you revoke one without affecting the others.
Is it safe to use environment variables if my server is compromised? Not fully — environment variables are readable by anything with process access on that machine. They protect against accidental leaks (commits, logs, config files) but not against a fully compromised host. Pair them with a secret manager and least-privilege key scoping.
How often should I rotate API keys? A quarterly schedule is a reasonable baseline for most teams, but rotate immediately and without waiting for the schedule if a key shows up anywhere it shouldn't — a log file, a public repo, a shared screenshot, or a support ticket.