The short answer
On 26 May 2025, Invariant Labs showed that an issue filed in a public GitHub repository could hijack an agent using the GitHub MCP server. When the repository owner asked the agent to look at open issues, the planted text led it to read the owner’s private repositories and publish what it found, including a relocation plan and salary information, in a pull request on the public repository.1
No code bug was involved. Invariant’s own words: it is “not a flaw in the GitHub MCP server code itself”, but an architectural issue at the agent system level.1 The fix is in how the agent is set up: a token that reaches only the repository the task needs, the server’s lockdown mode for public content, and a person reading tool calls that write to public places.
Deedglass
A tool call meets your rules before it runs
Asks to run
DELETE FROM visits;
Deletes every row in visits
Deedglass checks your rules
- Reading dataAllowed
- Deleting a whole table (DROP, TRUNCATE)Never
- Changing or deleting rows without saying which (WHERE)This one
- Other changesWait for a person
What the demonstration did
The setup was ordinary. A user runs Claude Desktop with the GitHub MCP server connected to their account, owns a public repository that anyone can file issues against, and owns a private repository with sensitive data.1
- The attacker files an issue in the public repository. Its text is written for the agent, not for a person.
- The user asks a normal question, such as “have a look at the open issues in my public repo”.
- The agent reads the issue and follows its instructions as if they were part of the task.
- It reads private repositories with the same token, because the token can.
- It opens a pull request on the public repository containing what it read. The attacker reads the pull request.
The model was Claude 4 Opus. Invariant stresses that the problem isn’t specific to one client or one model, and that strong safety training didn’t stop it.1
Claude Desktop asks before tool calls by default. Invariant notes that many users choose “Always Allow” and stop watching individual actions.1 A confirmation prompt only protects you while someone reads it.
Why no code fix closes it
Every step the agent took was a legitimate call the user’s token was allowed to make. Listing issues, reading a file, creating a pull request: the server did what it was asked. Nothing was malformed, so nothing in the server’s input validation could catch it.
What went wrong is that instructions arrived through data. The issue body is untrusted content written by a stranger, and the model has no reliable way to keep “text I’m reading” apart from “text I should obey”. That is prompt injection, and as this demonstration showed, safety training alone didn’t stop it.1
The three ingredients
Three weeks later, Simon Willison named the combination: the “lethal trifecta” of access to private data, exposure to untrusted content, and the ability to communicate externally.4 The GitHub demonstration has all three in one token:
| Ingredient | In the GitHub case |
|---|---|
| Private data | The token could read private repositories |
| Untrusted content | Anyone can file an issue on a public repository |
| A way out | Creating a pull request on a public repository |
Remove any one and the attack fails. That gives you three places to cut, and they differ in how reliable the cut is.
Fixes, in order of strength
1. Scope the token to the repository the task needs. GitHub’s fine-grained personal access tokens can be limited to specific repositories and given specific permissions; GitHub recommends the minimal access that meets your needs.3 One detail matters here: fine-grained tokens always include read-only access to all public repositories.3 So a token scoped to one private repository can still read public issues elsewhere, but it can’t read your other private repositories, which is the data that leaked. This cut doesn’t depend on the model behaving.
Invariant’s own recommendation goes the same way: restrict the agent to one repository per session.1
2. Turn on lockdown mode. The GitHub MCP server now has a lockdown mode (--lockdown-mode, or GITHUB_LOCKDOWN_MODE=1 in Docker). It limits the content the server surfaces from public repositories by checking whether each item’s author has push access. Some tools return an error for such content and others filter it out.2 An issue filed by a stranger never reaches the model’s context.
The README is clear about its limit: lockdown mode is not an authorization boundary. The credential’s access doesn’t change, so withheld content may still be reachable another way.2 It cuts the untrusted-content ingredient, not the private-data one. Use it with fix 1, not instead of it.
3. Use read-only mode when the task is reading. --read-only offers only read-only tools, and --toolsets enables only the groups you name.2 A triage agent that can’t create pull requests has no way out through GitHub. It may still have other ways out, such as a web fetch tool, so check every tool the session has.
4. Keep a person on public writes. The MCP specification says there should always be a human in the loop with the ability to deny tool invocations, and that clients should show tool inputs before calling the server.5 Applied narrowly, this is cheap: ask only before writes that land somewhere public. A prompt that appears twice a day gets read. A prompt on every call gets “Always Allow”.
A setup checklist
Before you connect an agent to GitHub:
- One fine-grained token per task, limited to the repositories that task needs.3
- Lockdown mode on for any session that reads public repositories.2
- Read-only mode when the task doesn’t need to write.2
- No “Always Allow” for pull request, issue or comment creation on public repositories.
- List every tool in the session. A web fetch or email tool is a way out too. Our MCP server security basics walk through it.
- Log the tool calls, so you can tell afterwards what the agent read and wrote.5
The same pattern appeared on another platform: a support ticket steered an assistant using the Supabase MCP server, as we covered from the primary source. Different server, same three ingredients.
What a gate can and can’t do here
We build Deedglass, which puts rules and approvals in front of an agent’s tool calls, so here is where that helps and where it doesn’t.
A rule can require approval before create_pull_request on a public repository, or refuse calls to repositories outside a list. That cuts the way out, and it holds even when the model has been talked into something. It is the same idea as Invariant’s one-repository-per-session policy.1
It can’t stop the injected text from reaching the model in the first place. Through an MCP server, only tool calls meet Deedglass’s rules; resources, prompts and other messages pass through.6 And it is weaker than fix 1: a gate that’s misconfigured, or routed around, protects nothing, while a token that can’t read your other private repositories protects you whatever sits in front of it. Scope the token first. Then add a gate if you want approvals and a record of every call. The approvals post shows the setup, and the blast radius entry explains why the token’s reach is the number that matters. To try the checks without installing anything, open the playground; to grade what an agent can reach on your own machine, run npx deedglass scan (the grade, explained).
Written with AI assistance and checked against our claims register.