# Chainguard Actions overview

URL: https://chainguard-docs-preview-git-fix-ai-metadata-year.chainguard.app/chainguard/actions/overview.md
Last Modified: October 13, 2026
Tags: Chainguard Actions, Overview

Learn how Chainguard Actions provides hardened drop-in replacements for popular GitHub Actions to protect your CI/CD pipelines from supply chain attacks.


Chainguard Actions are a set of hardened drop-in replacements for popular GitHub Actions. Each action preserves the same inputs and outputs as the upstream version, but has been examined and revised to better protect your CI/CD pipelines from supply chain attacks. The only change in your workflow configuration is the name of the action in the `uses:` line.

The catalog holds more than 1,000 hardened actions. Coverage spans GitHub first-party (`actions/*`), cloud-provider (`aws-actions/*`, `azure/*`, `google-github-actions/*`), Docker, HashiCorp, and security tools actions (Trivy, Grype, CodeQL, Semgrep), as well as a growing catalog of community actions.

Each hardened action:

- Is pulled from the upstream source at a pinned commit, then reviewed by a static ruleset and an AI-powered analysis pass
- Has every internal `uses:` and container image reference pinned to an immutable SHA digest
- Ships with a `HARDENING.md` report documenting exactly what was checked and fixed
- Ships with a signed SLSA provenance attestation recording the upstream source and the ruleset version applied (releases published before signing began don't carry one)
- Is re-reviewed and re-hardened whenever upstream publishes a new version or Chainguard adds a new rule

Chainguard Actions protect against common threats including tag hijacking, dependency confusion, `pull_request_target` abuse, and secret exfiltration.

This page provides enough to get you started. Browse the [Chainguard Actions organization](https://github.com/chainguard-actions) to find a specific action and read its hardening report.

Chainguard Actions hardens workflows that stay on GitHub Actions. To move a repository's pull request checks off GitHub Actions runners instead, refer to [Migrate from GitHub Actions to Chainguard Checks](/chainguard/checks/migrate-from-github-actions/).

## What hardening checks and fixes

Every action in the catalog goes through the same two-stage review. A deterministic static pre-pass runs first and catches patterns mechanically. An AI-powered analysis pass then evaluates the action against the policy ruleset. Findings from either stage carry an ID that appears in the action's `HARDENING.md` report, so you can trace any change back to the check that produced it.

### Policy checks

| Check | Finding IDs | Severity | What it catches |
| ----- | ----------- | -------- | --------------- |
| Unpinned uses | `unpinned-uses` | High | A `uses:` reference or a `runs.image:` container reference pointing at a mutable tag or branch instead of an immutable commit SHA or image digest. The `docker://` prefix is optional, so `image: ghcr.io/example/tool:latest` is a finding too. |
| Script injection | `script-injection` | High | Expressions such as `${{ inputs.name }}` interpolated directly into a `run:` block, where the shell can parse attacker-controlled text as commands. |
| Unsafe shell | `unsafe-shell` | High | Remote content piped straight into an interpreter, such as `curl ... \| bash`. |
| Hardcoded credentials | `hardcoded-credentials` | High | Literal secrets assigned to names containing `password`, `secret`, `token`, `api_key`, or `aws_secret`. |
| GitHub environment injection | `github-env-injection` | High | Untrusted values written to `$GITHUB_ENV`, `$GITHUB_PATH`, or `$GITHUB_OUTPUT` without newline sanitization, which lets an attacker inject variables into later steps. |
| Suspicious run content | `suspicious-run-content` | High | Malicious patterns in `run:` blocks, including obfuscated execution, process memory access, dynamic evaluation, credential file access, outbound exfiltration, reverse shells, persistence, and environment secret scraping. |
| Permissions | `permissions`, `missing-permissions`, `broad-permissions` | Medium | Workflows that leave `GITHUB_TOKEN` at its default permissions, or that set `read-all` or `write-all` instead of specific scopes. |

### Static pre-pass checks

The static pre-pass complements the analysis pass by catching patterns the analysis may miss. It reports one finding per occurrence, so a report can list the same ID many times, each with its own location. `static-inline-injection` is the most common finding in the catalog for that reason.

| Finding ID | Severity | What it catches |
| ---------- | -------- | --------------- |
| `static-inline-injection` | High | A single expression interpolated directly into a `run:` block. The finding names the expression and the step it appears in, and the fix moves the value into an `env:` map. |
| `static-unsanitized-env-write` | Medium | An unsanitized write to a GitHub environment file. |
| `invalid-yaml` | High | An action or workflow file that could not be parsed. |

Both stages read the action definition (`action.yml` or `action.yaml`) and every workflow under `.github/workflows/` in the action's own repository. Neither inspects built or vendored output: `dist/`, `vendor/`, and `node_modules/` are out of scope, as are the action's test fixtures.

Findings are fixed in place and the pipeline re-evaluates its own work, so a single hardening run can take several iterations before an action passes. Both the findings and the per-iteration notes are recorded in `HARDENING.md`.

## Transitive dependencies

Hardening the top-level action is only part of the problem. Actions pull in other actions and container images behind the scenes, and those transitive dependencies are a common path into a pipeline. Chainguard hardens the references it can see in the action definition:

- **Container images are pinned to digests.** When an action runs a registry image (`runs.using: docker` with a `docker://` image), the pipeline resolves the tag to its digest and rewrites the reference as `image:tag@sha256:<digest>`. A retagged image can't change what the action pulls.
- **Nested actions are pinned, and swapped for hardened copies where they exist.** Every nested `uses:` reference is pinned to an immutable commit SHA, with the original tag kept as a comment. In composite actions, when Chainguard has hardened the nested action, the reference points at the hardened copy instead of upstream. For example, the hardened `actions/upload-pages-artifact` calls `chainguard-actions/actions-upload-artifact`, not `actions/upload-artifact`.
- **The action's own workflows are reviewed too.** Both stages of the review cover the workflows under `.github/workflows/` in the action's repository, not only the action definition.
- **Missing dependencies can be onboarded.** When a hardened action depends on an action that isn't in the catalog yet, [request that action](https://github.com/chainguard-actions/.github/issues/new?template=new-action.yml) and Chainguard hardens and publishes it.

### How nested actions are swapped for hardened copies

When Chainguard hardens and publishes an action, every hardened action that depends on it goes back through hardening so its `uses:` reference can point at the hardened copy. Three limits apply:

- It covers composite actions only. Node and Docker actions have no `uses:` steps to rewrite.
- It applies only when a hardened copy exists for that exact upstream commit.
- It never rewrites a reference when the correct copy is ambiguous.

Coverage grows as more of the catalog is hardened, so read the `action.yml` on the version branch you plan to use to see what it references today.

### What hardening doesn't change

Hardening doesn't change the packages an action bundles or installs. A JavaScript action ships the same `dist/` bundle as its upstream release, and an action built from a Dockerfile uses the same base image as upstream. Packages and binaries an action downloads while it runs are outside what the review inspects.

To see the full dependency graph for your own repository, including actions reached through other actions, use the `--recursive` flag described in [View the actions you are currently using](#view-the-actions-you-are-currently-using-in-a-repository).

## Prerequisites

To follow this guide, you need:

- `chainctl` **v0.2.261** or later, installed and authenticated. Refer to [How to install `chainctl`](/platform/chainctl-usage/how-to-install-chainctl/) if you don't have it yet.
- An active Chainguard organization.
- Owner access on the organization.

## Set up Chainguard Actions

Setting up has two parts: entitle your organization, then choose how you migrate your workflows.

### Step 1: Create the Actions entitlement

Authenticate using `chainctl`:

```shell
chainctl auth login
```

Create the Chainguard Actions entitlement for your organization:

```shell
chainctl actions entitlements create
```

The output confirms the entitlement:

```output
Enabled Actions product for org chainguard.edu ($ENTITLEMENT_ID) [entitlement id: $ENTITLEMENT_ID]
```

Confirm your entitlement:

```shell
chainctl actions entitlements list
```

```output
                    ID                    |         CREATED
------------------------------------------|-------------------------
 $ENTITLEMENT_ID                          | 2026-06-18 17:33:24 UTC
```

### Step 2: Install the Chainguard App

The [Chainguard App](/chainguard/guardener/github/getting-started/) is the recommended way to adopt Chainguard Actions across more than a repository or two. Once you install it and link it to your Chainguard organization, Guardener:

- Inventories the actions your workflows use across every repository it can access
- Comments on pull requests that introduce unhardened actions, so your workflows don't drift back
- Opens and maintains a pull request that swaps in Chainguard hardened equivalents, once you enable migration

To set it up:

1. Install the [Chainguard App](https://github.com/apps/chainguard) on your GitHub organization.
2. Link your Chainguard organization to your GitHub organization with `chainctl guardener github link`.
3. Add a `.chainguard/actions.yaml` file to each repository you want Guardener to work on, or once to your organization's `.github` repository to apply it to every repository.

Both of the last two steps matter. Installing the app changes no repository on its own, and the Actions feature stays inert until a `.chainguard/actions.yaml` applies to the repository, either in the repository itself or through the [org-level `.github` configuration](/chainguard/guardener/github/configuration/#organization-level-configuration-with-the-github-repository). Once it does, pull request recommendations are on by default, but automated migration pull requests need `migrate.enabled: true` set explicitly:

```yaml
enabled: true
migrate:
  enabled: true
```

If installing an app in your organization needs an administrator's approval, they will be asked to approve a specific set of GitHub permissions. [Permissions Guardener requests](/chainguard/guardener/github/getting-started/#permissions-guardener-requests) lists each one and why it's needed, so you can take that to them before you start.

Refer to [Getting started with Guardener](/chainguard/guardener/github/getting-started/) for the installation and linking steps, and to [Hardened Actions](/chainguard/guardener/github/actions-security/) for the configuration reference, the migration options, and the on-demand migration command.

The Chainguard App is in beta. It runs in production and is supported, but its features and configuration may still change.

If you'd rather not install a GitHub App, you can migrate with the [cg-actions](https://github.com/chainguard-dev/cg-skills/tree/main/skills/cg-actions) skill or by hand. Both approaches are covered in [Configure your workflows to use Chainguard Actions](#configure-your-workflows-to-use-chainguard-actions).

## Basic usage (quick start)

To use a Chainguard hardened action, edit your workflow's YAML configuration file and change the `uses:` line to match the location in `chainguard-actions`:

```yaml
- uses: chainguard-actions/<action-name>@<commit-sha> # <version-tag>
```

Repository names are prefixed with the upstream organization, so `tj-actions/changed-files` becomes `tj-actions-changed-files`. This keeps two different sources of a `changed-files` action from clashing in the Chainguard Actions organization.

Search the Chainguard Actions repository, find the action you want to use, and then use the name you find there.

> **Note:** Don't reference a hardened action with `@main`. The main branch of each repository holds only metadata (`README.md`, `LICENSE_CHAINGUARD`, and `source.json`). The hardened action itself lives on the version branches, so a reference to `@main` fails to resolve.

Pin to the commit SHA that a version tag resolves to, and keep the tag as a comment. [Choose how to reference an action](#choose-how-to-reference-an-action) explains why, and [Replace the `uses:` line in each workflow](#replace-the-uses-line-in-each-workflow) shows how to look up the SHA.

## Choose how to reference an action

Pin to a commit SHA, and pair the pin with Dependabot or Renovate:

```yaml
- uses: chainguard-actions/actions-checkout@<sha> # v4
```

A commit SHA is the only immutable reference in the catalog. Tags are mutable by design, and not just the floating major version. Chainguard re-hardens published versions in place and moves the tag when it does, including fully qualified patch tags, so a single upstream release can be re-hardened several times with the same tag pointing somewhere new each time. Both `@v4` and `@v4.3.1` resolve to whatever was published most recently.

Pinning on its own isn't enough, though. A pin with no tooling behind it is the one configuration that strands you: you stay on that build, and stop receiving re-hardening, until someone updates the SHA by hand. Dependabot and Renovate both track a pinned SHA against its tag and open a pull request when the tag moves, so you receive every re-hardening as a change your own CI validates before it reaches a live workflow. That gives you more control than a mutable tag does, and costs you nothing in freshness.

Pinning is also the standard Chainguard applies to the actions it hardens. The `unpinned-uses` check fails any `uses:` reference on a tag, so if you run an actions linter against your own repository, referencing a hardened action by tag will register a finding.

Use the canonical repository name in the reference. Some catalog repositories answer to an older name through a GitHub rename redirect — `chainguard-actions/checkout` reaches `chainguard-actions/actions-checkout`, for example — but a redirect isn't something to depend on in a pinned workflow.

## Configure your workflows to use Chainguard Actions

You can save some time by using the optional [cg-actions](https://github.com/chainguard-dev/cg-skills/tree/main/skills/cg-actions) skill, a Claude Code skill for auditing GitHub Actions usage and migrating to Chainguard hardened actions.

Following the steps in this section will achieve a similar result.

### Inventory the actions you currently use.

Run this from the root of your repository to get a deduplicated list of every `uses:` line across every workflow:

```shell
grep -rhE "uses:\s*[^@]+@" .github/workflows/ | sort -u
```

For a more thorough inventory that also follows composite actions, use [`chainctl actions discover`](#view-the-actions-you-are-currently-using-in-a-repository).

### Check the Chainguard Actions catalog for each action.

Browse [the Chainguard Actions repository](https://github.com/chainguard-actions) or use the GitHub search UI. Match by organization and action name — for example, if you use `tj-actions/changed-files`, search for `org:chainguard-actions tj-actions-changed-files`.

If the action isn't in the catalog, [open an issue](https://github.com/chainguard-actions/.github/issues/new?template=new-action.yml) to request it.

### Replace the `uses:` line in each workflow.

Change the `uses:` line to match the location in `chainguard-actions`. Pin to the commit SHA and preserve the original tag as a comment so Dependabot, Renovate, and human reviewers can track upgrades:

```yaml
# Before
- uses: tj-actions/changed-files@v47
```

```yaml
# After
- uses: chainguard-actions/tj-actions-changed-files@<SHA> # v47
# originally - uses: tj-actions/changed-files@v47
```

To find the SHA digest for a specific release, use the `gh` CLI:

```shell
gh api repos/chainguard-actions/tj-actions-changed-files/commits/v47 --jq '.sha'
```

```output
4b4bd2ed96c7629e1c911f97f2390b91e1362735
```

For the short SHA digest:

```shell
gh api repos/chainguard-actions/tj-actions-changed-files/commits/v47 --jq '.sha[:7]'
```

```output
4b4bd2e
```

The resulting `uses:` line with the full SHA digest:

```yaml
- uses: chainguard-actions/tj-actions-changed-files@4b4bd2ed96c7629e1c911f97f2390b91e1362735 # v47
```

Run the command rather than copying the digest shown here. Because Chainguard re-hardens a published version in place and moves its tag, the SHA a version tag resolves to changes each time that version is re-hardened.

### Update your allowed-actions list.

If your GitHub organization or repository restricts which actions can run (**Settings > Actions > General > Allow select actions**), add `chainguard-actions/*` to the allowed patterns. Without this, workflows fail with a policy error on first run.

> **Note:** It's a good idea to remove allowed actions that are no longer being used.

### Commit, open a PR, and verify that CI passes.

The action's inputs, outputs, and behavior are almost always identical to the upstream version, so no other workflow changes are typically needed.

However, read the `HARDENING.md` file for each hardened action before migrating. In rare cases, the hardening process requires a change to inputs, outputs, or behavior — those changes are documented in this file.

If something breaks, [file an issue](https://github.com/chainguard-actions/.github/issues/new?template=action-issue.yml) with a reproducer.

## View the actions you are currently using in a repository

Use `chainctl` to scan every workflow and composite action in a repository and list the actions and container images they reference:

```shell
chainctl actions discover $GIT_ORGANIZATION/$REPO
```

```output
    scanning $GIT_ORGANIZATION/$REPO for workflows and actions
               ACTION                    | REQUESTED | USED BY
    -------------------------------------|-----------|---------
     actions/checkout                    | v4        | 1
     chainguard-actions/actions-checkout | v6.0.2    | 1

    2 actions, 0 container images

```

The command needs a GitHub token, which it reads from `$GITHUB_TOKEN` or from `gh auth token`. The target can be a local directory (the current directory by default), an `owner/repo` pair, or a single action reference such as `actions/checkout@v4`.

By default, `discover` lists only the actions your workflows reference directly. Add `--recursive` to follow each referenced action into its own definition and resolve the full transitive dependency graph:

```shell
chainctl actions discover $GIT_ORGANIZATION/$REPO --recursive
```

A recursive scan makes many GitHub API calls, so it caches responses and stops after `--timeout` (five minutes by default). Refer to [`chainctl actions discover`](/platform/chainctl/chainctl-docs/chainctl_actions_discover/) for the full set of flags.

## View the actions currently available

While you can search the [Chainguard Actions repository](https://github.com/chainguard-actions) directly in GitHub, you can also use `chainctl` to find an action.

```shell
chainctl actions catalog list --upstream-owner=$OWNER
```

For example, to list all the actions from the `tj-actions` source:

```shell
chainctl actions catalog list --upstream-owner=tj-actions
```

This example returns a list of all actions in the Chainguard Actions repository that originate from the `tj-actions` upstream source.

To list the catalog entries available to a specific organization rather than the whole public catalog, use [`chainctl actions list`](/platform/chainctl/chainctl-docs/chainctl_actions_list/):

```shell
chainctl actions list --parent $ORGANIZATION
```

## What ships in each hardened action

Each hardened action's repository has a main branch and one branch per hardened version. The hardened action lives on the version branches; the main branch holds only metadata.

The main branch of each repository contains:

- `README.md` — a pointer to the action and its upstream source
- `LICENSE_CHAINGUARD` — the Chainguard license for the hardened variant
- `source.json` — a manifest naming the upstream owner, repository, version, and commit, along with the policy SHAs applied

Each version branch contains:

- `HARDENING.md` — the authoritative, per-action record of what was checked, what was fixed, and how, including the policy SHA that pins the exact ruleset applied
- `action.yml` or `action.yaml` — the hardened action definition, preserving upstream inputs and outputs with fixes applied
- `attestations/provenance.intoto.jsonl` — a signed [SLSA provenance](https://slsa.dev/provenance/v1) attestation naming the upstream repository and commit, the ruleset version, the build times, and a SHA-256 digest for every file in the hardened action
- `LICENSE_CHAINGUARD` — the Chainguard license for the hardened variant
- The upstream action's own files, including its license and any documentation you can adapt for the hardened version

Because `HARDENING.md` and the attestation are per-version, read them on the version branch you plan to use rather than on the main branch.

Nearly every version branch in the catalog carries an attestation. A small number of older releases were published before Chainguard began signing them, so if a version branch has no `attestations/` directory, treat that release as unverifiable rather than as verified.

Chainguard doesn't publish a customer-facing verification procedure yet. Verification requires the signing key's fingerprint, which isn't published, so there is no complete recipe to follow today. Tooling for this is planned. In the meantime the attestation is still useful as a record: it names the upstream commit the release was built from and the ruleset version that was applied.

## The continuous re-hardening process

Chainguard Actions are continuously re-hardened:

- When upstream publishes a new version, the pipeline re-runs and publishes a new hardened version
- When the hardening ruleset is updated, every action in the catalog is re-evaluated against the new ruleset and re-hardened as needed
- The `HARDENING.md` report is regenerated on every hardening run, with its own policy SHA pinning the exact set of rules that were applied

Because the policy SHA is computed over the ruleset itself, any change to a rule produces a new SHA, which is what triggers the catalog-wide re-evaluation.

## Request a new action or report an issue

To request an action that isn't in the catalog, [open a new action issue](https://github.com/chainguard-actions/.github/issues/new?template=new-action.yml).

If an action isn't working as expected, [open an action issue](https://github.com/chainguard-actions/.github/issues/new?template=action-issue.yml) with the action reference, a description of the problem, and steps to reproduce.

## Learn more

- [Chainguard Actions telemetry and privacy](/chainguard/actions/telemetry/)
- [Hardened Actions with Guardener](/chainguard/guardener/github/actions-security/)
- [Chainguard Actions product page](https://www.chainguard.dev/actions)
- For other questions, [contact Chainguard](https://www.chainguard.dev/contact?utm=docs).

