Giving a coding agent a GITHUB_TOKEN is an easy way to make it useful. It can inspect issues, open pull requests, update a deployment, or call an MCP server.
It is also an extremely broad permission boundary.
Once the token is in the agent's environment, the model and every tool it invokes can read it. It can end up in a shell command, logs, a commit, a prompt, or an outbound request that has nothing to do with GitHub. Even if none of that happens maliciously, the token has already become part of the agent's working memory and failure surface.
The more useful question is not: does this agent have a GitHub token?
It is: what is this particular agent run allowed to do with GitHub right now?
A token is identity, not a capability boundary
Tokens identify a principal. Most API tokens were designed for a human or service that can make a wide range of requests over a long period of time. An agent run is different: it is short-lived, probabilistic, and often has a narrowly defined task.
Suppose an agent is asked to triage repository issues.
It may need to:
- read the authenticated user;
- list and inspect issues in an approved repository; and
- perhaps create a comment or a draft pull request.
It does not need to delete repositories, create new deploy keys, alter organisation settings, or send the token to an arbitrary URL. A bearer token alone cannot express that difference.
This does not mean an agent is uniquely dangerous. It means its access should be described at the same level as its task.
Put the credential outside the agent process
One useful design is to give the agent a placeholder rather than the secret value. A local proxy receives the outgoing request, checks it against policy, and injects the real credential only if the request is allowed.
For example, an agent can combine a shared service secret from a staging environment with the developer's own GitHub credential. The shared secret and the personal credential are deliberately separate:
[secrets]
project = "acme"
environment = "staging"
[secrets.STAGING_SERVICE_API_KEY]
[[secrets.STAGING_SERVICE_API_KEY.rules]]
effect = "allow"
hosts = ["api.staging.acme.internal"]
methods = ["GET"]
paths = ["/v1/orders/*"]
# Personal credential owned by the developer.
[personal_credentials.GITHUB_TOKEN]
[[personal_credentials.GITHUB_TOKEN.rules]]
effect = "allow"
hosts = ["api.github.com"]
methods = ["GET"]
paths = ["/user", "/repos/acme/service/issues*"]
Secrets holds values shared by the project and environment. credentials holds values owned by the developer. The important property is not the syntax. The agent process receives neither real value; it receives safe stand-ins. The proxy has the real credentials and will use each only for a request matching its host, method, and path rule.
That changes the failure mode. If the agent attempts DELETE /repos/acme/service, or sends the placeholder to another host, the proxy can stop it before an upstream service sees a valid credential.
Keep network access separate from credential use
Credential injection should not silently turn into an egress allowlist.
An agent may be allowed to reach a model provider, package registry, or a Linear MCP server without receiving credentials for those destinations. Conversely, permission to inject a GitHub credential should not grant general access to the internet.
Those are separate questions:
- Can this process connect to this destination?
- If it does, may it use this credential for this request?
Keeping them separate makes a policy easier to audit. A rule that grants read access to GitHub issues should mean precisely that, not "the agent can now connect anywhere GitHub-related."
The proxy is not the whole sandbox
This is also where the security model needs to be honest. A local proxy is not a hard boundary against a malicious process running as the same user. If the process can make arbitrary direct connections, it may bypass proxy settings.
For that reason, a local agent runner should be able to apply network containment as well: deny direct network access while preserving the loopback connection to the proxy. That is useful, but it still is not filesystem isolation or process isolation.
Other controls answer different questions:
- a filesystem policy can prevent reading ~/.ssh, ~/.aws, and stray .env files;
- a command denylist can keep a run from invoking tools such as ssh or sudo; and
- a network sandbox can prevent direct egress around the credential proxy.
None of these replaces the others. They limit different ways an agent can exceed its intended scope.
Start with narrow, observable policies
The first version of an agent policy should be boring. Allow only the requests required for one workflow. Record which request was allowed or denied, the relevant rule, and request metadata—but never the secret value.
Then run a real task. You will quickly find the missing cases: an API endpoint that redirects, a required POST, an MCP transport path, or a package tool that needs a separate host. Add the minimum rule that enables the workflow rather than granting a large new category of access.
This is more work than exporting a token. But it turns an unbounded ambient credential into an explicit capability. That is a better fit for agents that are allowed to act, but should not be trusted with every action their credentials could technically perform.
We have been building this model in Stashbase's Agent Proxy: shared project/environment secrets and developers' personal credentials can be injected at request time under host, method, and path rules.
Learn more at stashbase.dev, or see the public CLI at github.com/stashbase/cli.
Top comments (0)