What Carries Over in a Runner Snapshot?

A runner snapshot carries the VM filesystem as it stood at capture, including installed packages, warmed caches, and pulled images. Here is what resets.

A runner snapshot carries the machine state captured at the moment the save action ran, which means the filesystem as it stood: installed system packages, dependency directories, warmed compiler and package manager caches, container images already pulled into the local Docker store, and any prepared container or fixture data written to disk. Anything that lived outside that filesystem resets, including /tmp, running processes, and the credentials GitHub issued to the job that captured the image.

Answer

The snapshot runners documentation describes the unit precisely: WarpBuild takes a snapshot of the runner VM at whatever point in the workflow you call WarpBuilds/snapshot-save, and a later job that requests that alias boots from the saved image. The boundary that matters when you plan around it is the difference between disk and machine.

ItemAfter a snapshot bootSource
System packages installed with aptPresentSnapshot runners docs
Dependency trees such as node_modules, vendor, ~/.m2PresentSnapshot runners docs
Container images pulled into the local Docker storePresentSnapshot runners docs
Compiler and package manager caches on diskPresentSnapshot runners docs
Repository working tree as it stood at capturePresent, minus whatever the cleanup step removedSnapshot runners docs
Credentials written to disk, such as $HOME/.aws or $HOME/.sshPresent unless deleted before the saveWarpBuild security docs
Files under /tmpReset. The directory is cleaned on reboots and a snapshot boot is a bootSnapshot runners docs
Processes and services the capturing job startedNot running. Their installed files are on diskSnapshot runners docs
Variables exported through GITHUB_ENV or GITHUB_PATHReset. They apply to later steps of the job that set themGitHub workflow commands
Secrets injected as environment variablesNot in the image unless a step wrote them to a fileGitHub secrets docs
The GITHUB_TOKEN of the capturing jobGone. GitHub expires the token when that job finishesGitHub automatic token authentication
The runner registration itselfFresh. Each runner is its own VM, created on demand and destroyed after the buildWarpBuild security docs
WARPBUILD_SNAPSHOT_KEYSet to the alias the machine booted fromSnapshot runners docs

The last row is the one to build workflow logic on. A restored machine advertises itself, so a step can branch on whether the expensive setup work is already done rather than guessing.

Two limits bound how much can carry over. The first is disk: the Ubuntu shapes that accept snapshot labels run on 150GB SSD storage from 2 through 32 vCPU, per the runner catalog, so the snapshot holds whatever fits in that volume. The second is the runner shape in runs-on. A snapshot boots onto the machine size you request, so keep the size stable between the workflow that saves and the workflows that restore. Snapshot support covers the Ubuntu part of that catalog on both architectures.

Detail

Anything tied to the capturing job resets

The pattern behind half of the reset column is that GitHub scopes credentials and context to a single job. The GITHUB_TOKEN expires when the job that received it finishes, so a token file left on the disk is dead weight rather than a working credential. Repository secrets arrive as environment variables in the job that references them, so they are absent from a restored machine unless a step wrote them somewhere.

Long-lived credentials behave the opposite way. A cloud login step that writes $HOME/.aws/credentials, an SSH key added to $HOME/.ssh, or a container registry token in ~/.docker/config.json is a file on the disk, and files on the disk are exactly what the snapshot preserves. That is why the documentation puts a cleanup step immediately before every save:

rm -rf $HOME/.ssh $HOME/.aws
git clean -ffdx

git clean -ffdx removes untracked files from the working tree, including directories and files ignored by .gitignore. It also deletes build output you may have wanted in the image, so on repositories where the build directory is gitignored, replace it with targeted deletions of the paths that hold credentials.

The security boundary on public and private repositories

The snapshot runners documentation documents two distinct exposure paths, and they need different reasoning.

On public repositories, a snapshot is addressed by an alias that appears in the workflow file. Anyone who can read the repository can read the alias, and anyone who can open a pull request can run a workflow that names it. Cloud credentials, signing keys, and registry tokens have no place in a snapshot on a public repository.

On private repositories, WarpBuild provisions runners at the organization level, and GitHub may allocate a runner intended for a snapshot job to a different job in the same organization. A snapshot carrying secrets can therefore surface them to other users inside the organization.

