Skip to content

I Read the CI of 300 Popular Repos. 145 Still Run Third-Party Actions by Tag or Branch

Published Last updated 9 min read

Key takeaways

  • 145 of 265 repos (54.7%) that use third-party actions reference at least one by a tag or branch, and in the AI half it is 88 of 135 (65.2%).
  • Across 5,640 third-party references, 64.1% are pinned to a full SHA: 55.0% in AI/LLM/MCP repos against 78.3% in general repos.
  • The median AI repo pins 23% of its third-party actions, while the median general repo pins 100%.
  • Repos with Dependabot or Renovate for actions pin 77.0% of third-party references to a SHA, against 37.2% without an updater.
  • 11 days after GitHub's workflow lockfile preview, 0 of the 300 repos have an actions.lock, and 83 of 123 pypa/gh-action-pypi-publish references use the release/v1 branch.

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%.

AllAI/LLM/MCPGeneral
Third-party refs5,6403,4382,202
Pinned to full SHA64.1%55.0%78.3%
By tag32.3%39.8%20.6%
By branch3.6%5.2%1.1%
Repos using third-party actions265135130
…with at least one unpinned ref145 (54.7%)88 (65.2%)57 (43.8%)
…fully SHA-pinned120 (45.3%)47 (34.8%)73 (56.2%)
…with zero SHA pins93 (35.1%)55 (40.7%)38 (29.2%)
Median repo’s SHA share75%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.

ActionReposRefsSHA share
docker/login-action9536961%
docker/setup-buildx-action8927861%
docker/build-push-action7922657%
astral-sh/setup-uv6247043%
pnpm/action-setup5553349%
pypa/gh-action-pypi-publish4812331%
oven-sh/setup-bun4730066%
softprops/action-gh-release467141%
anthropics/claude-code-action214161%

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:

AllAI/LLM/MCPGeneral
Workflows with top-level permissions:70.3%67.6%74.3%
Workflows with no permissions: at any level878 (18.5%)579 (20.4%)299 (15.6%)
Repos where every workflow sets it84 (28.0%)29 (19.3%)55 (36.7%)
Repos with a pull_request_target workflow1226458
…checking out the PR head541
Repos with untrusted context in run:761
Repos with .github/workflows/actions.lock000

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.

Frequently asked questions

How do I pin a GitHub Action to a commit SHA?
Resolve the tag to a commit with gh api repos/OWNER/ACTION/commits/v1.2.3 --jq .sha, check it comes from the action's own repo and not a fork, then write uses: OWNER/ACTION@<full 40-character sha> # v1.2.3 in the workflow, keeping the version as a comment.
Does Dependabot update SHA-pinned actions?
Yes. Add the github-actions package-ecosystem to .github/dependabot.yml (version 2, directory /, a weekly schedule). Dependabot updates actions/checkout@<commit> references and the version comment when it is on the same line. One catch: Dependabot alerts are only raised for actions referenced by semantic version, not for SHA pins. On Renovate, add the helpers:pinGitHubActionDigests preset.
How can I enforce SHA pinning across a repository or organization?
Since 15 August 2025 the allowed actions policy at repository, organization or enterprise level has a checkbox to require full-length SHA pinning, and unpinned workflows then fail. You can also block specific actions with a ! prefix. GitHub's workflow lockfile preview (.github/workflows/actions.lock, generated by gh actions-lock) covers transitive pins.
How many popular repos pin their third-party actions by SHA?
Of 265 repos using third-party actions, 120 (45.3%) pin every one to a full SHA and 145 (54.7%) have at least one tag or branch reference. Per reference, 64.1% of 5,640 are SHA-pinned, 32.3% use a tag and 3.6% a branch.
Share Share on X Share on LinkedIn

About the author

Portrait of Kiell Tampubolon

Security Engineer & Technical Writer

Security engineer and technical writer based in Batam, Indonesia. I review and harden MCP servers and AI agents, build security automation, and write up what holds and what fails. I also train on DNS security as a PANDI .id Academy trainer.

All writing | RSS | Markdown