Env vars are not secrets managementWhat they are. What's missing.
12-Factor App said 'config in env.' That was 2011, and it solved the right problem for that year. Fifteen years later, the advice ships credentials to every child process your agent spawns.
-
01
Cause
export in shell rc
STRIPE_KEY lives in every child process
~/.zshrc · persistent -
02
Mechanism
agent inherits env
all vars, no scope, no grant
spawn · no filter -
03
Outcome
state file / log captures
value persists past session end
Knostic · Feb 2026
TL;DR· the answer, in twenty seconds
What: Environment variables are a delivery mechanism, not a secrets management layer. They carry no access control beyond Unix UID, no audit trail, and no scope. Every child process your shell spawns gets them all.
Fix: Stop exporting long-lived secrets in shell rc files. Use a store that encrypts at rest, scopes per-process, and records each grant. Inject values at exec time into the process that needs them, then let the value die with the process.
Lesson: Secrets management means store, scope, rotate, audit, and deliver. Env vars handle only the last step. Treating delivery as the whole system leaves four gaps an agent runtime will fall through.
In February 2026, Knostic found environment variables including API keys inside settings.local.json files shipped inside published npm packages. Claude Code had written them there during agent sessions. The keys were real. The packages were public. The bots that watch the npm release feed had already scraped them.
The post-mortem conversation blamed Claude Code. The actual failure started with export STRIPE_KEY=sk-... in a .zshrc file, possibly years earlier.
That line looks like secrets management. It is not. It is one step of one version of secrets delivery, with nothing else underneath it.
Nobody named the gap, so it grew into the default. This article names the gap.
The short version
- Env vars are process-inherited key-value string pairs. No encryption. No access control beyond Unix UID. No expiry. No revocation.
- Secrets management requires five things: a store, a scope, a rotation mechanism, an audit trail, and a delivery mechanism. Env vars supply only the last.
- The path
/proc/$PID/environon Linux exposes the full environment of a running process to any caller with the same UID. No root required. - Shell rc files persist secrets across every session, every project, and every child process you ever launch.
- Agents treat the inherited environment as input. When they write state files, crash dumps, or debug output, those inputs go with them.
What env vars actually are
An environment variable is a string key-value pair inherited by child processes. When your shell forks a subprocess, the child gets a copy of the parent's environment unless you strip it. That is the entire mechanism.
Two properties matter here.
First, scope is UID-wide, not process-specific. On Linux, /proc/$PID/environ lists every environment variable for a running process. Any process running as the same UID can read it without privilege escalation. On macOS, sysctl kern.procargs2 exposes process arguments and environment similarly. You do not need root. You need the same user.
Second, env vars passed as arguments to a command appear in ps aux output until the command reads them. On older kernel versions this window was long enough to matter. On modern systems the window is shorter but not zero. Some CI systems log env or printenv in debug steps, putting the full environment into the job log for whatever the log retention period is (90 days for GitHub Actions by default).
Neither of these is a design flaw in Unix. They are the known behavior of a mechanism built for configuration, not secrets. The 12-Factor App methodology called for config in env because it solved a real 2011 problem: deployers were hardcoding config values into source code and struggling to manage different settings per environment. Env vars solved that. Nothing in the original guidance claimed they were a secrets management layer. That inference happened later, quietly, as "don't put secrets in source code" got compressed into "put secrets in env."
The compression dropped three words: "instead of source code." It kept only "put secrets in env." The advice stopped being comparative and became prescriptive.
What secrets management actually requires
A secrets management system has five components. Miss one and the system has a gap.
A store holds secrets encrypted at rest. Not a text file, not a shell profile, not a .env file in a repo. Encrypted, with a defined key derivation scheme and a clear answer to "where does the key live."
Scope means a secret is readable by specific processes or roles, not by everything running as a given UID. Scope can be per-process, per-project, per-role, per-time-window. "Accessible" is not a binary property tied to the Unix user.
Rotation means when a credential changes, the store updates and downstream consumers receive the new value without each one needing a manual config change. Rotation is not an emergency procedure. It is a scheduled operation that should be boring.
An audit trail means every read of a secret produces a record: which process, which user, which secret, at what time. That log is append-only and verifiable. When someone asks "did the agent read the prod database password between 2pm and 4pm Tuesday," the answer comes from the audit log in seconds, not from grepping bash history.
Delivery is the mechanism that hands the secret to the process that needs it. This is where env vars live. Delivery is one step in the five-step system, and the least differentiating one.
HashiCorp Vault, AWS Secrets Manager, and 1Password Secrets Automation provide all five. A .zshrc export provides delivery, partially, with none of the others.
hasp is one local implementation of the full model: an Argon2id-encrypted vault handles storage, per-project bindings handle scope, and hasp run delivers values into a single child process at exec time and revokes them when it exits. That delivery-plus-scope combination is what stops the agent from writing a credential to a state file it never received.
How env keeps getting called secrets management
Nobody named the gap, so the gap stayed invisible.
The conversation around secrets usually focuses on "don't hardcode values in source." Moving secrets out of source and into env solves the source-code problem. It does not solve the storage problem, the scope problem, the rotation problem, or the audit problem. Because it solves the most visible problem, the conversation stops there.
Developers learn "use env vars, not hardcoded values" in onboarding docs or a security training. They implement it and consider the box checked. The training did not mention what secrets management requires beyond delivery.
.env files appear in repos, then get added to .gitignore. This is better than committing them, but a .env file is a plaintext file on disk. It has no encryption, no scope, and no audit trail. It is source-code storage with one layer of indirection removed.
Secrets rotate manually, when remembered, in response to incidents. Rotation is not systematic. The question "when did we last rotate the prod Stripe key" has no reliable answer.
The GitGuardian State of Secrets Sprawl 2026 report found AI-service token leaks up 81% year over year, with AI-assisted commits leaking around twice as often as baseline. That acceleration does not come from developers becoming worse at security. It comes from more secrets in more processes with more agents reading them, all while the underlying storage and scope model stayed the same.
The part people push back on
The common objection: "But I use a secrets manager. I pull values into env at deploy time via Vault/Chamber/Doppler. That's different."
It is different. That pattern uses a proper store (the secrets manager), adds audit on the read from the store, and limits the blast radius to the deployment context. It is a real improvement.
The remaining gap is what happens after the value enters the environment. Once a secret lands in a process environment at deploy time, it inherits all the same properties described above: UID-wide scope, readable via /proc/$PID/environ, copyable by any child process. If that deploy environment runs an agent, the agent inherits the value.
A secrets manager at the top of the pipeline does not protect you from what the pipeline hands to agents at the bottom.
The Knostic February 2026 disclosure showed exactly this. Claude Code was not circumventing any security tool. It was reading the environment it inherited, the same environment the deployment process or the developer's shell handed it. The agent wrote what it received. The state file shipped what the agent wrote.
The agent-era failure modes are different in kind
The original 12-Factor env var guidance assumed a relatively stable process tree. A web app forks workers. The workers handle requests. The env is set at deploy time and stays constant. Nothing in that tree decides to write the env to a file and then email it to someone.
Agents change the assumption.
An agent spawns as a child process and inherits your full environment. If you have forty environment variables exported in your shell, the agent gets all forty, including the ones from other projects and other clients. Not because the agent is malicious, but because that is how Unix process inheritance works.
Knostic's February 2026 finding was that Claude Code, doing normal work, wrote a state file that included environment variables it had seen. The agent was not trying to leak secrets. It was trying to remember context. Those two goals collide when the context contains secrets.
Crash dumps include environment data. Many crash reporting libraries capture the process environment on exception. A debug step that runs printenv or env in CI writes the full environment to the job log. A prompt to an LLM that includes "here's my current environment for debugging" copies the values into the model's context window, which may persist in conversation history.
Each of these is a normal developer behavior. None is an attack. Together they map the surface area of a system where secrets live in ambient environment variables and every process that spawns reads them without asking.
A checklist for your current setup
## Env var audit
- [ ] Grep ~/.zshrc, ~/.bashrc, ~/.profile for export statements containing keys, tokens, passwords
- [ ] For each: does this need to be available to every child process, or only to specific commands?
- [ ] Check ~/.env, .env, .env.local files in repos for plaintext secrets
- [ ] Run: grep -r "export.*KEY\|export.*SECRET\|export.*TOKEN\|export.*PASSWORD" ~/.zshrc ~/.bashrc ~/.profile
- [ ] Check CI job logs for debug steps that print environment (search for "printenv" or "env" in workflow YAML)
- [ ] Confirm crash reporting config does not capture env vars (check Sentry, Bugsnag, Datadog config)
- [ ] Verify that agent tools (Claude Code, Cursor, Codex) do not inherit secrets they do not use
- [ ] Document when each long-lived credential was last rotated
- [ ] Confirm there is an audit log that answers "who read this secret and when"
What this means for your stack
Stop exporting long-lived secrets in shell rc files. That pattern predates agents, crash dump collectors, and CI pipelines that log their full environment. It solved the 2011 problem of secrets in source code. It creates the 2026 problem of secrets available to every child process indefinitely.
The replacement model: a local encrypted store holds credentials. An agent requests access to a specific secret by name for a specific session. The broker injects the value into that one child process at exec time and the value disappears when the process exits. Nothing persists to a state file. Nothing lands in a crash dump. The agent sees a reference, not the value. An HMAC-chained audit log records each grant.
hasp is one working implementation. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, hand the next session a reference instead of a key. Source-available (FCL-1.0), local-first, macOS and Linux, no account.
Env vars as a delivery mechanism are fine. Treating delivery as the whole system leaves a store, a scope, a rotation mechanism, and an audit trail unbuilt. Those four gaps were tolerable in 2011. They are not tolerable when every agent you spawn inherits your shell's ambient state.
Sources· cited above, in one place
Stop handing the agent your real keys.
hasp keeps secrets in one local encrypted vault, brokers them into the child process at exec, and never lets the agent read the value.
- Local, encrypted vault — no account, no cloud, no telemetry by default.
- Brokered run — agent gets a reference, the child process gets the value.
- Pre-commit + pre-push hooks catch managed values before they ship.
- Append-only HMAC audit log answers "did the agent touch the prod token?" in seconds.
macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.