Neither path changes the isolation model of the runner itself. Each runner runs in its own virtual machine, created on demand and destroyed after each build, on an encrypted storage volume, per the security documentation. The snapshot is the one artifact designed to outlive that machine, so it is the one artifact that needs a cleanup policy.

Reading the boot in the workflow

This is the shape that uses the carried state without assuming it. The default branch boots clean and republishes the image; other runs boot from it and skip only the steps whose work the image already contains.

name: build
on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: >-
      ${{ github.ref == 'refs/heads/main'
        && 'warp-ubuntu-latest-x64-4x;snapshot.enabled=true'
        || 'warp-ubuntu-latest-x64-4x;snapshot.key=web-app-main' }}
    steps:
      - uses: actions/checkout@v5

      - name: Install system packages
        run: |
          if [ -z "$WARPBUILD_SNAPSHOT_KEY" ]; then
            sudo apt-get update
            sudo apt-get install -y libvips-dev protobuf-compiler
          fi

      - name: Start services
        run: docker compose up -d postgres redis

      - name: Install dependencies
        run: npm ci

      - name: Test
        run: npm test

      - name: Cleanup credentials
        if: github.ref == 'refs/heads/main'
        run: |
          rm -rf $HOME/.ssh $HOME/.aws
          git clean -ffdx

      - name: Save snapshot
        if: github.ref == 'refs/heads/main'
        uses: WarpBuilds/snapshot-save@v1
        with:
          alias: "web-app-main"
          fail-on-error: true
          wait-timeout-minutes: 60

The docker compose up step runs unconditionally on purpose. The Postgres and Redis images are on the restored disk, so the pull is local, but the containers themselves are not running after a boot and the job has to start them. npm ci is unguarded for a different reason: it reconciles the lockfile against the restored node_modules tree, which is cheap when the tree is correct and correct when the lockfile moved on since capture.

When a clean machine is the requirement

Some jobs exist to prove that a clean machine can build the code, and a snapshot removes exactly the evidence they are meant to produce.

  • Release and publish jobs. An artifact assembled on a disk carrying leftovers from earlier runs is harder to reason about than one built from the base image.
  • Lockfile and dependency resolution checks. A restored disk already holds the packages, so a job that should fail on an unresolvable or removed dependency can pass on cached bytes.
  • First reproduction of a reported bug. Ruling out machine state is the first step, and a snapshot reintroduces it.
  • Jobs handling long-lived credentials on public repositories, where the alias is readable by every contributor.
  • Short jobs. A snapshot boot takes 45 to 60 seconds per the snapshot runners documentation, and a restore bills $0.04 per job from the pricing page, checked on 2026-08-13, so a job with under a minute of setup work gives back more than it gains.

Both snapshot.enabled=true and no snapshot label at all boot from the base image, so the producing workflow and the clean-machine workflows share the same starting point. Snapshot storage bills $0.025 per snapshot-hour while an alias is alive, also from the pricing page checked on 2026-08-13, and snapshots are deleted after 15 days, which caps a forgotten alias on its own.

Does a runner snapshot include running processes or memory?

No. A job that boots from a snapshot boots a machine, which is why the /tmp directory comes back empty: that directory is cleaned on reboots. Database containers, language servers, and any daemon the capturing job started are on disk as installed software and have to be started again by the restored job. VM snapshot, defined covers the disk-versus-memory distinction across implementations.

Do secrets end up inside a runner snapshot?

Only the ones written to disk. A secret that reaches the job as an environment variable is not part of the filesystem, but a login step that writes $HOME/.aws, $HOME/.ssh, or a registry config puts those files in the image. Remove them in a cleanup step before the save action runs, as the snapshot runners documentation shows.

When should a job avoid a snapshot entirely?

When the job exists to prove that a clean machine can build the code: release and publish jobs, lockfile and dependency resolution checks, and first-run reproduction of a bug. Those jobs should carry snapshot.enabled=true or no snapshot label at all, since both boot from the base image. The snapshot runners page lists the labels and the Ubuntu sizes that accept them.

How is this different from state carrying across jobs by itself?

It is the only way state carries at all. Every job gets a fresh ephemeral VM whose storage is destroyed when the runner terminates, so nothing persists unless a snapshot, a cache, or an artifact carries it. Do GitHub Actions runners keep state between jobs? walks through the three mechanisms and what each one restores.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.