SecurityArticle
Model-generated workflows inherit the permissive habits of their training data; gate the permissions block, not just the syntax.
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:
Flip the org default. Settings β Actions β workflow permissions β read-only. One click, and every workflow that omitspermissions
β 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 fewResource not accessible
errors you fix by adding explicit, minimal grants. -
Run Itszizmorin CI.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. -
Put A diff that touches a.github/workflows/
behind CODEOWNERS.permissions:
block or adds asecrets.
reference is a security review, not a code review. Start every generated workflow atcontents: 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 #
Model-Generated GitHub Actions Need a Permission Gateβ dev.to - How Compliant Are GitHub Actions Workflows? A Checklist-Based Study with LLM-Assisted Auditingβ arxiv.org - GitHub Actions - Updating the default GITHUB_TOKEN permissions to read-onlyβ github.blog - Supply Chain Compromise of Third-Party tj-actions/changed-files (CVE-2025-30066)β cisa.gov - zizmor - Static Analysis for GitHub Actionsβ zizmor.sh - Under the hood: Security architecture of GitHub Agentic Workflowsβ github.blog - GitHub Actions Security Pt 2: AI-Powered Actions Analysisβ wiz.io
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 #
No comments yet
Be the first to weigh in.