Review AI-written GitHub Actions workflows for triggers, token permissions, untrusted input, pinned dependencies, runners, and release evidence.

A Workflow File Is Privileged Executable Code

An AI assistant can produce plausible workflow YAML from a sentence such as “test every pull request and deploy main.” The result may parse and even pass once while still granting a write token to the wrong job, evaluating attacker-controlled text as shell code, trusting an artifact from an unprivileged run, or executing pull-request code on a persistent runner. A green check reports what happened in one run. It does not establish that every actor, ref, input, dependency, or failure path is safe.

GitHub’s Secure use reference treats workflow authoring as a security boundary because jobs can reach repository tokens, secrets, deployment credentials, caches, artifacts, and networked systems. Review the file as code with authority, not as a formatting task. The trigger decides who can start the path; expressions and shell steps decide how their data is interpreted; permissions and environments decide what the path can change.

This is a practical review workflow, not a security or compliance guarantee. Repository visibility, organization policy, runner design, cloud configuration, branch protection, application architecture, and threat model change what is appropriate. AI may draft bounded YAML, test cases, and review notes. It must not receive secrets, select the acceptable risk, approve a deployment, or mark its own output RELEASE.

1. Freeze the Intended Event-to-Effect Contract

Start with a brief that exists outside the model conversation. Name the workflow’s one intended job: for example, compile untrusted pull-request code without credentials, publish a package after an approved tag, or deploy an exact commit after a protected build. Record the repository, visibility, default branch, allowed event and activity type, allowed actor class, source and target refs, runner class, expected inputs, output artifact, destination, and owner.

Then record the maximum effect. Which repository scopes need read or write access? Which environment, cloud role, package registry, cache, artifact store, or network destination may the job reach? Which steps may use a secret or request an OIDC token? What must never happen on a fork, unreviewed ref, failed upstream run, or changed workflow file? If the brief says “normal CI permissions” or “deploy access,” it is not frozen enough to review.

Keep event identity separate from commit identity. A branch name, event SHA, pull-request head SHA, and default-branch SHA can refer to different objects. The reviewer should be able to trace one authorized event to one reviewed revision and one bounded effect.

2. Choose the Trigger From the Trust Boundary

Read the current GitHub event documentation for the exact trigger instead of relying on a remembered template. For pull requests from forks, a pull_request workflow normally receives no repository secrets other than a read-only GITHUB_TOKEN. That is not a universal guarantee: private repositories can be configured to send write GITHUB_TOKENs and secrets to workflows from fork pull requests, so verify repository and organization fork-workflow settings before treating this lane as unprivileged. Every checked-out file, build script, test, filename, branch name, and artifact should still be treated as untrusted.

pull_request_target is materially different. It runs in the context of the base repository’s default branch and is useful for base-context operations such as labeling or commenting. GitHub explicitly says to avoid it when the workflow needs to build or run code from the pull request, and warns that executing untrusted code there can expose write privileges or secrets and enable cache poisoning. Do not “fix” a missing-secret problem by switching triggers and then checking out the pull-request head.

workflow_run can separate an unprivileged build from a later privileged action, but the boundary must be designed, not assumed. GitHub documents that the later workflow can access secrets and write tokens even when the earlier workflow could not. It also warns about running untrusted code and consuming untrusted artifacts in that privileged stage. workflow_run has requested, in_progress, and completed activity types; when types is omitted, all three trigger, and the consuming workflow file must exist on the default branch. For a post-build privileged consumer, specify types: [completed], require github.event.workflow_run.conclusion == 'success', and verify the named upstream workflow, upstream event and branch, run ID, artifact identity, and expected producer before any effect.

3. Make Every Permission Explicit and Minimal

Inventory capabilities job by job. GitHub’s organization security guidance recommends explicitly declaring the minimum workflow permissions with the permissions key. Begin at no write authority or read-only repository contents where the task permits, then add a named scope only to the job that requires it. Do not grant a workflow-wide write token because one late step posts a status or publishes a release.

