Credential Boundaries: What an Agent Should Never Be Able to See
'Use environment variables, not literals' addresses where a secret is stored. It says nothing about what happens once the agent can read it — which is the part that matters.
The credential question, asked properly
Most discussions of agent credentials stop at "use environment variables, not literals". That is necessary and nowhere near sufficient, because it addresses only where a secret is stored, not what happens once the agent can use it.
Three separate questions matter, and they have different answers:
- Which credentials can the agent see?
- Which can it use without seeing?
- What can each one reach if the session is steered by someone else?
Seeing versus using
The distinction is the most useful idea here and is routinely missed.
An agent that can read DATABASE_URL from the environment holds
a value that can appear in its output — in a log line, an error message,
a commit, a summary. An agent that can run ./query.sh, where the
script reads the credential and the agent never sees it, can achieve the same
task without ever holding the secret.
| Pattern | Agent sees secret | Leak paths |
|---|---|---|
| Secret in env, agent reads env | Yes | Logs, output, commits, tool calls, transcripts |
| Secret in env, wrapper script uses it | No | Only what the wrapper prints |
| Secret in a vault, agent has vault token | Yes, on fetch | Same as the first, plus the token itself |
| Broker holds secret, agent calls broker | No | Only what the broker returns |
Rows two and four are structurally better, and the improvement is not marginal. A secret the agent never holds cannot be exfiltrated through the agent, regardless of how convincingly it is instructed to do so.
#!/bin/bash
# The agent calls: ./query.sh "SELECT count(*) FROM orders"
# It never sees DATABASE_URL.
set -euo pipefail
case "$1" in
*[Dd][Rr][Oo][Pp]*|*[Dd][Ee][Ll][Ee][Tt][Ee]*|*[Tt][Rr][Uu][Nn][Cc]*)
echo "Refused: destructive statement." >&2; exit 1 ;;
esac
psql "$DATABASE_URL" -c "$1" 2>&1 | head -100
Two benefits, not one. The agent never holds the credential, and the wrapper is a natural place to put constraints — here a refusal on destructive statements and a cap on output size. Both would be impossible if the agent talked to the database directly.
Every constraint you can express in a wrapper is a constraint enforced by code rather than requested of a model. When choosing between "tell the agent not to" and "make the wrapper refuse", the wrapper wins every time.
Scope, not convenience
The second question is what each credential reaches. The default in most setups is a token with far more scope than the task needs, because that is the token that already existed.
- Read-only by default. An agent investigating a bug needs read access. Write access is a separate grant for a separate task.
- Non-production by default. Most agent work does not need production. Point it at staging and make production an explicit, uncomfortable-to-arrange exception.
- Narrow resource scope. A token for one repository, one bucket, one database — not an organisation-wide token that happens to be handy.
- Short-lived. A credential that expires in an hour bounds the damage from a leak. A long-lived personal access token does not.
- Separately identifiable. Agents get their own credentials, not a human's. When the audit log shows an action, you want to know which principal took it.
That last point is worth insisting on. An agent operating under a developer's personal token produces an audit trail that is actively misleading — every action attributed to a person who may not have been at their desk.
What must never be in context
Some things should never reach the window under any circumstances, regardless of how the session is structured.
| Never | Why |
|---|---|
| Production database credentials | Blast radius is unbounded |
| Cloud root or org-admin keys | Can grant themselves anything else |
| Signing keys, release credentials | Enables supply-chain compromise downstream |
| Customer PII beyond the immediate task | Regulatory exposure, no upside |
| Other people's credentials | Destroys attribution entirely |
The paths a secret actually escapes through
Worth enumerating, because the obvious one — the agent printing a credential in its reply — is neither the most likely nor the most damaging.
| Path | How it happens | Persistence |
|---|---|---|
| Written into a committed file | Agent adds a config with a real value while debugging | Permanent, in git history |
| Sent as a tool argument | Passed to a third-party MCP server as a parameter | In that server's logs, outside your control |
| Echoed in an error | A failed command prints the full connection string | In the transcript, and in CI logs if it ran there |
| Included in a summary | Agent quotes an env dump when explaining a failure | Transcript, and anywhere the summary is pasted |
| Committed to a test fixture | A real key used to make a test pass | Permanent |
The second row is the one that should worry you most and gets the least attention. A credential passed as an argument to a third-party server has left your infrastructure entirely. You cannot scan for it, you cannot rotate it out of their logs, and you will not know it happened. This is the concrete reason the wrapper pattern matters: a secret the agent never holds cannot be passed as an argument to anything.
The blast radius question
The third question — what can each credential reach if the session is steered by someone else — is the one to actually sit down and answer, credential by credential. Assume the agent is doing exactly what an attacker wants and ask what that gets them.
| Credential | Worst realistic outcome | Acceptable? |
|---|---|---|
| Read-only staging DB | Test data disclosed | Usually yes |
| Repo write, protected main | Bad code in a PR, caught in review | Yes, if review is real |
| Repo write, no protection | Direct push to main, deployed | No |
| Cloud credential with IAM rights | Privilege escalation to anything | No |
| Package registry publish token | Malicious release to your users | No |
Row two is the interesting one, because it is the whole argument for branch protection stated in security terms. With protected branches and required review, a fully steered agent still cannot ship code unilaterally — it can only produce a pull request a human must approve. Without them, the same credential is a direct path to production.
Detecting a leak after the fact
Assume something will end up in a transcript eventually, and arrange to find out. Two mechanisms, both cheap:
Canary credentials. Place a credential that does nothing except alert when used. If it is ever exercised, something read it out of a place it should not have.
Scan transcripts. If your tooling retains session logs, run the same secret-scanning rules over them that you run over commits. The patterns already exist; pointing them at a second corpus is nearly free.
#!/bin/bash
# Point existing secret-scanning rules at agent session logs.
gitleaks detect --no-git --source ./agent-logs \
--report-path leaks.json --redact
Most teams scan their commits and have never scanned a transcript, despite the transcript being the place a secret is most likely to appear first.
Separate seeing a credential from using one — a wrapper script the agent calls is strictly better than a secret it can read, and gives you a place to enforce policy in code. Scope every credential to read-only, non-production and short-lived by default, give agents their own identity, and scan transcripts with the same rules you point at commits.