GUIDE · CONCEPT 9 min ·

Runtime secret injection explainedWhat it is. Why boot-time loading fails. What to do.

Loading secrets at boot means they sit in memory, in environment dumps, and in your agent's context for the entire session. Injecting at runtime means they arrive when a command needs them and disappear when it exits.

TL;DR· the answer, in twenty seconds

What: Most projects load secrets once at startup, where they sit in ambient environment variables for hours. Any tool, agent, or subprocess that reads your environment inherits them. Runtime injection flips this: the value arrives inside a specific process at exec time and dies with that process.

Fix: Pick a delivery mode that matches your toolchain. Env var injection at exec (env KEY=value cmd) works everywhere. Temp file with mode 0600 works for tools that only accept file paths. Named-pipe streaming works for commands that read stdin. None of them require a secret manager to start.

Lesson: The attack surface for a secret scales with its lifetime. Shorter lifetimes mean smaller blast radius regardless of what tool leaked it.

Your typical backend project sources a .env file at startup. The database URL, the Stripe key, the S3 credentials, all of them land in the process environment before the first request arrives. They stay there until the process stops. That window is often measured in days.

Coding agents inherit this pattern. When Claude Code, Codex CLI, or Aider launches inside your shell, it inherits whatever your shell exported. Knostic's February 2026 disclosure showed that Claude Code wrote those inherited values into settings.local.json. Every agent session logged your secrets into a predictable file, and npm publish dutifully included it. The agent did not misbehave. It read ambient state, which is exactly what processes do.

The problem is not that agents are curious. The problem is that secrets with long lifetimes accumulate risk at every point of contact: log files, swap partitions, /proc entries, state files, core dumps. Runtime injection is the practice of shrinking that lifetime to the duration of one command.

What you need to know in 60 seconds

  • Loading a secret at boot means every subprocess you spawn for the rest of the session can read it from /proc/$PID/environ or via ps if you pass it as an argument.
  • Runtime injection delivers the value directly to one process at exec time. The value is not present in your shell's exported environment before or after.
  • Three delivery modes cover most cases: env var at exec, temp file with restricted permissions, named pipe or stdin streaming.
  • Each mode has failure modes. None of them is a complete solution. Defense in depth still matters.
  • The point of injection is not secrecy during transit. It is limiting the time window in which a compromise can reach the secret.

Three ways to inject at runtime

Env var injection at exec

The most common form. You pass the secret as part of the command invocation rather than exporting it into your shell.

# Don't do this (exports into ambient shell env for the whole session)
export STRIPE_KEY=sk_live_...
stripe charges list

# Do this instead (key exists only for the duration of stripe charges list)
STRIPE_KEY=sk_live_... stripe charges list

Or with a wrapper that fetches from a vault:

STRIPE_KEY=$(vault kv get -field=key secret/stripe) stripe charges list

The variable exists in the child process's environment. It is not in your shell's exported environment. Any subsequent command you run in the same shell does not see it.

Failure modes to know: the value is still readable from /proc/$PID/environ by any process running as the same UID for the lifetime of the stripe process. If stripe charges list spawns its own subprocesses, they inherit the value. If you pass the key as a positional argument rather than an environment variable, ps aux will expose it to any user on the system during execution.

Use this mode for: anything that reads standard environment variables, which is most tools.

Temp file with mode 0600

Some tools accept a file path instead of an env var. kubectl can read a kubeconfig from a path you specify. git can read credentials from a file. AWS CLI accepts --credentials-file.

Write the secret to a temp file, run the command, delete the file:

SECRET_FILE=$(mktemp)
chmod 0600 "$SECRET_FILE"
echo "STRIPE_KEY=sk_live_..." > "$SECRET_FILE"

stripe --config "$SECRET_FILE" charges list

rm -f "$SECRET_FILE"

Mode 0600 means owner read/write only. Root can still read it, and so can any process running as the same UID. A crash between the write and the rm leaves the file on disk. If your system uses a temp filesystem in RAM (/dev/shm on Linux), the file never touches disk at all. If your /tmp is on the main filesystem, a swap event can capture the content even after deletion.

Use this mode for: tools that refuse env vars and require a file path. Put the temp file in /dev/shm on Linux if you care about swap exposure.

Named-pipe or stdin streaming

The cleanest from a persistence standpoint. The secret never touches disk. The process reads it from a file descriptor that disappears when the command exits.

# Using process substitution (bash/zsh)
some-tool --key-file <(echo "sk_live_...")

# Using a named pipe explicitly
mkfifo /tmp/key_pipe
chmod 0600 /tmp/key_pipe
echo "sk_live_..." > /tmp/key_pipe &
some-tool --key-file /tmp/key_pipe
rm /tmp/key_pipe

Or if the tool reads from stdin:

echo "sk_live_..." | some-tool --key-from-stdin

Failure modes: few tools actually accept file descriptors or stdin for credentials. The ones that do (some Kubernetes tooling, some database CLIs) are worth the extra plumbing. The ones that don't will silently ignore the option or error out in confusing ways. Test this before wiring it into a pipeline. Named pipes also block until both ends connect, which can cause a script to hang if the consumer never reads.

Use this mode for: kubectl with credential plugins, database clients that accept stdin password input, any tool that explicitly documents stdin or file-descriptor credential input.

What runtime injection actually buys you

