2026-07-29
Triage a held GitHub Actions run at its exact head SHA
Bind a potentially malicious held run to its immutable commit, review every executable change, and approve only when the run is explained.
GitHub Actions can now hold certain potentially malicious workflow runs before they start. The stated threat is a compromised GitHub credential used to push a workflow that steals CI/CD credentials or enables a further attack. A write collaborator can approve the held run, but write access is an authorization requirement, not evidence that the push is trustworthy. GitHub's July 28, 2026 changelog says approval is through an authenticated web session.
This article was source-reviewed on 2026-07-29. No held workflow was approved or executed for this publication.
Establish what is being held
Do not start from the current default branch, a pull request's latest commit, or a copied snippet from the Actions page. A held run is a claim about one run and one head commit. Record the run URL, repository, event, actor, triggering actor, workflow path, head branch, and head_sha before discussing approval. The workflow-run REST endpoint exposes the run and its head_sha; it is read-only with Actions read access. Get a workflow run
The following is an inspection template. Replace both placeholders, run it in a disposable clone or an otherwise appropriate local checkout, and do not paste credentials into the shell. It fetches Git objects but does not check out, run, approve, rerun, or cancel anything.
set -euo pipefail
REPO='OWNER/REPOSITORY'
RUN_ID='1234567890'
REMOTE='origin'
RUN_JSON="$(gh api "repos/$REPO/actions/runs/$RUN_ID")"
HEAD_SHA="$(jq -r '.head_sha' <<<"$RUN_JSON")"
BASE_SHA="$(jq -r '.pull_requests[0].base.sha // empty' <<<"$RUN_JSON")"
jq '{html_url, event, status, conclusion, path, head_branch, head_sha, actor: .actor.login, triggering_actor: .triggering_actor.login, pull_requests}' <<<"$RUN_JSON"
test "$HEAD_SHA" != 'null'
git fetch --no-tags --no-recurse-submodules "$REMOTE" "$HEAD_SHA"
git cat-file -e "$HEAD_SHA^{commit}"
git show --no-ext-diff --no-patch --format=fuller "$HEAD_SHA"
if [ -n "$BASE_SHA" ]; then
git fetch --no-tags --no-recurse-submodules "$REMOTE" "$BASE_SHA"
git cat-file -e "$BASE_SHA^{commit}"
git diff --no-ext-diff --no-textconv --find-renames --name-status "$BASE_SHA" "$HEAD_SHA"
git diff --no-ext-diff --no-textconv --find-renames "$BASE_SHA" "$HEAD_SHA"
else
printf '%s\n' 'No pull request base SHA was supplied by this run response. Do not invent a comparison base.' >&2
figh api makes a GET request unless fields are supplied, and --jq is documented output filtering. The command above uses neither write fields nor an approval endpoint. gh api The API may include a pull request and its base SHA for a pull-request run. If it does not, stop calling the diff a review against the pull request base. Establish a defensible comparison point from the event and repository history, document who chose it, then fetch and inspect that commit separately.
Treat a changed SHA as a new case. If the pull request is updated or the Actions UI points to another run, repeat the capture. An approval decision tied only to a branch name can approve code nobody reviewed.
Review what can execute, not only the workflow YAML
Read the complete diff before narrowing it. A workflow edit is an obvious execution change, but a workflow can execute repository scripts, local actions, package lifecycle hooks, build configuration, containers, and downloaded code that changed elsewhere in the same commit.
Start with .github/workflows/, then trace every uses: and run: target into the held tree. Review local actions, shell scripts, JavaScript or Python entry points, package.json scripts, dependency manifests and lock files, Dockerfiles, Compose or build configuration, and generated files that the workflow consumes. Resolve reusable workflows and third-party actions to the exact references used by the held commit. A full-length action commit SHA is GitHub's documented immutable release form; a tag remains movable. Secure use reference
Look for capability changes rather than suspicious spelling alone:
permissions:at workflow or job scope, including a token changed from read-only to write access- secret, environment, OIDC, deployment, or cloud-credential paths, including a new
id-token: write - a privileged trigger such as
pull_request_targetorworkflow_run, especially if it checks out untrusted code - downloads, curl or package install steps, new action references, altered lock-file resolutions, and image changes
- artifacts and caches that cross an untrusted to privileged boundary, including any later workflow that consumes them
- Docker socket access, self-hosted runner labels, mounted paths, or network reachability changes
- outbound URLs, DNS names, telemetry endpoints, webhook destinations, encoded payloads, and commands that gather environment values or GitHub contexts
Do not dismiss a dependency or lock-file delta as routine because the YAML looks unchanged. GitHub warns that privileged pull_request_target and workflow_run workflows that check out untrusted code can expose repository secrets, write access, and the main-branch cache. It also says that artifacts from other workflows need caution. Secure use reference
Map each executable path to its authority: runner type, token permissions, available secrets and environment approvals, writable repository or package scope, trusted cache or artifact inputs, and reachable internal services. On a self-hosted runner, inspect the runner group and host boundary as part of the decision. GitHub does not guarantee a clean ephemeral environment for self-hosted runners and warns that untrusted workflow code can persistently compromise it. Secure use reference
Separate the two approval mechanisms
This hold is automatic. GitHub says it currently applies to public repositories on GitHub.com, requires no configuration, and is not added by GitHub Enterprise Server at this time. The announcement does not say that private repositories receive the same protection, so do not rely on it there. GitHub's July 28, 2026 changelog
Fork-run approval is separate. A contributor pull request from a fork may require maintainer approval according to repository, organization, or enterprise configuration. Its documentation tells a write maintainer to inspect the pull request's changed files, especially .github/workflows/, before approving. That page also says fork workflow runs awaiting approval for more than 30 days are deleted. Do not transfer that 30-day statement to every automatic potentially-malicious-run hold without a source that does so. Approving workflow runs from forks
Neither mechanism turns authorization into trust. The automatic hold specifically exists because a collaborator credential can be compromised. Approval through the web session is the final action after review, not a substitute for it. There is no CLI approval instruction here because GitHub's announcement specifies the authenticated web flow.
Keep an unexplained run blocked
If you cannot explain why the workflow, event, actor, or exact diff exists, keep the run blocked. A pre-execution hold means you should not assert that the held run exposed secrets. It does mean the change and the account or delivery path need investigation.
Preserve the identifiers and review record, then inspect adjacent pushes, pull requests, workflow runs, and changes around the same actor and time window. Check whether the altered workflow has already run at another SHA, whether an action reference or dependency changed in a related commit, and whether another branch contains the same executable path. Use the account security log and organization audit log where available to establish what changed and by whom; GitHub documents those logs as records of the action, time, and personal account. Secure use reference
Contain based on the scope you establish. Revoke active sessions or tokens for a suspected compromised account as appropriate. Rotate credentials that were exposed by another executed path, or that the suspicious code could have reached if it had run, according to their actual scope and provider controls. Review repository secrets, environment protections, cloud trust policies, deployment credentials, package credentials, runner access, and integration webhooks. Do not claim that a secret leaked merely because this particular run was held before execution.
Approval rule
Approve in the authenticated GitHub web session only when the run still names the recorded head_sha, the complete executable diff and every reachable authority are understood, the triggering event and actor are explained, and no reviewed path can exfiltrate or misuse credentials beyond its intended scope. Keep it blocked when any one of those conditions is missing, then investigate the push and account as a possible compromise.