A public issue talked an AI agent into leaking private repos. So we audited our own.
The GitLost disclosure needed no exploit code, just words in a text box and an over-permissioned agent. We treated it as a fire drill on our own agent pipelines. Here is what we looked for and what we found.
An attacker leaked private repos with plain English in a public issue. We used it as a checklist against our own agents.
Earlier this month, Noma Labs published GitLost: a way to make GitHub's agentic workflow read private repositories and paste their contents into a public comment. There was no exploit code, no stolen credential, no clever memory-corruption trick. The attacker opened a normal-looking public issue and, in plain English buried in the body, told the agent what to do. It did it. We build AI workflows and custom agents for a living, so we did not read that as GitHub's problem. We read it as a fire drill and pointed it at our own setup.
What actually happened in GitLost
Strip it to the mechanism and the attack is four boxes lining up. One: an agent reads some untrusted content (here, the text of a public issue anyone can file). Two: hidden in that content are instructions, not a bug report, actual commands aimed at the agent. Three: the same agent holds read access to things that matter (other repositories, including private ones) and a tool that produces public output (posting a comment). Four: nothing sits between the untrusted text and the public action to say no.
When those four line up, you do not need a vulnerability in the usual sense. The words are the payload. The agent was working exactly as built. That is what makes this class of bug easy to ship by accident: every piece is a feature.
Why this is our problem too
Our marketing site is a static export with no server, no API routes, and nothing that runs a visitor's input. That part is boring on purpose and it is not where the risk lives. The risk lives in the automation we run to build the site: agents that read things off the open web and agents that can write to the repo. If those are ever the same agent with no gate between, we have built GitLost by accident.
So we took the three lessons out of the disclosure and turned them into questions to ask every one of our pipelines.
The three rules we pulled out
- Untrusted content is data, not instructions. Anything an agent reads from the web, an issue, an email, or a fetched page is input to be processed, never a command to obey, no matter how confidently it is phrased.
- Least privilege per flow. The flow that reads untrusted content should not also hold the keys to publish. Keep read-the-world and write-something-permanent in separate hands.
- A human gate before any public or irreversible action. When untrusted input can influence an output that leaves the building, a person approves that output, every time.
How the audit went
We walked every automated flow in the project against those three rules. The honest summary: the one place the pattern could have bitten us was already fenced off, and the reason it was fenced off was structure, not luck.
The flow that reads untrusted content is our news pipeline. To find story candidates it pulls from public feeds and fetches arbitrary article pages, which is exactly the untrusted-input box from GitLost. What breaks the chain is that this flow has no write power at all. It stops at a checklist. It never edits the files that become live posts, it cannot publish, and a separate relevance reviewer, one that cannot be talked out of a decision inside a chat, sits in front of anything reaching the site. A human reads and approves every candidate and every finished draft before it goes live. Read-untrusted and write-public were already two different hands with a person in between.
The one scheduled job that does commit to the repo pulls from an authenticated Google service we control, not from attacker-controllable text, and it runs on a timer rather than being triggered by a public event like an incoming issue or comment. Different shape, not the GitLost shape.
We are deliberately not publishing the exploitable specifics of our own setup here. The point of this post is the method and the result, not a map for someone to probe. The gist: we found no live instance of the four-box pattern, and we found one flow where the safeguard was real but was resting on convention.
What we changed
A safeguard that lives only in someone's head is one distracted afternoon from being gone. So the fix was not new code, it was writing the rules down where the automation actually reads them: the three rules above are now explicit in our agent instructions and in the guardrails our content and pipeline agents follow, so the separation of read-untrusted from write-public is enforced by the setup and not by whoever happens to be paying attention. We also logged the whole review as an internal decision entry, so the next person (or the next version of us) inherits the reasoning, not just the outcome.
And this post itself went through the same gate it describes: a human read it before it went live.
The honest limits
One audit is a snapshot, not a guarantee. The four boxes can reappear the moment we wire a new agent to a new tool, so the real work is keeping the rules in front of us every time we add capability, not congratulating ourselves for passing once. Prompt injection also has no clean fix the way a SQL-injection bug does: you cannot fully sanitize natural language, so the durable defense is architectural, keep the dangerous combination from ever assembling, rather than hoping to catch every malicious sentence.
Next step: if you run agents that read the open web and can also act, take ten minutes and check the four boxes against each flow. Reading untrusted content, holding sensitive access, producing external output, no human in between. If one flow ticks all four, that is your GitLost. If you want a second pair of eyes on an agent system before it ships, write to us at hello@gattyworks.com.