2026-08-30 · 6 min read

An AI wrote the bug. Another AI exploited it. A human approved it.

In June, a change co-authored by GitHub Copilot Autofix landed in Snowflake's open-source connector repo and quietly created a script injection hole in a CI workflow. Five days later, an autonomous attack agent from Wiz found the hole entirely on its own, exploited it, and walked out with a Jira token that gave read access to Snowflake's engineering and security compliance projects. The internet spent August arguing about which AI to blame, which made for great headlines but missed the only part of this story that you, personally, can do anything about.

What actually happened

The repo is snowflakedb/snowflake-connector-net, and it has a GitHub Actions workflow that files a Jira ticket whenever someone opens an issue. On June 18, a merged PR reworked that workflow, and among other things it replaced a safe pattern, one where the issue title was passed as an environment variable and then handled with jq --arg, with direct template interpolation into a shell command plus some sed escaping for quotes.

Here's the shape of the change, compressed to the two lines that matter:

copilot-autofix co-authored · .github/workflows/jira_issue.yml -2 +1
1- env: ISSUE_TITLE: ${{ github.event.issue.title }}
2- run: jq -n --arg title "$ISSUE_TITLE" ...
3+ run: TITLE=$(echo '${{ github.event.issue.title }}' | sed "s/'/\\\'/g")

If you've ever reviewed GitHub Actions workflows, this is the classic trap: ${{ }} expressions are expanded by GitHub's runner infrastructure before the shell ever starts, which means the sed escaping runs after the dangerous input has already been injected into the command. The escaping sanitizes nothing. Any issue title containing a single quote breaks out of the string literal, and whatever comes after the quote runs as Bash in a runner that holds secrets.

On June 23, five days after the PR merged, Wiz's autonomous "Red Agent" found the flaw without any human pointing it in the right direction, crafted an issue title with a quote-breakout payload (and here's the part that should make you sit up: its first attempt failed with a Bash syntax error, so it read the error message, adjusted its own exploit, and tried again), and exfiltrated the workflow's JIRA_API_TOKEN. The token gave read access to Snowflake's engineering, security compliance, and bug bounty Jira projects. Snowflake patched the same day, rotated the token, and their audit found no evidence of third-party access during the window. As incidents go, this was a lucky one.

The blame game is a decoy, and here's why

August's news cycle turned the whole thing into an attribution fight. GitHub disputes that Copilot Autofix actually wrote the vulnerable line; the unsafe refactor seems to originate in an earlier commit by a Snowflake engineer, and a squash merge folded everything into one commit that happened to list Copilot Autofix as co-author because it had touched other parts of the same PR. Forbes covered the back-and-forth, and it's the kind of story that generates a lot of heat and almost no light.

Because here's the thing: it doesn't matter. Not for you, not for anyone whose job is to review code before it ships. Whether the vulnerable line was typed by a model, typed by a human, or typed by a human accepting a model's suggestion (which is the most common case now, and precisely why the question "who wrote this?" has stopped being answerable in any meaningful way), the change went through the same gate that every change goes through. A pull request. Which someone reviewed. And approved.

And the fact that squash merges blur authorship into an unrecoverable mess is not a footnote or an edge case, it's the condition we all work in now. When a single commit can contain a human's refactor and an AI's autofix fused together with no way to tell which line came from which source, "who wrote this?" is no longer a question you can answer, and the only position left in the pipeline that still has leverage over whether the change ships is the reviewer's.

Why review missed it, and why it's a fair miss

It would be easy to say the reviewer was sloppy, except that's not what happened. Look at the diff again. It adds escaping. It reads like a security improvement: someone is clearly thinking about quotes and injection, there's a sed call right there scrubbing dangerous characters, and a reviewer skimming for intent sees defensive code and nods, because it looks like exactly the kind of change you'd want to see in a workflow that handles user input.

The bug is not in any single line. It's in the ordering of two execution contexts, template expansion happening before shell execution, and the diff quietly deleted the only pattern that respected that ordering. To catch this, you would have needed to have internalized one very specific fact about how GitHub Actions processes ${{ }} expressions, and you would have needed the reflex to ask about it at the exact moment you saw an interpolation move from an environment variable into a run command. That's not general carefulness or "reviewing more thoroughly." That's a trained pattern, the same way "check the boundary condition on a date filter" is a trained pattern, or "who else calls this function?" is a trained pattern. You either have it in your repertoire or you don't, and if you don't, no amount of careful reading will make this diff look dangerous, because it looks like an improvement.

This is also what makes reviewing in the AI era harder in a specific, nameable way: models produce code that is locally plausible and stylistically confident, so the usual reviewer instinct of "this part looks off, slow down" has nothing to grab onto. A survey this month found 80% of organizations traced an incident or outage to AI-generated code in the past year, not because AI code is uniformly worse than human code, but because it fails in ways that look fine on the screen, in front of reviewers who were never explicitly trained to be the last line of defense.

The uncomfortable symmetry

The most instructive detail in Wiz's write-up, and the one that stuck with me longest, is that the attacking agent debugged its own exploit. It tried a payload, hit a Bash syntax error, read the error output, adjusted the payload to fix the syntax, and got in on the second attempt. Five days from merge to compromise, zero humans involved on the offense side at any point.

Offense has automated its feedback loop: the attacker tries, fails, learns from the failure, and tries again, all within minutes. Defense's equivalent feedback loop is code review, and for most engineers working today it still has no feedback at all. You approve a PR, it merges, and nothing ever circles back to tell you what you missed. You can't out-skill an adversary that learns from every attempt when you learn from none of yours.

Train the reflex before the incident does

The fix for this class of failure is not "be more careful" or "review harder," because this reviewer probably was careful and probably did read the diff, and the bug still shipped because carefulness without the right trained reflex is not enough. The actual fix is reps against known bugs, where you find out what you missed in minutes instead of discovering it five days later in a disclosure. That's a trainable loop: review a change, commit to a verdict, and then see exactly what was planted and what you walked past. Injection through trusted templating, sanitization applied after expansion, the diff that looks like it's hardening your security posture while actually dismantling it: these are all patterns you can meet in practice, on low-stakes code, before you meet them with your name on the approval.

Try a review

A diff that looks like a security improvement. Train the reflex before the incident does.

200+ realistic PRs with planted bugs, including security ones in this family. Free, no signup. You get the canonical review after your verdict.

Browse the library →
Read next How to review AI-generated code: a guide for the human in the loop
← All posts