Review implicit access too. GitHub notes that an action can obtain the GITHUB_TOKEN through the github.token context even when the workflow author did not pass it as an input. A third-party action therefore runs inside the job’s authority boundary. Separate build, analysis, publishing, and deployment jobs when their trust or permission needs differ, and avoid sharing writable directories, caches, credentials, or a Docker socket across those boundaries.

4. Keep Untrusted Context Out of Generated Shell Code

Pull-request titles and bodies, issue text, branch names, commit messages, labels, email-like fields, and artifact contents can be controlled by someone outside the trusted workflow. If an expression inserts one of those strings directly into an inline shell script, the resulting script can have different syntax than the author intended. Quoting that looks plausible in YAML may not survive expression evaluation and the target shell.

GitHub’s secure-use guidance prefers passing a context value to a purpose-built action as data. For inline scripts, it recommends placing the expression in an intermediate environment variable and then handling that variable according to the shell’s rules. That pattern reduces one injection path; it is not a universal sanitizer. Also validate the expected type, length, character set, and meaning, and keep untrusted values out of command names, option names, file paths, evaluators, templates, and dynamically constructed network requests.

Build negative fixtures that contain quotes, spaces, line breaks, wildcard characters, option-like prefixes, path traversal segments, and strings that resemble expressions. The expected result should be rejection or literal data handling, never an extra command or a different target. Review the rendered logs and actual process arguments without printing sensitive values.

5. Draw a Separate Boundary Around Secrets and OIDC

Do not paste repository, environment, organization, registry, cloud, signing, or deployment secrets into an AI prompt. Give the model stable placeholders and a capability description such as “short-lived token permitted to upload to staging.” Keep secret names out too when they reveal internal systems without helping the draft. The controlled record—not the chat—should map placeholders to owners, storage locations, rotation rules, and authorized jobs.

Automatic log redaction is not guaranteed. GitHub warns that transformed or structured secret values may not match the registered value and can escape redaction. Test valid and invalid paths, inspect standard output and error output, and register generated sensitive values for masking where the platform supports it. If a secret appears unredacted, treat it as exposed: remove the log through the approved process and rotate the credential rather than assuming deletion alone ends the risk.

OIDC can replace some long-lived cloud credentials with short-lived, scoped tokens. Granting id-token: write lets the job request an OIDC JWT; despite the permission name, it does not itself grant write access to GitHub or cloud resources. The provider’s trust conditions and role mapping determine what the exchanged credential can do. Verify the workflow’s need for the permission, the cloud-side trust rule, allowed repository, ref or environment claims, audience, role, session duration, and destination. Do not grant OIDC in a general test job, and do not let AI invent the provider policy or approve the resulting cloud access.

6. Resolve Every Action and Reusable Workflow to Reviewed Code

Build a dependency ledger for each external repository-hosted action or reusable workflow reference: owner, repository, path, full commit SHA, human-readable release or tag for maintenance, source-review date, reviewer, inputs, outputs, permissions, network behavior, and update route. GitHub states that a full-length commit SHA is currently the only way to consume a repository-hosted action as an immutable release. Verify that the SHA belongs to the intended repository rather than a fork.

A full Git commit SHA does not apply to local action paths, same-repository reusable-workflow paths, or docker:// references; record and verify the exact code or immutable image identity those forms actually resolve to. A fixed reference gives the reviewer a stable dependency. It does not prove the code, maintainer, inputs, or effects are trustworthy.

A tag is convenient but movable. A verified-creator badge identifies the publisher; it does not freeze the code or establish that the action fits this job. Review source in proportion to its authority, including secret access, outbound requests, executable downloads, shell construction, artifact extraction, workspace writes, and cache keys. Apply the same review and pinning principles to reusable workflows.

7. Treat the Runner as Part of the Security Decision

Write down whether the job uses a GitHub-hosted or self-hosted runner, its image or labels, installed tools, network reach, workspace lifecycle, and cleanup owner. GitHub says its hosted runners use ephemeral, clean isolated virtual machines. It also says self-hosted runners do not carry that guarantee and can be persistently compromised by untrusted workflow code.

