ASK KNOX
beta
LESSON 607

Secrets Hygiene: When Agents Touch Your Env Files

Agents write output — logs, config files, transcripts, documentation — and they reproduce what they saw in context. A secret that passes through an agent's context window is one step from a git commit, a log file, or a PR diff unless specific gates block it.

9 min read·Agent Infrastructure Security

The Incident That Started as a Billing Anomaly

An unattended agent job was granted an API key for a third-party platform to enable a content-generation task. The job ran on a schedule, completed its work, and logged its output. Somewhere in that process — in an error message, in a session transcript, in a config example the agent wrote to help document its own setup — the key ended up in a place with broader read access than the secret store.

An attacker found it. They did not announce themselves. They made API calls with the key. The first sign of the breach was not a security alert. It was a billing anomaly: tens of millions of tokens of platform credits drained before anyone looked at the invoice.

This is the billing-as-security-surface failure mode. The secret exposure was the vulnerability. The billing spike was the only indicator, and it lagged the attack by enough time for significant damage to occur. A spend alert with a low threshold would have surfaced the anomaly within hours instead of days. Rotation on suspicion — before confirming — would have limited the blast radius to what the attacker could do in a single session.

This lesson is about preventing that first step: the secret leaving the controlled environment.

Why Agents Are a Different Kind of Risk

Secrets hygiene in traditional software is mostly a discipline problem: developers sometimes commit credentials by accident, and tools like git-secrets and secret scanning prevent the most common mistakes. The rules are well-understood: keep secrets out of source code, use environment variables, rotate on exposure.

Agents introduce a new failure mode that is not about developer discipline. It is about what agents do by nature.

Agents write output. They generate documentation, configuration examples, code, logs, and reports. They write to files, to APIs, to databases. They produce transcripts of their own reasoning.

Agents reproduce what they see. A language model's output is shaped by its context. If an API key was in the context when the agent generated a configuration example, the example may contain the key. Not because the agent made a mistake — because accurately reproducing context is what models do.

Transcripts are log files. A session transcript captures the full context the agent operated on. Everything the agent saw — tool results, file contents, env values that were passed as context — is in the transcript. This is valuable for debugging. It is dangerous if transcripts are stored in locations with broad read access or committed to source control.

The flow diagram above maps the five leak paths in detail. The root cause is the same for all of them: a secret that was meant to stay in a controlled environment found a route to an accessible one.

The Four Rules

Once the leak paths are clear, the rules follow directly.

Rule 1: Secrets are injected at runtime, never written by agents.

The injection path and the agent's write access must be separated. Secrets travel from the secret manager (or CI secret store) into the process's environment at startup. The agent never sees the injection mechanism; it sees only the value in memory. The agent's file-write tools must not have access to the paths where secrets live or the patterns that match secret file names.

Concretely: .env* paths should be in the agent's deny-listed write paths at the tool-policy level. This is not an instruction to the agent — it is a constraint enforced by the tool implementation itself. The file-writing tool returns an error if the target path matches the deny pattern, regardless of what the agent requested.

Rule 2: .env* files belong in .gitignore and in the agent's path deny-list.

These two controls are redundant by design. .gitignore prevents the developer from accidentally staging the file. The agent's path deny-list prevents the agent from writing to the file. Neither alone is sufficient: git operations can be forced, and agent tool policies can have gaps. Defense in depth means both are in place simultaneously.

Rule 3: Pre-commit hook plus CI gate, both exiting nonzero on any secret-shaped pattern.

A pre-commit hook catches accidental commits before they reach the remote. A CI gate enforces the rule at merge time regardless of how the commit was created — force-push, direct commit, GitHub UI edit. Neither gate is optional.

The gate must scan for patterns, not specific values. Values change; patterns persist. A gate that knows to look for key = "sk-..." shapes, PEM private key headers, and token-assignment patterns catches unknowns as well as knowns. The gate should also maintain an allowlist of recognizably safe patterns — placeholder shapes like <your-api-key> that appear in documentation and should never trigger the check.

Rule 4: Rotate on any suspected exposure. Treat git history as permanent.

A secret that may have been exposed should be rotated immediately, before confirming the exposure. The cost of an unnecessary rotation is low: a few minutes of reconfiguration. The cost of waiting to confirm is measured in the damage window.

Git history is permanent in the sense that matters: if a secret was committed at any point in the history of a repository, anyone with access to the repository can retrieve it using standard git commands. Deleting the file does not help. The correct response is to rotate the secret (rendering the old value useless), and then optionally rewrite history using a dedicated tool to remove the value from the historical record. Rotation is the primary fix; history rewriting is hygiene.

The Maturity Ladder

Not every team can jump to the fully gated posture on day one. The maturity ladder shows where most teams start, where the gaps are at each level, and what it takes to reach the minimum acceptable state for a production agent fleet.

The target is L3 as a minimum: runtime injection from a proper secret manager, agent path deny-lists on .env* patterns, active pre-commit and CI scanning, and access-controlled transcripts. L4 adds per-agent credentials and automatic rotation — the blast radius of a compromise is limited to one agent's scope, and rotation is an operational procedure rather than an emergency response.

Most teams that have been operating agents for a while discover they are at L1 or L2: secrets are kept out of source control through developer discipline, but agents have not been explicitly denied write access to secret paths, transcripts are stored alongside application logs, and secret scanning is either absent or only on the CI side without a pre-commit layer.

The jump from L2 to L3 is primarily a configuration change, not a rewrite. Add the agent path deny-list to the tool-policy configuration. Add a pre-commit hook to the repository. Lock down the transcript storage location. Configure spend alerts on all keys used by agents. None of these requires changing the agent's logic or the application architecture.

What the Gate Looks Like in Practice

The BuildChallenge for this lesson is the pre-commit gate itself. The gate is a bash script that runs before every commit, scans the staged files using git show :"$file" (the staged content, not the working copy), and exits nonzero if it finds any pattern matching a credential.

The critical implementation detail: scan the staged content, not the working tree. A developer might have a real key in a local .env file that is not staged — the hook should not flag it. A developer might have staged a file that has a real key but a clean working copy — the hook should flag it. The difference is git show :"$file" versus cat "$file".

The allowlist is equally important. A security gate that blocks documentation examples using realistic-looking placeholders will be disabled by frustrated developers. The gate must recognize safe placeholder shapes — <your-api-key>, YOUR_KEY_HERE, REPLACE_ME — and pass them through. Teaching examples should use these shapes by design.