Skip to content
Security Article

Your LLM Writes GitHub Actions Like It's 2022

Model-generated workflows inherit the permissive habits of their training data; gate the permissions block, not just the syntax.

Ji-ho Choi
Ji-ho Choi
Security & Cloud Editor · Aug 17, 2026 · 5 min read
Your LLM Writes GitHub Actions Like It's 2022

Ask a coding model for "a workflow that builds and publishes the docs" and you'll usually get back clean, plausible YAML. It parses. It passes actionlint. It might also carry permissions: write-all, trigger on pull_request_target, and touch a repository secret in a step that never needed one. A recent dev.to post put the rule memorably — "a valid YAML file is not a valid action" — and argued that model-generated workflows need a permission gate before merge. The diagnosis is correct. The homegrown fix the post reaches for is where most teams will go wrong.

Models write CI like it's 2022

The over-provisioning isn't a mystery, and it isn't malice. It's the training corpus. Until February 2023, every repo's GITHUB_TOKEN defaulted to read-write, and GitHub's switch to read-only defaults only applied to new repos and orgs — existing ones kept the permissive setting. So the millions of public workflows a model learned from overwhelmingly either omit the permissions block entirely (inheriting whatever the repo default is) or slap on broad scopes because that's what made the red X go away in 2021.

The numbers back this up. A 2026 checklist-based audit of real-world Java workflows on arXiv found 28% overall compliance with documented best practices — and 4% compliance on permission controls specifically. Four percent. When least-privilege permissions are the rarest pattern in the wild, a model reproducing the statistical center of its training data will hand you an over-privileged workflow almost every time. Wiz's analysis of AI-related actions found the same shape from the other side: most delegate permission validation entirely to the workflow author, who is increasingly a model.

There's a second, subtler pressure. A workflow that requests too little permission fails visibly — Resource not accessible by integration — while one that requests too much fails never. Every feedback loop a model has ever seen rewards the broad grant. You have to supply the counter-pressure yourself, because nothing in the generation loop will.

The token sets the blast radius

Why this matters stopped being theoretical in March 2025. When tj-actions/changed-files was compromised (CVE-2025-30066), the attackers retroactively repointed nearly every version tag at a malicious commit that dumped runner memory — access keys, PATs, npm tokens, RSA keys — into workflow logs. It hit workflows in over 23,000 repositories, and CISA issued an alert. The lesson wasn't "audit your third-party actions harder," though everyone said that. The lesson was that the permissions and secrets in scope when a step runs determine the damage, because you will not catch the compromise itself in time. A workflow with contents: read and no secrets in the job leaked read access. A workflow with write-all leaked the keys to the repo.

Model-generated workflows multiply the left side of that equation. Every over-scoped token a model ships is pre-positioned blast radius waiting for the next tj-actions.

Gate the diff, don't write a checker

The dev.to post's answer is a custom Python script that rejects contents: write and secret references. The instinct — enforce policy in CI, after linting — is exactly right. Writing your own checker is exactly wrong, for the same reason you don't write your own actionlint: the failure modes are legion (pull_request_target with checkout of the PR head, unpinned tags, template injection via ${{ github.event.issue.title }}, id-token: write enabling OIDC impersonation), and a 40-line script covers three of them while producing a false sense of coverage.

The tooling already exists. Here's the gate that actually works, in adoption order:

  1. Flip the org default. Settings → Actions → workflow permissions → read-only. One click, and every workflow that omits permissions — which is most of what models generate — becomes safe-by-default instead of privileged-by-default. This is the single highest-leverage change and it costs nothing but a few Resource not accessible errors you fix by adding explicit, minimal grants.

  2. Run zizmor in CI. Its excessive-permissions audit flags both explicit broad grants and the implicit kind — workflows that declare nothing and inherit repo defaults. It also catches the injection and untrusted-input patterns your script never will, emits SARIF, and lands findings in code scanning on the PR itself. OpenSSF Scorecard's token-permissions check gives you the same signal if you're already running it.

  3. Put .github/workflows/ behind CODEOWNERS. A diff that touches a permissions: block or adds a secrets. reference is a security review, not a code review. Start every generated workflow at contents: read, and add scopes back one at a time, each one justified in the PR:

permissions:
  contents: read

That's the whole gate. Lint for syntax, scan for policy, require a human on the privilege escalation path. None of it is novel — which is the point. The novel part is refusing to treat model output as trusted just because you prompted for it.

GitHub's own agents don't get write tokens

If you want confirmation that this is the right architecture, look at what GitHub built when it put agents inside Actions. GitHub Agentic Workflows gives its agents zero secret access, a read-only GitHub MCP server for repository state, and buffers every write through a separate "safe outputs" stage where deterministic checks filter and limit operations before anything lands. The compiler turns an agent's intent into a workflow with explicit permission constraints. GitHub, with every incentive to make agents feel frictionless, decided model-driven CI gets read-only by default and earns each write.

That's the tell. The company that owns the platform won't hand its own models a write token on trust. The gap right now is that this discipline exists in GitHub's agentic architecture and in opt-in tools like zizmor, but not in the default path where most model-generated YAML actually enters repos: a developer pasting Copilot or Claude output into .github/workflows/ and merging on green checks.

This one's a genuine problem, not AI-security hype — the corpus really is over-privileged, models really do reproduce it, and the 2025 supply-chain incidents already showed what scoped-too-wide tokens cost. But the fix isn't new tooling or a bespoke checker. It's an afternoon: flip the org default, add zizmor, gate the workflows directory. Do it before the next tj-actions, because there will be one.

Sources & further reading

  1. Model-Generated GitHub Actions Need a Permission Gate — dev.to
  2. How Compliant Are GitHub Actions Workflows? A Checklist-Based Study with LLM-Assisted Auditing — arxiv.org
  3. GitHub Actions - Updating the default GITHUB_TOKEN permissions to read-only — github.blog
  4. Supply Chain Compromise of Third-Party tj-actions/changed-files (CVE-2025-30066) — cisa.gov
  5. zizmor - Static Analysis for GitHub Actions — zizmor.sh
  6. Under the hood: Security architecture of GitHub Agentic Workflows — github.blog
  7. GitHub Actions Security Pt 2: AI-Powered Actions Analysis — wiz.io
Ji-ho Choi
Written by
Ji-ho Choi · Security & Cloud Editor

Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading