On 25 October 2026 I downloaded every workflow file from 300 popular open-source repos, half of them AI, LLM and MCP projects, and parsed 4,750 workflows. 265 of the 300 use at least one third-party action. 145 of those 265 (54.7%) still reference at least one of them by a tag or a branch, which is a pointer that whoever controls that action can move. In the AI half it is 88 of 135 (65.2%).

TL;DR (for non-technical readers)

  • Most open-source projects use small add-ons ("actions") written by other people to build and release their software. You can point to an add-on by a version name, which its owner can quietly change, or by a fixed fingerprint, which cannot change.
  • I checked 300 popular projects. Of the 265 that use outside add-ons, 145 still use at least one by its changeable name. In AI projects it is about 2 in 3.
  • This exact weakness was abused at least three times in the past 19 months: in March 2025, March 2026 and June 2026. Projects that used the fixed fingerprint were not affected.
  • The fix is cheap: use the fingerprint, let a bot keep it updated, and give the build robot only the permissions it needs.

Why now

Three incidents, all the same shape:

  • tj-actions/changed-files, March 2025. The GitHub advisory for CVE-2025-30066 says attackers retroactively pointed version tags at a malicious commit that printed CI secrets into public logs, impacting "over 23,000 repositories" between 14 and 15 March 2025.
  • aquasecurity/trivy-action, March 2026. According to Snyk's write-up, attackers force-pushed malicious code to 75 of 76 version tags, exposed for about 12 hours on 19 and 20 March 2026.
  • codfish/semantic-release-action, June 2026. StepSecurity reports a malicious commit force-pushed on 24 June 2026 and version tags repointed to it.

In all three, users who pinned a full commit SHA were not affected. GitHub's own secure use reference says to "Pin references to actions to a commit SHA in your workflow files". Since 15 August 2025, administrators can enforce that with a checkbox in the allowed actions policy at enterprise, organization or repository level, and block specific actions with a ! prefix. And on 14 October 2026, GitHub put workflow lockfiles (.github/workflows/actions.lock, generated by gh actions-lock) into public preview. So: how far along are popular repos?

