# Ratchet: pinning CI/CD versions without losing the constraint you asked for

> Ratchet is a Go tool that rewrites unpinned CI references such as actions/checkout@v7 into a commit SHA, and records the original constraint in a trailing comment so the pin can be refreshed later. It covers GitHub Actions, Circle CI, GitLab CI, Google Cloud Build, Drone and Tekton, and it can refuse to adopt a version that was published minutes ago.

**sethvargo/ratchet** — A tool for securing CI/CD workflows with version pinning.

- Repository: https://github.com/sethvargo/ratchet
- Stars: 970 · Forks: 51
- Language: Go
- License: Apache-2.0
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/sethvargo-ratchet

## CI definitions are mutable by default

The problem Ratchet is built around is described in one blunt sentence in the README: most CI/CD systems are one layer of indirection away from `curl | sudo bash`. Unless you specifically pin workflows, containers and base images to checksummed versions, everything downstream of a tag is mutable. GitHub labels are mutable, Docker tags are mutable, so a workflow that references `actions/checkout@v7` is not referencing a fixed artefact, it is referencing whatever that tag points to at the moment the job runs.

The pinned form looks like this:

```yaml
uses: 'actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0'
# or
image: 'ubuntu@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90'
```

The commit SHA for the action and the digest for the image are the fixed points. The problem is not writing them, it is the update lifecycle afterwards: resolving a new checksum, keeping the record of what you wanted, and doing that across every workflow in every repository without turning it into toil. That toil is what Ratchet aims to remove.

## The constraint travels with the pin in a comment

Here is the mechanism that makes the tool more than a search and replace. After pinning, Ratchet leaves the original constraint in a comment on the same line:

```yaml
uses: 'actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0' # ratchet:actions/checkout@v7
# or
image: 'ubuntu@sha256:4fbb8e6a8395de5a7550b33509421a2bafbc0aab6c06ba2cef9ebffbc7092d90' # ratchet:ubuntu:24.04
```

That single comment carries the intent. The workflow says `actions/checkout@v7` and the pin says this is the commit that satisfies it. When a new v7 release appears, the tool knows both what the constraint was and what the current answer to it is, so it can refresh the SHA without a human remembering that v7 was the requirement.

It also means the pin is self describing when you read the file. A reviewer sees the SHA in the reference and the human readable version in the comment, without having to look anything up.

The same annotation syntax is used for exclusions. If a particular reference should never be pinned, you mark it with the `ratchet:exclude` comment and Ratchet leaves it alone. That is the escape hatch for references that cannot be pinned, and it is visible in the file rather than hidden in a configuration file elsewhere.

## pin, update and upgrade do three different jobs

Three commands cover the workflow, and confusing them is the most likely early mistake.

`pin` pins unpinned references to specific versions. `update` updates all versions to the latest matching constraint, which means it reads the constraint comment and re-resolves it, keeping the same version range. `upgrade` upgrades all versions to the latest version, changing the ratchet comment as well as updating the ref, so it deliberately moves the constraint forward. The README notes that upgrade only works with GitHub Actions references and does not support container or Docker based references.

The pin command takes a parser and an output file:

```shell
# pin the input file
ratchet pin workflow.yml

# pin a circleci file
ratchet pin -parser circleci circleci.yml

# output to a tekton file
ratchet pin -out -parser tekton tekton.yml

# output to a different path
ratchet pin -out workflow-compiled.yml workflow.yml
```

Updating resolves the constraint again, and unpin reverses the process:

```shell
# update the input file
ratchet update workflow.yml

# update a circleci file
ratchet update -parser circleci circleci.yml

# output to a different path
ratchet update -out workflow-compiled.yml workflow.yml
```

To undo a pin entirely:

```shell
# unpin the input file
ratchet unpin workflow.yml

# output to a different path
ratchet unpin -out workflow.yml workflow-compiled.yml
```

One detail deserves attention. The README notes that upgrade performs an update if the constraint ref is for a branch, which is the sensible fallback when there is no release to move to.

## One parser per CI system, and a linter to catch drift

Ratchet supports six CI systems: Circle CI, GitHub Actions, GitLab CI, Google Cloud Build, Harness Drone and Tekton. Each has its own parser, selected with the `-parser` flag, and the examples cover `circleci`, `cloudbuild`, `drone`, `gitlabci` and `tekton`, with GitHub Actions as the default for the plain `workflow.yml` case.

The parser set explains why Ratchet describes itself as being like Bundler, Cargo, Go modules, NPM, Pip or Yarn, but for CI/CD workflows. Each of those tools pins dependencies to a resolved version and keeps the requested range somewhere. Ratchet applies that idea to workflow definitions, where the equivalent of a lock file is a comment next to the SHA.

The `lint` command is the enforcement half. It reports whether all versions are pinned, prints any violations, and exits with a non-zero error code when entries are not pinned:

```shell
ratchet lint workflow.yml
```

That non-zero exit is the point. Linting in a pull request turns unpinned references into a failing check rather than a review comment that somebody can wave through, which is the only way this kind of pinning survives contact with a busy team. The command and its options are documented through the `-help` flag on each subcommand.

## Bake delay refuses versions that are too fresh

The `-bake-delay` flag, or the `RATCHET_BAKE_DELAY` environment variable, is the most interesting safety feature. When running pin, update or upgrade, it refuses to resolve any commit, release or image younger than the given duration. The idea is that a version should sit for a while before you adopt it, which lowers the risk of pinning something that was compromised and published minutes ago.

```shell
# only adopt versions at least 24 hours old
ratchet update -bake-delay 24h workflow.yml

# also configurable via the environment
RATCHET_BAKE_DELAY=24h ratchet upgrade workflow.yml
```