GitHub advises that self-hosted runners should almost never serve public repositories and urges caution even for private or internal repositories whose readers can fork and open pull requests. Inventory private keys, tokens, cloud metadata endpoints, internal services, package caches, build caches, container sockets, signing devices, and other jobs that share the machine. Runner groups can narrow which repositories may schedule work, but they do not clean a compromised host.

8. Test Denials and Failure Paths With Safe Fixtures

Use an isolated test lane with synthetic data, non-production destinations, no customer information or production signing material, and credentials that cannot affect real releases. Review the parsed workflow, then run both intended-success and expected-denial cases:

  • a trusted push or pull request with the exact allowed ref and ordinary inputs;
  • a fork, unexpected actor, disallowed branch, lookalike tag, or stale commit;
  • malformed titles, branch names, filenames, inputs, and artifact metadata;
  • a job with each unnecessary permission removed and each required permission deliberately denied;
  • a missing secret, rejected OIDC claim, unavailable dependency, network failure, timeout, cancellation, and re-run;
  • a failed upstream workflow, substituted artifact, changed digest, duplicate upload, and expired artifact;
  • concurrent runs that target the same environment, release name, cache, or mutable destination.

Write the expected event, checked-out commit, token scopes, runner, network calls, files changed, artifact digest, destination state, and log markers before each test. Compare expected with observed. A negative test passes only when the prohibited effect does not occur and the failure is visible enough for an operator to understand. “The step failed” is inadequate if it failed after publishing, left a partial release, or exposed a value in logs.

Do not promote a fixture artifact into production. Repeat the final validation against the exact release candidate under the approved release lane, with real authority introduced only at the latest necessary point. Passing tests reduce uncertainty about tested cases; they do not prove the absence of an untested path.

9. Verify Artifact Identity and Provenance, Not Just Job Success

Give every artifact an evidence record: repository, source commit, workflow path and workflow revision, event and run ID, builder or runner identity, declared inputs, resolved dependencies where available, build time, artifact name, cryptographic digest, retention location, and destination. Reject an artifact when its identity cannot be tied to the reviewed run or when a privileged consumer receives unexpected files, paths, parameters, or producers.

The approved SLSA version 1.2 specification describes build and source tracks plus attestation formats for software-supply-chain assurance. Its build model connects an artifact to a build platform, process, and inputs through provenance. Verification should authenticate the provenance envelope’s signature using configured roots of trust, confirm its subject matches the artifact digest, and check predicateType before comparing builder identity, canonical source repository, buildType, externalParameters, and any source revision or resolved dependencies required by the release contract against preconfigured expectations. A provenance file that is never verified is another attachment, not a release decision.

Keep build and release authorization separate. An authentic artifact can still be the wrong commit, use an unacceptable parameter, target the wrong platform, or lack destination approval. Compare its verified record with the frozen contract before publishing or deploying.

10. Review Logs as Evidence With a Data-Minimization Rule

Logs should let a reviewer reconstruct the event, selected revision, major decisions, dependency versions, test result, artifact digest, and release outcome without exposing credentials or unnecessary repository content. Inspect both successful and failed runs. Tools often send unexpected details to standard error, and masking can fail after encoding or transformation.

Preserve the small evidence packet needed for audit and incident response, not every environment dump. When a log contains sensitive data, follow the exposure process, rotate affected secrets, and correct the emitting step. Do not send raw private logs to AI unless that use and data are explicitly authorized.

11. Give AI a Drafting Role, Not an Approval Role

Provide the model with the sanitized event-to-effect brief, allowed trigger, placeholder capabilities, runner class, required actions by reviewed SHA, expected tests, and organization-approved conventions. Ask it to mark missing facts VERIFY, keep permissions explicit, and explain each job’s inputs and effects. Do not provide secrets, private tokens, proprietary logs, live cloud policies, or unrestricted repository context merely because the model asks for them.

Review the output as a proposal. Parse the YAML, inspect the event semantics against current GitHub documentation, resolve every action, trace untrusted values, calculate job permissions, and execute the safe test matrix. Model explanations are not evidence that the YAML behaves as described. A second AI critique can suggest questions, but it cannot be the independent security reviewer or the release approver.