Method

  1. Repo list. GitHub search sorted by stars, non-archived, non-fork, pushed since 25 April 2026. AI queries: topic:llm, topic:mcp, topic:model-context-protocol, topic:ai-agents, topic:large-language-models, topic:agents, topic:rag, topic:mcp-server. General: language:python, language:typescript, language:javascript, with any repo carrying an AI or ML topic moved to the AI group. From 1,921 candidates I walked down by stars, skipped 11 awesome-lists and 25 repos without workflows, and stopped at 150 per group. The smallest repo has 38,791 stars and the median 65,347.
  2. Fetch. The contents API listed .github/workflows, and every .yml/.yaml was downloaded and cached. Read-only gh token, GET requests only.
  3. Classify. Each uses: is local (./), docker://, first-party (actions/*, github/*), same-owner (the repo's own org) or third-party. Refs are a 40-hex SHA, a version tag, or a branch. Refs that were neither, like release/v1 or stable, I resolved through the API.
  4. Risky patterns. Top-level permissions:, pull_request_target combined with a checkout of the PR head, and attacker-controlled contexts (github.event.issue.title, pull_request.title, head_ref, comment.body and the rest of GitHub's list) written straight into a run: step.

The core of the classifier:

SHA40 = re.compile(r"^[0-9a-f]{40}$") VER = re.compile(r"^v?(\d+)(\.\d+)?(\.\d+)?([-+.].*)?$") path, _, ref = uses.partition("@") owner = path.split("/")[0].lower() kind = "first_party" if owner in {"actions", "github"} else "third_party" ref_class = "sha" if SHA40.match(ref) else "tag" if VER.match(ref) else "branch" 

The fetch finished at 12:41 WIB (05:41 UTC): 29,330 uses: references, 5,640 of them to 472 distinct third-party actions.

Results

Third-party refs by pin type for all, AI and general repos, and the share of repos with at least one unpinned third-party action: 55%, 65% and 44%.

All AI/LLM/MCP General
Third-party refs 5,640 3,438 2,202
Pinned to full SHA 64.1% 55.0% 78.3%
By tag 32.3% 39.8% 20.6%
By branch 3.6% 5.2% 1.1%
Repos using third-party actions 265 135 130
...with at least one unpinned ref 145 (54.7%) 88 (65.2%) 57 (43.8%)
...fully SHA-pinned 120 (45.3%) 47 (34.8%) 73 (56.2%)
...with zero SHA pins 93 (35.1%) 55 (40.7%) 38 (29.2%)
Median repo's SHA share 75% 23% 100%

Of the 1,822 tag references, 1,525 are major-only like @v4, the kind of tag that is designed to move. When people do pin, they mostly do it well: 2,579 of 3,614 SHA references (71.4%) carry a version comment.

The most-used third-party actions, plus anthropics/claude-code-action (rank 16) because it is so common in AI repos:

SHA share for the 12 most-used third-party actions, from 71% for codecov/codecov-action down to 31% for pypa/gh-action-pypi-publish.

Action Repos Refs SHA share
docker/login-action 95 369 61%
docker/setup-buildx-action 89 278 61%
docker/build-push-action 79 226 57%
astral-sh/setup-uv 62 470 43%
pnpm/action-setup 55 533 49%
pypa/gh-action-pypi-publish 48 123 31%
oven-sh/setup-bun 47 300 66%
softprops/action-gh-release 46 71 41%
anthropics/claude-code-action 21 41 61%

pypa/gh-action-pypi-publish stands out: 83 of its 123 references use the release/v1 branch, which is what the project's own README uses in its examples. That action handles PyPI publishing credentials, so it is the one I would pin first. tj-actions/changed-files, 19 months after its compromise, is still in 11 repos, and 8 of its 23 references are tags.

Permissions and the risky patterns:

All AI/LLM/MCP General
Workflows with top-level permissions: 70.3% 67.6% 74.3%
Workflows with no permissions: at any level 878 (18.5%) 579 (20.4%) 299 (15.6%)
Repos where every workflow sets it 84 (28.0%) 29 (19.3%) 55 (36.7%)
Repos with a pull_request_target workflow 122 64 58
...checking out the PR head 5 4 1
Repos with untrusted context in run: 7 6 1
Repos with .github/workflows/actions.lock 0 0 0

Three findings

1. The median AI repo pins less than a quarter of its third-party actions. The median general repo pins all of them: 23% against 100%. Counted per reference, it is 55.0% against 78.3%. AI projects are younger and move fast, and their CI shows it. That is uncomfortable, because their workflows can hold model API keys, PyPI and npm tokens and registry credentials.

2. Having a bot for updates goes hand in hand with pinning. 145 of the 300 repos (48.3%) configure Dependabot for github-actions or Renovate. In those repos, 77.0% of third-party references are SHA-pinned, and 91 of 139 are fully pinned. Without an updater, it is 37.2%, and 29 of 126.

SHA share with and without Dependabot or Renovate: 77% vs 37% overall, 66% vs 33% in AI repos, 93% vs 45% in general repos.

This is a correlation, not proof. My reading: pinning once is easy, and keeping dozens of SHAs current by hand is the part that hurts, so repos that pin tend to be the ones that automated the updates.

3. The scary patterns are rare, and mostly gated. 122 repos (40.7%) have a pull_request_target workflow, but only 5 check out the PR's code in it. Two of those put the job behind a GitHub environment used as a manual approval gate. The other 3 deserve a closer look, and I am handling them privately. 7 repos write an attacker-influenced value straight into run:. On reading them, every one was on an unprivileged trigger, gated by an if: such as "same-repo branch" or "author is a member", or behind a trust-check job. None looked exploitable from outside, but each is one edited if: away from being exploitable. I am not naming any of these repos.

And the newest control has not landed yet: 11 days after the public preview, none of the 300 repos has an actions.lock.

Fix checklist

  1. Pin with a SHA plus a version comment. Resolve the tag to a commit, and check that it comes from the action's own repo, not a fork:
gh api repos/OWNER/ACTION/commits/v1.2.3 --jq .sha 
- uses: OWNER/ACTION@<full 40-character sha> # v1.2.3 
  1. Let a bot keep the pins current. Dependabot, in .github/dependabot.yml:
version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" 

GitHub's docs say Dependabot updates actions/checkout@<commit> references, and updates the version comment "when the comment is on the same line". One catch from the same docs: Dependabot alerts are only raised for actions referenced by semantic version, not for SHA pins. On Renovate, add the helpers:pinGitHubActionDigests preset to extends.

  1. Enforce it. The allowed actions policy in your repository, organization or enterprise Actions settings has a checkbox to require full-length SHA pinning; unpinned workflows then fail. For transitive pins, try the gh actions-lock lockfile preview.

  2. Start every workflow with minimal permissions and raise them per job:

permissions: contents: read jobs: release: permissions: contents: write 
  1. Never put untrusted input inside run:. Pass it through env:. Adapted from GitHub's docs:
- name: Check PR title env: TITLE: ${{ github.event.pull_request.title }} run: | if [[ "$TITLE" =~ ^octocat ]]; then echo "PR title starts with 'octocat'" fi 
  1. Do not check out PR code in pull_request_target. If you must, put the job behind an environment with required reviewers. Since 8 December 2025 GitHub always takes the workflow file from the default branch for this trigger, but that does not make an explicit checkout of the PR head safe.

Caveats

  • Static analysis only. I read YAML. I did not run anything, and composite actions inside the repos (.github/actions/*) and transitive actions were not scanned.
  • A SHA pin is not a full fix. A pinned action that downloads a script or uses @main internally still moves.
  • Settings are invisible. A repo can set the default GITHUB_TOKEN to read-only or enforce SHA pinning in its settings, and neither shows up in YAML. "No permissions: block" is not proof of a write token.
  • Classification limits. Version-like refs were counted as tags without checking the API, and a few could be branches. Same-org actions (for example a project's own reusable workflows) were left out of third-party counts.
  • Sample. 300 repos by stars and topics, one day, default branch only. The AI split follows self-chosen GitHub topics.

I am publishing aggregates only.

Closing

Pinning is not exotic anymore: 120 of 265 repos pin every third-party action, and GitHub now lets you enforce it with a checkbox. The gap is in fast-moving AI repos, and in workflows nobody has touched since they were copied from a README.

If you maintain a popular repo that still uses @v4: what is actually stopping you, the review noise from update PRs, or something else?

Related: a 1-day cooldown would have blocked 70% of 2026's hijacked npm releases and the LiteLLM supply chain backdoor.