The core claim is simple: a secret that exists for 200 milliseconds has a smaller attack surface than one that exists for 8 hours.

The Knostic February 2026 disclosure is a clean example. Claude Code captured environment variables and wrote them to disk because they were in the ambient shell environment for the entire session. If those values had been injected per-command, the agent session would have seen nothing worth capturing. The settings.local.json file would have been clean.

The same logic applies to swap. Linux and macOS can swap memory pages to disk during normal operation. A secret that lives in a process for 8 hours has many opportunities to land in swap. A secret that lives for 200 milliseconds has fewer, though not zero. Encrypted swap (on by default in macOS with APFS) reduces this further but does not eliminate it.

Core dumps are the other failure mode. If a process crashes and your system is configured to write core dumps, the dump includes process memory, which includes environment variables. A tighter lifetime window reduces the probability that a dump captures a specific secret, though a crash during the critical 200-millisecond window can still do it.

The practical outcome: runtime injection does not make secrets invisible. It narrows the window in which a failure can capture them.

What runtime injection does not fix

A common mistake is treating injection as a complete answer. It is not.

/proc/$PID/environ is still readable by processes running as your UID. On Linux, any process you own can open /proc/<pid>/environ for a process you own and read every environment variable for the lifetime of that process. A malicious dependency in the same Node or Python process can read this. Runtime injection limits which processes have a readable environ entry, not whether those processes can be inspected.

Process inheritance still happens. If your injected command spawns subprocesses (most do), those subprocesses inherit the environment. A package that shells out in a postinstall script runs as the same UID and inherits whatever the parent process had. Supply-chain attacks exploit this.

Swap exposure shrinks but does not disappear. The 200-millisecond window is better than 8 hours. It is not zero.

Argument leakage is unaffected by the injection mode you choose. If you pass the secret as a command-line argument (--api-key sk_live_...) rather than through an environment variable or file, ps aux will show the value to any user on the system during execution. Use env vars or file paths, not positional arguments.

The upside of knowing the limits is knowing what else you need. Process isolation (containers, separate UIDs), encrypted swap, core dump controls, and supply-chain auditing fill in the gaps injection leaves open. Together they form a workable defense.

The thing people get wrong about "runtime"

"Runtime" is an overloaded word. In one sense, everything that runs after your app starts is "runtime." In that sense, loading a .env file at application start is already runtime injection: the values arrive when the process needs them to boot.

That framing obscures what actually matters. A value that exists for the lifetime of an interpreter session is fundamentally different from a value that exists for the duration of one command. Both are "runtime" in the loose sense. Only the second one limits blast radius.

The boundary that matters is not boot vs. runtime. It is session-lifetime vs. call-lifetime.

A session-lifetime secret in a coding agent session can be captured by the agent's context window, its state files, its log output, and any tool it spawns across the entire session. Knostic's work and the GitGuardian 2026 State of Secrets Sprawl (which tracked an 81% YoY increase in AI-service token leaks) both point to the same failure: agents reading ambient environment state that was never meant to be read across a session boundary.

A call-lifetime secret is scoped to one subprocess. The agent cannot capture it from the environment because it was never in the agent's environment. The state file the agent writes holds nothing. The next tool that reads state finds a clean slate.

The terminology shift matters for how you talk to teammates. "Don't put secrets in .env" is easy to dismiss. "This value has a 6-hour lifetime and gets inherited by every subprocess" gives someone a concrete surface to reason about.

A checklist you can paste into a PR

## Runtime injection audit

- [ ] No secrets exported in ~/.zshrc, ~/.bashrc, or ~/.profile
- [ ] No long-lived secrets in .env files sourced by agent sessions
- [ ] Coding agent launched with env -i PATH=$PATH HOME=$HOME to strip ambient env
- [ ] Per-command injection in place for any call that needs a secret
- [ ] Temp files use mktemp + chmod 0600, deleted immediately after use
- [ ] No secrets passed as positional CLI arguments (use env vars or file paths)
- [ ] Subprocesses that do not need a specific secret do not receive it (pass selectively)
- [ ] Core dump generation reviewed and disabled or encrypted if secrets are in scope
- [ ] Encrypted swap confirmed on (macOS: default with APFS; Linux: check /proc/swaps)
- [ ] Supply-chain check: postinstall scripts in dependencies audited for env reads

What this means for your stack

The goal is call-lifetime secrets. Env var injection at exec gets you most of the way there for most tools. Temp files at 0600 cover the remainder. The hard part is not the mechanics but the discipline of never exporting a secret into your shell where it persists across commands.

hasp is one working implementation. curl -fsSL https://gethasp.com/install.sh | sh, hasp setup, connect a project, and the next agent session receives secrets through its three delivery modes (env var at exec, temp file at 0600, temp dotenv outside the repo root). The session-lifetime window collapses to zero: nothing ambient to capture, nothing ambient to persist to a state file. Source-available (FCL-1.0), local-first, macOS and Linux, no account.

The principle holds regardless of tooling. Keep secrets out of your shell's exported environment. Inject them per call. Treat session-lifetime secrets as unacceptable in any context where an agent or dependency can read your environment.

Sources· cited above, in one place

NEXT STEP~90 seconds

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.
→ okvault unlocked · binding ./api
→ okgrant once · pid 88421
→ okagent never read

macOS & Linux. Source-available (FCL-1.0, converts to Apache 2.0). No account.

Browse all clusters· eight threads, one index