12. Require Named RELEASE or HOLD Ownership

Assign decisions to roles. The repository owner confirms the intended event, refs, branch protections, and merge path. The workflow author explains every job and dependency. Security reviews trust boundaries, input handling, permissions, secrets, OIDC, runners, and failure impact. The platform or cloud owner verifies environments, identities, network reach, and destination controls. The release owner confirms the exact artifact and decides RELEASE or HOLD.

The decision record should name the workflow revision, evidence packet, reviewer, time, destination, residual risks, and re-review triggers. Put the workflow on HOLD when an actor or ref is ambiguous, untrusted code crosses into a privileged trigger, permissions exceed the brief, a dependency is unresolved, secrets appear in logs, runner state is unknown, artifact identity fails, or a required negative test produces an effect.

Re-review after trigger, permission, action SHA, reusable workflow, runner image, secret, OIDC trust rule, cache, artifact path, environment, branch policy, build command, or destination changes. Also re-review after a security advisory, suspicious run, leaked credential, unexpected outbound request, or changed organization policy. “It passed last month” is not a durable authorization.

A Worked Review: Split the Pull-Request Test From Release Authority

A team asks AI for one workflow that tests every pull request and publishes a preview. The draft uses pull_request_target, checks out the contributor’s head commit, grants write permissions at workflow level, loads a deployment secret, and runs the repository’s build script. It is compact and likely to work. It also crosses the central trust boundary: unreviewed code executes in a privileged base-repository context.

The reviewers return HOLD and restate the contract. The first lane uses pull_request to test untrusted code without deployment credentials, with minimal read authority and synthetic fixtures. It produces a narrowly specified artifact plus its source commit and digest. A separate privileged lane is allowed only after the organization’s approved review boundary. It does not execute the pull request, trusts no free-form artifact metadata, checks the upstream conclusion and expected producer, verifies the artifact identity, and receives only the permission needed for the preview destination.

The team then tests a fork, hostile-looking title, failed build, substituted artifact, denied token, duplicate preview name, and concurrent run. They inspect logs for secret-like values and verify cleanup. This redesign is not declared “secure” because the tests passed. The named owners record what they verified, the remaining assumptions, and why this exact revision is RELEASE for the bounded preview environment.

Fourteen Questions Before the Workflow Is Released

  • Does the brief name the exact event, activity, actor, ref, commit, runner, artifact, destination, and owner?
  • Does the trigger match the trust boundary documented by GitHub today?
  • Is untrusted pull-request code kept out of privileged pull_request_target and workflow_run paths?
  • Are workflow and job permissions explicit and no broader than the tested task?
  • Are context strings handled as data rather than inserted into generated shell syntax?
  • Were all secrets withheld from AI and limited to the jobs that require them?
  • Are OIDC permission and provider-side trust rules both narrow and reviewed?
  • Is every third-party action and reusable workflow reviewed and pinned to a full commit SHA?
  • Is the runner lifecycle appropriate for the repository and actor class?
  • Do negative tests show, for the tested cases, that prohibited effects are denied before they occur?
  • Were success and failure logs inspected for unintended sensitive output?
  • Does the artifact record bind the digest to the expected source, builder, process, and inputs?
  • Did a named human compare the exact release candidate with the frozen contract?
  • Does the record say RELEASE or HOLD and list concrete re-review triggers?

If any answer is missing, keep the workflow on HOLD. AI can accelerate a first draft and enumerate test cases. It cannot turn privileged automation into a trusted release path by sounding confident or by producing a green run.

Scope Notes and Primary Sources

NIST’s publications page also lists SSDF version 1.2 as a draft. This article uses final SSDF version 1.1 and does not describe the draft as final. The risk-based framework supports disciplined requirements, protection, testing, provenance, and response; it does not certify this workflow.

Review the Draft, Then Verify the Workflow

Use AI to improve wording and structure while keeping credentials, execution tests, security judgment, and release approval with accountable humans.

Open AI Humanizer