Your AI agent doesn't need your GitHub token (or your .env)
AI agents rarely need the secret itself, only the action it authorizes. Least-privilege rules for GitHub tokens, API keys, and env vars, plus how Fletch keeps credentials out of the sandbox.
The default way people give a coding agent access to a service is to hand the
agent the secret. Export GITHUB_TOKEN into its shell. Source the whole .env
so tests pass. Set every API key it might touch, because guessing which ones it
needs is annoying and setting all of them is one line. This is the
least-privilege violation almost nobody names, and it hides in plain sight
because it feels like setup, not a security decision.
Here is the thing worth sitting with: a coding agent reads files, writes files, and runs commands. That is its whole job. So any secret in its environment is a secret it can read, print to a log, paste into a fixture, commit, or send over the network. Not because it is malicious. Because that is the shape of the tool. A model following a plausible-looking instruction in a file it just read will do all of those things without a flicker of intent to harm you.
And the agent almost never needs the secret. It needs the action the secret authorizes. That distinction is the whole game.
A credential is not the same as an action
Think about what “let the agent push to GitHub” actually requires. One way to
grant it is to put your token in the agent’s environment. Now the agent can
push. It can also fetch every private repo you can reach, open and close issues,
delete branches, rewrite releases, and read org secrets, because that is what
the token is for. You wanted one verb and you handed over the whole vocabulary.
A confused agent, or one steered by a poisoned instruction, has exactly your
GitHub reach. One git push --mirror to the wrong remote, one script that logs
its own environment for debugging, and the token is somewhere you did not want
it.
The other way is to give the agent no token at all and let it ask for the action. The agent says “push this branch” to a broker that runs on the host, outside the agent’s reach. The host holds the credential and performs the push. The broker accepts a small, fixed set of operations and nothing else. Now the blast radius is not “everything the token can do.” It is “the specific verbs the broker exposes.” A compromised agent can still push bad code, so you still pick your tasks and review what lands, but it cannot exfiltrate the token, because it never touched the token.
Same logic for environment variables. Inheriting your shell means the agent gets
STRIPE_SECRET_KEY, DATABASE_URL, the lot, whether or not the current task
goes anywhere near payments. A more careful design injects only the specific
variables you approved for this run. The agent’s environment stops being a copy
of yours and becomes a short, deliberate list.
What actually goes wrong
The failure modes here are boring, which is exactly why they happen.
A .env in the agent’s environment: you ask it to write an integration test, it
needs the app to boot, so it reads STRIPE_SECRET_KEY to construct a client,
and along the way it writes a .env.test fixture with real values in it or
echoes the config into a log line “to check it loaded.” Nothing here is an
attack. It is an agent being helpful with a live production key, and now the key
is in a file, maybe in git, maybe in your CI logs.
A push token in the environment: the agent is cleaning up remotes, or a script
it wrote does git push against a URL it assembled from a variable, and the
target is not the one you meant. Or a dependency it installed reads the
environment on postinstall. The token does not have to be used wrongly to
leak. It only has to be present somewhere it can be read.
None of this requires a clever adversary. It requires a secret sitting in the environment of a process whose literal purpose is to read, write, and run things.
Rules of thumb
A few principles that hold regardless of which agent or tool you run:
- Keep secrets out of the agent’s environment. If a secret is not in the process, it cannot be read, logged, or leaked from that process. This is the one that does the most work.
- Broker privileged actions through something the agent cannot impersonate. Let the agent request an action; let a component it does not control hold the credential and decide whether to perform it. The narrower the set of allowed actions, the smaller the damage from a bad instruction.
- Share environment variables one at a time, and only when a run needs them.
Opt in per variable, per task. Most runs need far fewer secrets than your full
.envcontains. - Use per-agent throwaway values where you can. A disposable per-agent database URL means one agent’s mess, a dropped table, a bad migration, cannot reach into another agent’s data or your real one.
How Fletch does it
Fletch runs each agent in a write-protected sandbox (there is a separate post on that boundary). The sandbox keeps an agent from trashing your machine, but it does not on its own solve credentials, because a sandboxed process can still read what you put inside it. So Fletch is deliberate about what goes in.
GitHub. You connect GitHub once through an OAuth device flow, and Fletch stores the token in your macOS Keychain. The token never enters the sandbox. Local git work runs natively inside the workspace, but any action that needs your credentials, pushing, fetching, opening or merging a pull request, goes through a broker on the host that holds the token and performs the operation on the agent’s behalf. The agent asks for the action; the host does it. So an agent can publish a branch and open a PR under your identity, which is why you still choose its tasks with care, but it can never read the token, because the token is not where it can reach.
Environment variables. The sandbox withholds the project’s .env from a
run by default. Nothing is shared. Project settings show the variables found in
your .env, each with its own “share with sandbox” toggle, and you flip on only
the ones a run actually needs. A shared variable’s value is mirrored live from
.env at spawn, so there is one source of truth and nothing to drift. You can
also override a value in place, and overrides support {{agent_id}}
interpolation, which is how you give each agent its own disposable resource, a
per-agent database URL that resolves to a different value for every agent.
Override values never touch Fletch’s database; on release builds they live in
the macOS Keychain. Only the variables you approved, resolved to their final
values, are injected into the sandboxed Run-panel process.
The pattern underneath both is the same. Decide whether the agent needs the secret or the action. It is almost always the action. Give it that, hold the secret somewhere the agent cannot reach, and the worst a confused or steered agent can do shrinks to something you can actually reason about.