The README names the equivalents in other ecosystems, pnpm calls it `minimumReleaseAge` and Dependabot calls it `cooldown`. The feature is off by default, so the delay is an explicit choice rather than something you inherit.

The documentation is honest about the limits of the timestamp it uses. For GitHub Actions, Ratchet uses the most trustworthy time it can find: a release's `published_at`, which is set by GitHub and not forgeable, then an annotated tag's date, then the commit date. Only the release date is tamper proof, so a bake delay reduces risk but does not guarantee against a compromised upstream. Two specific gaps are named. It does not detect a tag repointed at an already old commit, and a moving major tag like `v4`, which has no matching release, falls back to the commit date. The window is real but partial, and knowing which cases fall outside it is more useful than assuming the delay closes everything.

## Running Ratchet as a container or inside a CI job

Ratchet is distributed as a very small container, and the Dockerfile in the repository builds from scratch, copying only the certificate authorities and the binary itself. The image entry point is the ratchet binary, and the file carries a comment explaining why it does not drop to an unprivileged user: it has to run as the default user to play nicely with GitHub Actions.

The README shows the pattern for using it as a step in a GitHub Actions job:

```yaml
jobs:
  my_job:
    runs-on: 'ubuntu-latest'
    name: 'ratchet'
    steps:
      - uses: 'actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0'
      - uses: 'sethvargo/ratchet@main' # ratchet:exclude
        with:
          files: '.github/workflows/*.yml'
```

Note the `ratchet:exclude` annotation on the action step itself, which stops the tool from trying to pin the very action that performs the pinning. The same pattern extends to other CI systems by downloading the binary from the releases page.

For local use, the container can be run directly, and the README suggests a shell alias to keep the command short:

```shell
docker run -it --rm -v "${PWD}:${PWD}" -w "${PWD}" ghcr.io/sethvargo/ratchet:latest COMMAND
```

```shell
function ratchet {
  docker run -it --rm -v "${PWD}:${PWD}" -w "${PWD}" ghcr.io/sethvargo/ratchet:latest "$@"
}
```

Installation is also possible with `brew install ratchet`, with `nix run 'github:NixOS/nixpkgs/nixpkgs-unstable#ratchet' -- --help`, with `go install github.com/sethvargo/ratchet@latest`, or from a single static binary on the releases page. The README marks the Homebrew and Nix routes as community supported and possibly not the latest version, and compiling from source yourself as not supported.

## Authentication decides which references Ratchet can resolve

Two resolvers matter for what the tool can actually do, and both need credentials for anything beyond public data.

The container resolver uses default keychain auth, which looks for local system auth in the same way the Docker and gcloud command line tools do. The GitHub resolver defaults to public github.com, and you provide an oauth access token with appropriate permissions through the `GITHUB_TOKEN` environment variable. To point at a GitHub Enterprise installation you set `ACTIONS_BASE_URL` and `ACTIONS_UPLOAD_URL` to your instance.

The dependencies confirm the shape of the tool. The Go module requires github.com/google/go-github/v89 for the GitHub side and github.com/google/go-containerregistry for the container side, along with golang.org/x/oauth2 for the token handling and github.com/braydonk/yaml for parsing the workflow files. The repository separates concerns into directories that match those roles: parser/, resolver/, linter/, formatter/, command/, internal/, testdata/ and docs/, with an action.yml at the root for the GitHub Action wrapper.

On maintenance and licence, the project is Apache-2.0 and the newest release is v0.12.0 from 2026-07-18, with the last push on 2026-07-18 as well. The README also carries a warning that it corresponds to the main branch of development and may contain unreleased features, which is a good reason to pin the tool version in your own CI rather than track main.

## Conclusion

Adopt Ratchet if your workflows already reference mutable tags and you want the pinning done by a tool that remembers what you asked for, and if you can run it in CI through the container image or the action. Do not expect it to manage container or Docker references in the upgrade command, since that limitation is documented, and remember that the README on the main branch may describe features that are not yet released. Before adopting it, check the licence is Apache-2.0, confirm your CI system has a parser, and note that v0.12.0 was released on 2026-07-18 with the last push on the same date.

## FAQ

### What does Ratchet do to a GitHub Actions workflow file?

Ratchet rewrites unpinned references such as actions/checkout@v7 into a commit SHA and records the original constraint in a trailing ratchet comment, for example # ratchet:actions/checkout@v7, so the pin can be refreshed later.

### How do I install Ratchet?

Options include brew install ratchet, a single static binary from the releases page, the container image, nix run 'github:NixOS/nixpkgs/nixpkgs-unstable#ratchet' -- --help, and go install github.com/sethvargo/ratchet@latest. The Homebrew and Nix routes are community supported and may not be the latest version.

### What is the bake delay flag for in Ratchet?

The -bake-delay flag, or the RATCHET_BAKE_DELAY environment variable, makes pin, update and upgrade refuse to resolve any commit, release or image younger than the given duration. It is off by default.

### Which CI systems does Ratchet support and how do I select one?

It supports Circle CI, GitHub Actions, GitLab CI, Google Cloud Build, Harness Drone and Tekton, and you select the parser with the -parser flag, for example ratchet pin -parser circleci circleci.yml.

### How do I stop Ratchet from pinning a particular reference?

Add the ratchet:exclude annotation as a line comment on that reference and Ratchet will leave it unpinned.

## Sources

- [Issues](https://github.com/sethvargo/ratchet/issues)
- [License: Apache-2.0](https://github.com/sethvargo/ratchet/blob/main/LICENSE)
- [README](https://github.com/sethvargo/ratchet/blob/main/README.md)
- [Releases](https://github.com/sethvargo/ratchet/releases)
- [sethvargo/ratchet on GitHub](https://github.com/sethvargo/ratchet)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/sethvargo-ratchet
