One word broke into a private repository this month. Not a zero-day. Not stolen credentials. Not malware.
One word: "Additionally."
Security rese...
For further actions, you may consider blocking this person and/or reporting abuse
Splitting the read-broad agent from the write-public one is the right shape, but the publish channel is wider than comments and PRs. Anything that can fetch a URL or pull down a remote image is an output channel too, and none of those look like a write permission when you go down the list. Worth putting egress next to the token scopes in that audit.
None of those look like a write permission when you go down the list is exactly the blind spot. my audit checklist checks for publish-anything (comments, PRs, external comms), but it's still framed around GitHub-native write actions. a fetch call or an image pull is a write in every sense that matters, it's the agent sending data somewhere, but it shows up on the permissions review as read-only network access, if it shows up at all.
DNS exfiltration through a crafted URL, or data encoded into a request to an attacker-controlled image host doesn't need any GitHub write scope whatsoever. an agent with zero repo write permissions and outbound network access already has a full exfiltration channel, which means the lethal trifecta doesn't even need GitHub's own permission model to complete.
egress belongs next to token scopes in the audit, agreed, and probably needs its own line item rather than living under publish output. that's a genuine gap in what I wrote.
This is exactly the kind of concrete security thinking agent builders need more of. I just shipped an agent evaluation harness (AgentEval Forge) and one of the hardest design decisions was making safety failures always fail the run — correctness comes second. Your single-word-bypass example is terrifying and completely believable. Are you thinking about this as a prompt-layer problem, a tool-permission problem, or both? I think the real fix might need to live at the adapter/contract layer, not the prompt layer.
Safety failures always fail the run" is a design choice more tools should be forced to make explicit.
honest answer: this thread's pushed my thinking from prompt-layer toward tool-permission, and your point takes it further. scoping answers should this tool exist at all which still requires predicting every case in advance. an adapter/contract layer sounds closer to taint tracking: flag data by trust level on entry, refuse to let tainted input reach an output tool regardless of what the token scope allows.
is that close to what AgentEval Forge enforces, or is it doing something more specific at the schema level?
the "Additionally" vector is the kicker. it works because the model sees the instruction as a continuation of context rather than a new command. guardrails trained to refuse standalone requests don't have the surface area to catch continuation framing.
the fix isn't better guardrails here. it's capability scoping. if the agent's MCP toolset has a "read private repo" tool, the right place to enforce the boundary is before the tool runs, not in the model's refusal logic. you can't reliably make the model not want to use a tool once the tool exists in its context.
what does the audit actually look like for your setup — are you reviewing granted token scopes per agent config, or does your IdP do it at runtime?
Guardrails trained to refuse standalone requests don't have the surface area to catch continuation framing names exactly why this keeps working across every writeup of it. the refusal logic is evaluating a request in isolation, and additionally isn't a new request, it's grammatically parasitic on the one that already got approved. there's no clean boundary in the conversation for the guardrail to re-evaluate against.
agreed on capability scoping being the actual fix rather than better refusals. if the tool to read a private repo exists in the agent's available toolset at all, you're one continuation-framing away from it getting called, no matter how well-trained the refusal is. the enforcement has to happen at the boundary of can this tool even be invoked in this context before the model's reasoning is in the loop, not as a judgment the model makes about whether it should want to.
honest answer on my own setup: it's config-level, not IdP-enforced at runtime. I went through it manually after this disclosure, checked what my Claude Code instance actually had read access to, and scoped the token down by hand rather than through anything automated. no per-agent policy engine, just a person looking at a permissions list. which, reading your question back, is probably exactly the gap you're pointing at, manual review doesn't scale and doesn't get re-checked unless something scares someone into doing it again. runtime enforcement through the IdP is the actual answer for anyone past a single developer's setup, I just don't have firsthand experience running one.
"grammatically parasitic" is the right framing — the refusal checks a request in isolation, and continuation framing exploits that context boundary by design.
the enforcement before reasoning point pushed us toward context scoped MCP tool definitions rather than service level ones. the available toolset IS the policy. if the
repo-readtool is not in scope, continuation framing has nothing to grab.the "scares someone into rechecking" pattern is universal. does IdP enforcement in your mental model fire per agent session or per tool invocation?
Continuation of context rather than a new command is the precise mechanical explanation for why Additionally worked, and it's sharper than anything in my article. guardrails trained to catch standalone malicious requests are pattern-matching against the shape of an attack. reframing the same request as a natural next sentence in an ongoing exchange just doesn't trigger that pattern, because structurally it isn't the pattern.
the capability-scoping point is the right correction to where I put the emphasis. the model shouldn't want to leak this is a behavioral hope. the tool physically can't be called with this scope is an architectural fact. one of those degrades under enough creative rephrasing. the other doesn't care how the request was phrased.
honest answer on my own setup: it's not IdP-enforced at runtime, it's manual. I went through my Claude Code config by hand after reading about GitLost and scoped the token down based on what I found. no policy engine sitting between the agent and the tool call, just me checking the scopes directly. for a personal project that's been enough so far, but your question is basically pointing at the actual gap: a manual audit catches what exists today, it doesn't stop scope creep the next time I add a tool or grant broader access for convenience. runtime enforcement would.
"behavioral hope vs architectural fact" is the best design heuristic i've seen for this space. if a capability needs runtime enforcement to be safe, the tool boundary was probably drawn wrong upstream — the scope should've been smaller before the policy problem existed.
the PR per tool addition audit is basically what we landed on too. not elegant, but it catches new grants before they hit prod.
what's pushing you toward runtime over just tightening the OAuth scopes on the token itself?
I appreciate how this article sheds light on a critical security concern that many developers may overlook, providing a timely warning and practical advice for safeguarding public repositories.
If you're interested in sharing your expertise with a broader audience, consider cross-posting your content to ZyVOP, where you can connect with like-minded professionals and grow your reach.
Thanks for reading!
The permission-layer framing is the part I wish more teams would copy. Prompt hardening helps, but repo access should be treated like a production credential. Read-only by default, short-lived tokens, branch protections the agent cannot edit, and a log that ties every write back to the workflow that granted it. Otherwise the agent has a prettier way to make the same old CI/CD mistake.
A prettier way to make the same old CI/CD mistake" is the line that should've closed the article. that's exactly it, the failure mode isn't new, it's the classic over-scoped credential problem wearing a chatbot costume.
the short-lived tokens point is one I underweighted. a scoped token that's also long-lived still gives an attacker a wide window if the trifecta ever lines up, short expiry shrinks the blast radius even when everything else fails.
branch protections the agent cannot edit is the detail I think most teams miss entirely. it's easy to scope what an agent can read and still forget that write access plus the ability to modify its own guardrails is functionally no guardrail at all.
and the audit log tying every write back to its granting workflow is the piece that turns we think this happened into we know exactly what happened which is the difference between a bad afternoon and a bad month when something does go wrong.
treating repo access like a production credential is the right mental model, full stop. most teams already have that discipline for database creds and API keys. the gap is just that nobody's extended it to the thing reading GitHub issues yet.
The Sasi Levi framing is the key shift: prompt injection used to be about manipulating what an agent says, but GitLost is about what an agent does with real permissions. The structural problem is that agents reading untrusted content inherit no implicit trust hierarchy — a malicious instruction embedded in a GitHub issue looks syntactically identical to a legitimate one. One mitigation pattern worth considering: treat any content the agent reads as the "data plane" and any instruction set as the "control plane", enforcing that the agent's execution context only ever acts on the latter, never on what it reads. The architecture isn't fundamentally different from SQL injection defense — parameterization beats sanitization every time.
Parameterization beats sanitization is the analogy that should be the standard reference point for this entire problem, and I'm a little annoyed I didn't reach for it myself. SQL injection took years of just sanitize the input better attempts before the industry accepted that sanitization is fundamentally a losing game against creative encoding, the fix was never smarter filtering, it was separating data from the instruction channel structurally so there's nothing left to sanitize.
the data plane versus control plane framing does the same work here. no implicit trust hierarchy names precisely why this keeps happening: a malicious instruction embedded in a GitHub issue is not malformed in any way the model can detect, it's syntactically identical to a legitimate one, the same way a well-formed SQL injection string is syntactically valid SQL. there's no pattern to catch because there's no pattern, it's just data that happens to also parse as an instruction.
enforcing that the agent's execution context only ever acts on the declared instruction set, and treats everything it reads as inert data regardless of what it says, is a genuinely different design goal than "train the model to recognize bad instructions." one is architectural. the other is playing an infinite game of prompt whack-a-mole that the model will eventually lose, the same way pattern-matching WAFs eventually lost to SQL injection variants nobody had thought to blocklist yet.
this is a better articulation of the actual fix than the audit checklist I wrote. the checklist reduces blast radius. what you're describing would remove the vulnerability class.
GitLost needing zero push permission is the detail I keep pointing people at. The exfiltration channel was a public comment, which most permission reviews file under harmless, and your checklist is one of the few that treats publish-anything as write access. That reframing matters more than any specific fix: an agent that can see everything and say anything is already the full lethal trifecta, no code-push involved.
One angle the disclosure leaves open: Agentic Workflows compile the Markdown instructions into YAML before execution. The Markdown reads like intent, the YAML carries the effective permissions, and the two drift as workflows get edited. Diffing the compiled workflow's permissions in CI, the way we already diff lockfiles, would have flagged the cross-repo read here before any attacker typed "Additionally". Has anyone seen tooling that does this yet?
Turns out GitHub already built exactly this, and it's more baked in than I expected. Agentic Workflows compile the Markdown into a lock.yml file that gets committed alongside it, and that lockfile (not just the Markdown) is what actually gets reviewed in PRs. compilation runs three scanners: actionlint, zizmor for privilege escalation, and poutine for supply chain risk.
so the diff the compiled permissions like a lockfile tooling largely exists already, at the platform level, for exactly the reason you're describing: catching drift between what a workflow says in Markdown and what it actually executes.
here's the honest limit though: it wouldn't have caught GitLost specifically. the cross-repo read access in that setup wasn't drift, it was correctly configured from day one, intentionally granted for legitimate cross-repo context. the lockfile would have shown exactly what everyone expected to see. the vulnerability wasn't a gap between Markdown and compiled permissions, it was that even accurately-compiled, intentionally-granted permissions become dangerous the moment they're combined with untrusted input and a publish channel.
so the tooling you're describing solves a real, adjacent problem (permission drift across edits) that's genuinely valuable, but it's a different failure mode than the trifecta itself. you'd still need something checking for the combination, not just the accuracy of what's declared.
Amazing article, Harsh! ❤️ This is one of those posts that every developer working with AI agents should read. I truly believe AI is only as safe as the permissions we give it, and your article explains that with great clarity. Thanks for sharing such practical insights and reminding us that security should always come before convenience. Keep writing such valuable content , looking forward to your next post! 🧡
Thank you so much genuinely 🙏
AI is only as safe as the permissions we give it is a cleaner way to say the whole point of the article in one sentence. appreciate you reading it closely, and glad it landed the way it was meant to.
The detail that stands out is that a single connector word ('Additionally') flipped a refusal into compliance, which tells me the guardrail was reasoning about the request as one command boundary rather than the actual data flow. The real fix here is scoping the token so the agent physically cannot read the internal repo, not trying to teach it to say no, because prompt-level boundaries lose to rephrasing every time. Reframing a new instruction as a continuation of an approved one is a pattern worth testing against your own workflows directly.
The guardrail was reasoning about the request as one command boundary rather than the actual data flow is a sharper diagnosis than anything I wrote. that's the actual mechanism, not just the model got tricked. it was tracking whether this looked like a new command, not whether the data it was about to touch belonged to a different trust zone than the one the conversation started in.
that distinction is why prompt-level boundaries lose to rephrasing every time is true and not just pessimism. a boundary based on conversational framing will always have another framing that slips past it, because the check is happening at the wrong layer. the token scoping fix works because it doesn't ask the model to recognize the attack, it just removes the thing being attacked.
reframing a new instruction as a continuation of an approved one as a named pattern is genuinely worth testing directly, you're right. that's specific enough to actually try against a real workflow rather than just something to keep in mind. going to go run that against my own setup rather than assume the token scoping alone covers it.
Is it possible to verify what the exact prompt that bypassed the security measures looked like (aside from the single word "Additionally")? Or what the actual workflow and logs looked like?
yes it's verifiable, and this is worth knowing about because it's not locked behind a private disclosure.
Noma Security published the full proof-of-concept on their own blog, including the actual workflow run, the original test issue text, and even a video walkthrough of the exploit. it's not just a summary in the press coverage, it's the real artifact.
the short version of what's in there: the test issue was written to look like a routine VP of Sales follow-up after a customer meeting, with the actual exfiltration instruction buried near the end after some innocuous context (color scheme feedback, meeting notes) rather than being the obvious first line. the workflow itself was configured to trigger on issues.assigned, read the issue title and body, use an add-comment tool to respond, and had read access spanning both public and private repos in the org.
worth pulling up Noma's own writeup directly if you want the exact wording rather than my summary of it they're the only ones with the actual verbatim PoC and logs, and I'd rather point you to the primary source than risk misquoting it secondhand.