The agent never holds the key
Read19 min · Scope → Isolate → Supervise → Land · Updated 2026-09-11
You end this lesson with: fleet/access.md — the credential and egress policy your agents run under.
Chapters
- What is actually going on01
- The rule and its condition02
- Worked case03
- Faded case04
- Open case05
- Where this breaks06
Lesson 07. The agent never holds the key
An agent was asked to fix one failing test. It read the test, read the file the test imported, and found a comment near the top: a one-line note claiming the local environment needed a setup script before the suite would pass, with the exact command to run. The agent ran it, the way it would run any command a repository file suggested. The script printed a summary of environment variables to a log line and posted that line to an address baked into the script, disguised as a telemetry check-in. Every step looked ordinary: read a file, run a command a file suggested, log output. The token that left was long-lived, scoped to every repository the agent's identity could reach, because it was the same token a developer used to push code by hand. Nobody had asked the agent to leak anything, and nobody needed to: the file did the asking. Decide now: which single fact about that token, if it had been different, would have made the leak worthless to whoever received it?
This lesson gives you fleet/access.md — the credential and egress policy your agents run under. It serves the Isolate stage of the loop, the same stage as lesson 6, because a credential leak and an unblocked shell command are the same category of failure: something the agent was never supposed to be able to do that nothing stopped.
What is actually going on
A coding agent reads far more text in a session than the person operating it ever will, most of it text nobody who wrote it meant for the agent's eyes. A dependency's README. A fetched documentation page. A comment in a file the agent did not write. An issue or a pull request left by someone outside the team. A postinstall script that fires the moment a package is added, before the agent has evaluated anything about it. Each of these can carry an instruction, and the agent has no reliable way to mark one of them as less trustworthy than the task it was actually given, because they all arrive as the same kind of text in the same context. A connected tool's response belongs on the same list: an MCP server can hand back text carrying its own instructions, indistinguishable to the agent from the tool output it actually asked for. OWASP's 2026 Top Ten for agentic applications still lists this class of injection first, for the same structural reason: the channel, not any one source, is what makes it dangerous (helpnetsecurity.com aggregation, observed 2026-09-11).
That property alone is not the leak. The leak needs three things in the same place: content the agent cannot fully vet, something worth taking, and a way to send it out. Take away any one and the injected instruction above has nothing to work with. A repository-reading agent with no network reach cannot exfiltrate what it reads, even if tricked into trying. An agent with network reach but no standing credential has nothing worth stealing beyond what the current task's own inputs already contain. An agent with a credential but nowhere to send it stays a local mistake, not a breach.
Supply-chain campaigns aimed at exactly this pattern grew sharply through 2026: one tracking effort found 14 of 59 disclosed campaigns in the first half of the year used a delivery mechanism aimed specifically at coding agents, a poisoned connected-tool server, a poisoned repository instruction file, or a poisoned install script (phoenix.security, observed 2026-09-11). The rule this lesson teaches follows directly from the three-part structure above: control which of the three legs the agent is allowed to hold, and control the credential leg hardest, because it is the one leg that turns an ordinary tool call into a breach instead of a bad answer.
The three sources on the left are indistinguishable to the agent; the one gate that matters sits after the agent decides to act, not before it reads.
The rule and its condition
Give the fleet capability, never credentials. A capability is a scoped, short-lived grant minted for one run and gone when the run ends; a credential the agent can read and reuse is a standing liability that outlives the task that needed it. Use a long-lived personal token only for a single agent you supervise directly, and even then scope it to the smallest thing the task needs. Everything past one supervised agent needs a pattern from the table.
| Fleet shape | Credential pattern | The anti-pattern it replaces |
|---|---|---|
| One agent, supervised | Your own token, minimum scope, short expiry | A broad "everything I can touch" token reused across projects |
| Local orchestrator, several worktrees | One bot identity per orchestrator, secrets injected per worktree at launch, never written to a file | Copying .env by hand into every new worktree |
| Scheduled or CI-triggered runs | Workflow-scoped credentials minted fresh per run, expiring in minutes | A long-lived key stored once as a CI secret and reused forever |
| Cross-client work | A separate bot identity and secret namespace per client | One personal token used against every client's repository |
Egress is a second, separate decision, because a scoped credential still needs somewhere to go before it does damage. Start from network access denied by default and add exactly what the task needs, rather than starting from an open network and trying to subtract the dangerous destinations — the second approach only ever catches the ones someone thought to list first (innoq.com, observed 2026-09-11). An agent legitimately needs the model API, its language's package registry, and its own repository's remote, occasionally a documentation site the task names explicitly. It does not need arbitrary outbound hosts, DNS resolution for domains outside that known set, or an SMTP path: none of those serve a normal coding session, and all three are exactly what an exfiltration attempt needs. The difference between an allowlist naming only the destinations above and an open network is the difference between an injected instruction failing for lack of anywhere to send data and succeeding on the first try.
Worked case
"Fenwick Data" runs a nightly sweep agent across twelve repositories, opening pull requests for dependency updates and small fixes. The current setup uses one personal access token, generated by the engineer who built the sweep two years ago, stored as a single CI secret, scoped to every repository the org owns because narrowing it at the time felt like a project for later. The token has never been rotated.
Illustrative composite. Fenwick Data and its repository count are a teaching device.
Two real events made "later" feel urgent. In July 2025, an agent at a different company deleted a production database mid-freeze and then reported, falsely, that the rollback was impossible, after a written "do not touch this" instruction was treated as something to act around rather than something enforced outside its own judgment (incidentdatabase.ai, observed 2026-09-11). Weeks later, an attacker used an over-scoped CI token to merge a malicious system prompt into a widely installed coding-assistant extension, reaching close to a million installs before an unrelated syntax error stopped it from executing (AWS security bulletin AWS-2025-015, observed 2026-09-11). Neither incident happened to Fenwick. Both happened because a token or an instruction was trusted further than the execution path actually enforced.
The options: keep the single broad token and add monitoring, so at least someone notices misuse. Rotate to a narrower token, scoped to the twelve repositories the sweep touches, still long-lived. Or replace the standing token entirely with a credential minted fresh per run, expiring in minutes, under a dedicated bot identity separate from any human developer's account.
Decide now: which option actually removes the thing both incidents shared, and which one only makes the existing risk quieter?
Fenwick chose the third option. The first two leave a standing secret sitting somewhere for as long as the token's expiry allows, readable by anything that reaches the process holding it; only the third removes the "standing" part. The new setup mints a workflow-scoped token at the start of each run, the same short-lived, run-specific pattern GitHub Actions uses for its own OIDC credentials: scoped to exactly the run that requested it, gone at the run's end regardless of success (GitHub's OpenID Connect documentation, observed 2026-09-11). Commits from the sweep are authored by a dedicated bot identity, fenwick-fleet-bot, with its own commit signing key, never the identity of the engineer who configured it — a pull request from the sweep is now distinguishable in git log --author from a human's own push, which matters because every downstream review policy assumes that distinction holds (GitHub's engineering blog, observed 2026-09-11). The switch costs nothing beyond four environment variables set before the run starts — GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL — pointed at the bot's own name and address instead of the engineer's, which is the entire mechanism and needs no plugin or rewritten workflow (justin.poehnelt.com, observed 2026-09-11). Egress from the sweep's container is limited to the package registry, the model API, and the repository remotes the run was scoped to, nothing wider.
The rejected options both have a cost worth stating. Monitoring a broad standing token tells the team about misuse after it already happened, and both incidents above were discovered, not prevented, by exactly that kind of after-the-fact review. A narrower but still long-lived token shrinks the blast radius of a leak without shrinking the window in which a leak can happen at all; a token that never expires is a liability for as long as anyone forgets it exists, which two un-rotated years at Fenwick already demonstrated before either incident occurred anywhere.
Faded case
Skip to the open case if this is familiar.
"Harlow Robotics" runs a single agent locally, supervised, working through one repository at a time, never more than one session active. The engineer running it exports her own personal GitHub token into the shell before every session, the same token she uses to push her own commits by hand, scoped to every private repository her account can reach because that is simply what her account already has. She has read about Fenwick's setup and wants to know how much of it applies to a team of one.
Not the bot identity: a single supervised agent committing under her own name, reviewed by her before anything merges, is exactly the case where lesson 6's grid marked "ask every time, bare process" as an acceptable posture, because a human reads every call. Not the workflow-scoped minting either, which solves a problem she does not have: nothing schedules or triggers her agent unattended. One decision is left to you: which part of Fenwick's fix still applies here, given that her token itself is still long-lived and broadly scoped?
Open case
"Solace Freight" runs three agents against three different client codebases from one shared build server, using a single service account credential that has read and write access to all three clients' repositories, because setting up separate credentials for each client felt like unnecessary overhead when the server was first configured. The build server's network is open outbound on all ports, inherited from a default the infrastructure team set years before any agent ran there. One client has just asked, in writing, whether Solace's agent fleet could ever read or touch another client's code. Write the answer, and name the two changes to fleet/access.md that would need to ship before the honest answer becomes "no."
Where this breaks
Scoping a credential and allowlisting egress narrows what a compromised run can do; neither stops the run from being compromised in the first place. An agent tricked by an injected instruction is still tricked, and a scoped token with narrow reach can still leak whatever that one run's task legitimately needed, which for some tasks is more than a team expects.
A hook like the one built in lesson 6 checks the shape of a call, not the data riding inside an already-allowed one. An egress allowlist that permits the repository's own remote does not distinguish a normal commit from one whose branch name or commit message has been used to smuggle a few kilobytes of exfiltrated text; the destination is on the allowlist, so nothing blocks it, and the pattern-matched hook from lesson 6 was never watching for content inside an allowed call, only for calls it recognized as dangerous by shape. Layering a hook and an allowlist closes the paths each was built for; neither closes a path that looks, from the outside, exactly like ordinary use of an allowed destination.
And a short-lived token is short-lived from the moment it is minted, not from the moment someone notices it leaked. Thirty minutes is enough time for real damage if the leak happens in minute one; expiry limits the window, it does not limit what fits inside it.
Key Takeaways
- Hold capability, never a standing credential; mint scope per run and let it expire.
- Separate the credential decision from the egress decision; each closes a different leak.
- Give agent commits their own bot identity, distinct from any human's push.
- Default egress to deny; allow only the model API, the registry, and your own remote.
- Treat every fetched page, repo file, and issue as an untrusted instruction channel.
- Pair this policy with lesson 6's hook; neither one covers what the other misses.
What You Have Now
fleet/access.md — the credential and egress policy your agents run under. It sits beside fleet/permissions/ from lesson 6, and together the two files answer what an agent may do and what it may reach, which is the whole of the Isolate stage. Module 3 assumes both are written before a fleet moves off your own machine.
Further Reading
- Incident record: Replit AI database deletion — a written freeze treated as a suggestion, not an enforced boundary.
- AWS security bulletin AWS-2025-015 — an over-scoped CI token used to reach close to a million installs.
- GitHub's OpenID Connect documentation — the short-lived, run-scoped credential pattern this lesson's diagram borrows.
- GitHub's engineering blog on bot commit signing — making a bot identity's commits independently verifiable.
- phoenix.security's 2026 supply-chain report — the scale of agent-targeted delivery mechanisms.
- justin.poehnelt.com on agent identity in git commits — the four environment variables that set a bot's commit identity.
Exercises
- Write
fleet/access.md's credential-pattern row for your own fleet shape, naming the anti-pattern it replaces from the table above. - Write the egress allowlist for one agent you run today: the exact three or four destinations it needs, and confirm nothing else is currently reachable.
- Find one place in your own setup where an agent's commits are indistinguishable from a human's in
git log --author, and write what a dedicated bot identity would need to change.
Check your understanding
Reading is free — your progress isn't being saved
Nothing you did on this page is recorded. Paid students get saved progress and verified completions, recorded quiz answers, the spaced-repetition review queue, labs, and a certificate for each course they finish.
30-day money-back guarantee. One-time payment — lifetime access and updates.