Install

What a repository needs depends on its mode:

Every repository also needs the shipcheck App to post its checks on PRs.

Set up a Mac

Tested on macOS with Docker Desktop on Apple silicon. Host mode uses macOS's own sandbox-exec.

  1. Install shipcheck with Homebrew. It also installs act, git and the Docker client:

    brew install gabrielkoerich/tap/shipcheck
  2. For act mode, install and start a Docker engine. Host mode needs none:

    EngineInstallWhat ran on it
    Docker Desktop 4.44.3 or laterdocker.com, then open it onceThe act mode trials and benchmarks on these pages. Recommended
    OrbStackorbstack.devact jobs. It sometimes showed a job the files of the previous run at the same path, under /var/folders. shipcheck fails such a job
    colimabrew install colima && colima startact jobs, with the repository and SHIPCHECK_HOME under your home directory, the only one colima shares by default. Elsewhere a job sees an empty checkout and fails

    OrbStack and colima also switch Docker's default context to their own when they start. Here colima is only the Docker engine behind act's containers. engine = "colima", a VM of its own for one job, is a different thing (engines).

    shipcheck refuses Docker Desktop before 4.44.3, which let any container reach the Docker engine.

  3. Check it:

    shipcheck doctor

    It checks git, the OS sandbox or act and the Docker engine, the disk and the repository, marks each line ok or fix, and prints the command that fixes each fix. It exits 0 when a run can work.

macOS disks use APFS, so each job's checkout is a copy-on-write clone and costs no disk. On Apple silicon, jobs run arm64 images. A job whose tools ship no arm64 build can get an amd64 image of its own with act.jobs in shipcheck.toml. Turn on Rosetta in Docker Desktop's settings so amd64 images run faster.

shipcheck keeps its results, logs and checkouts in ~/.local/state/shipcheck. An older shipcheck kept them in ~/Library/Application Support/shipcheck: the first command of a newer one moves that folder and says so. Then run shipcheck install once, so the daemon logs to the new folder. shipcheck doctor says when it still logs to the old one.

Set up Linux

Tested on Debian 13 with Docker Engine 29 on x86_64.

For host mode, install bubblewrap, which runs each check in a sandbox: sudo apt install bubblewrap or sudo dnf install bubblewrap.

Ubuntu 23.10 and later let only programs with an AppArmor profile create user namespaces, and bubblewrap needs them. Allow bwrap alone with the profile apparmor ships, and leave the restriction on for everything else:

sudo apt install apparmor-profiles
sudo cp /usr/share/apparmor/extra-profiles/bwrap-userns-restrict /etc/apparmor.d/
sudo apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrict

shipcheck doctor runs bwrap once. When it fails and /proc/sys/kernel/apparmor_restrict_unprivileged_userns is 1, it prints these commands, or only the last one when the profile is in /etc/apparmor.d but not loaded. Without a working bubblewrap, host mode refuses to run.

  1. For act mode, install Docker Engine from Docker's own package repository, which brings a current runc. shipcheck refuses runc before 1.2.8 or 1.3.3, which have known container escapes.

  2. Let your user run Docker without sudo, then log out and in:

    sudo usermod -aG docker "$USER"

    Membership of the docker group is as strong as root on that machine. shipcheck uses the Docker socket to start jobs, and never gives the socket to a job.

  3. Install shipcheck. The install script needs no other tool:

    curl -fsSL https://shipcheck.run/install.sh | sh

    Homebrew on Linux works too, and also installs act, git and the Docker client: brew install gabrielkoerich/tap/shipcheck. Without Homebrew, install git from your distribution and act from its releases.

  4. Check it:

    shipcheck doctor

    It prints the command that fixes each line it marks fix, as on a Mac.

On ext4 there is no copy-on-write, so each job's checkout is a plain git worktree: a full copy of the files and a slower start. btrfs, or XFS with reflink, gives copy-on-write clones as on a Mac. Jobs run as runner, uid 1001, so shipcheck gives the checkout to that uid before each job and back to your user after it.

Other ways to install

With cargo, from crates.io. This builds it with Rust 1.89 or later:

cargo install shipcheck

Or with cargo-binstall, which downloads the binary of the latest release, signed and notarized on macOS, with no Rust needed. mise uses it too:

cargo binstall shipcheck
mise use -g cargo:shipcheck

mise waits a while before it offers a new release, so right after one it can install the one before.

With the install script, on macOS or Linux, arm64 or x86_64:

curl -fsSL https://shipcheck.run/install.sh | sh

It downloads the binary from the latest GitHub release, checks it against the release's SHA256SUMS, and installs it to ~/.local/bin. Set SHIPCHECK_INSTALL_DIR to install it somewhere else.

Or build it from source, from the repository or from a clone, with Rust 1.89 or later:

cargo install --git https://github.com/gabrielkoerich/shipcheck shipcheck
git clone https://github.com/gabrielkoerich/shipcheck && cargo install --path shipcheck

A build with cargo, from crates.io or from source, gets a new signature each time, so the keychain asks again after it (where shipcheck keeps its keys).

Post the checks

The shipcheck App puts the checks on your PRs. Only a GitHub App can create checks, and a ruleset can require each check to come from that App, so an agent's own token cannot post a passing one (require the checks). Your machine runs the checks, and shipcheck's service posts the result with the App. The App's key stays in the service, never on your machine.

The App has three permissions:

PermissionFor
Checks: writePosting the job checks and the shipcheck summary
Pull requests: writeListing a repository's open PRs for the daemon, finding a commit's PRs, learning who pushed a PR's head, and the report comment
Metadata: readBasic repository data, which every App has

It has no Contents permission, so it cannot read or change your code.

Install the App

  1. Open github.com/apps/shipcheck-run/installations/new. shipcheck init prints this link and opens it when you apply.
  2. Pick your user or an organization. On an organization, an owner installs it, or approves the request of a member who asks.
  3. Choose all repositories, or only the ones that use shipcheck. Public and private repositories work the same.

To add a repository later, open github.com/settings/installations, then shipcheck-run, Configure. For an organization, open its settings, GitHub Apps, shipcheck-run, Configure.

On a private repository, the checks and their log tails are visible to everyone who can read the repository. On a public one, everyone sees them (logs).

Sign in each machine

Each machine that posts needs a machine token. You need write, maintain or admin access to the repository to post its checks.

shipcheck login

It prints a one-time code and the page github.com/login/device. In a terminal, Enter opens that page in the browser. Enter the code there and approve. shipcheck then says signed in as <login> and keeps the token, in the login keychain on macOS (where shipcheck keeps its keys). shipcheck's service revokes the GitHub token from the sign-in once it knows who you are, and keeps only a hash of the machine token.

shipcheck init and shipcheck install sign the machine in the same way when it cannot post yet and they run in a terminal. Without a terminal they print one line that says to run shipcheck login or set SHIPCHECK_TOKEN, and go on.

Unattended machines

A VM, a CI runner or an agent box has no person to enter the code. Make a token for it on a machine where you can open a browser:

shipcheck token --name agent-vm

It prints a code to enter at github.com/login/device on stderr, as login does, then only the token on stdout. It does not touch this machine's own sign-in, and it keeps no copy. Put the token in the other machine's environment as SHIPCHECK_TOKEN, or in the agent's secret store. It lasts 90 days after its last post. shipcheck logout on that machine revokes it.

If a machine with a token is compromised, run shipcheck logout --all on any signed-in machine. It revokes every token of your GitHub user at once, this machine's too, so sign the others in again after. Each GitHub user can hold 50 live tokens, and logout --all also clears that limit.

The daemon

The daemon posts with the same machine token. It runs a PR only when the GitHub login you signed in as, or a login daemon_authors lists, opened the PR and pushed its head (when a PR changes on GitHub). The service init and install start gets only PATH from your environment, so it uses the shipcheck login token, never SHIPCHECK_TOKEN. Until the machine is signed in, the daemon posts nothing and its log says to run shipcheck login.

Check it

shipcheck doctor shows two lines about posting:

A post that fails leaves the same reason and fix in shipcheck status, shipcheck run, the push's output and the daemon's log.

What works without the GitHub App

These work on any repository with shipcheck.toml on main, with no App:

Where shipcheck keeps its keys

shipcheck keeps two secrets on your machine: the machine token from shipcheck login, and the secrets act jobs get, one file per repository at secrets/<owner>/<repo>.env in SHIPCHECK_HOME, in act's NAME=value format. shipcheck token prints a token for another machine and keeps no copy. The App's private key lives only in shipcheck's service.

On macOS, shipcheck login writes its token straight into an item of your login keychain. A secrets file you write moves into an item the first time shipcheck reads it, and the file goes to the Trash. Only shipcheck reads an item without asking, so an agent running as you gets a keychain prompt instead of the secret. To change the secrets of a repository, write the file again. An item an older shipcheck kept under its old home, ~/Library/Application Support/shipcheck, gets its new name the first time shipcheck reads it.

The keychain knows shipcheck by its code signature. Release builds, from Homebrew or the install script, are signed with one Developer ID and notarized by Apple, so an upgrade keeps access. A build from cargo install or from source gets a new signature each time: macOS then asks once per item whether the new shipcheck may read it. Run shipcheck doctor at the Mac and choose Always Allow. Choose it only for shipcheck, never for security or another program.

On Linux they stay files of mode 0600, which any program you run can read. The Linux keyring has no per-program access, so it would not keep an agent out.

Next: Quickstart