Install
What a repository needs depends on its mode:
- Host mode needs no Docker. On a Mac it needs only shipcheck. On Linux it needs shipcheck and bubblewrap.
- act mode, and any
[jobs]entry withrunner = "act", needs act 0.2.86 or later and a running Docker engine: Docker Desktop 4.44.3 or later, OrbStack, or colima as the Docker engine. On Linux, Docker Engine with runc 1.2.8 or 1.3.3 or later. shipcheck refuses older versions. - A job with
engine = "colima"needs colima and lima on the Mac, besides act and the running Docker engine above, which shipcheck checks before every act run and which runs the jobs a colima job needs. The job's own steps use the Docker inside its VM.
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.
-
Install shipcheck with Homebrew. It also installs act, git and the Docker client:
brew install gabrielkoerich/tap/shipcheck -
For act mode, install and start a Docker engine. Host mode needs none:
Engine Install What ran on it Docker Desktop 4.44.3 or later docker.com, then open it once The act mode trials and benchmarks on these pages. Recommended OrbStack orbstack.dev act jobs. It sometimes showed a job the files of the previous run at the same path, under /var/folders. shipcheck fails such a jobcolima brew install colima && colima startact jobs, with the repository and SHIPCHECK_HOMEunder your home directory, the only one colima shares by default. Elsewhere a job sees an empty checkout and failsOrbStack 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.
-
Check it:
shipcheck doctorIt checks git, the OS sandbox or act and the Docker engine, the disk and the repository, marks each line
okorfix, and prints the command that fixes eachfix. 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.
-
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.
-
Let your user run Docker without sudo, then log out and in:
sudo usermod -aG docker "$USER"Membership of the
dockergroup is as strong as root on that machine. shipcheck uses the Docker socket to start jobs, and never gives the socket to a job. -
Install shipcheck. The install script needs no other tool:
curl -fsSL https://shipcheck.run/install.sh | shHomebrew 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. -
Check it:
shipcheck doctorIt 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:
| Permission | For |
|---|---|
| Checks: write | Posting the job checks and the shipcheck summary |
| Pull requests: write | Listing 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: read | Basic repository data, which every App has |
It has no Contents permission, so it cannot read or change your code.
Install the App
- Open github.com/apps/shipcheck-run/installations/new.
shipcheck initprints this link and opens it when you apply. - Pick your user or an organization. On an organization, an owner installs it, or approves the request of a member who asks.
- 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.
- A machine token stops working after 90 days without a post. Run
shipcheck loginagain then. shipcheck logoutrevokes this machine's token and deletes it.- The service receives the check's text and the report comment, never your code. The text holds the end of each job's log (the checks on the PR).
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:
postsays which token posts: theshipcheck logintoken or theSHIPCHECK_TOKENmachine token. On a machine with neither, the line staysokand saysnot signed in. Optional: `shipcheck login`, or SHIPCHECK_TOKEN from `shipcheck token`, lets `shipcheck post` put the check on PRs.app, on a machine that can post, asks GitHub for the repository's open PRs. It saysinstalled on <owner>/<repo>, runs can post its checks, or, when the App is not installed there or you have no access, marks itfixwithadd <owner>/<repo> to the shipcheck App: https://github.com/settings/installations (shipcheck-run, Configure).
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:
shipcheck runruns the checks locally and gives the verdict as an exit code.- The result cache answers at once for a tree that already passed.
- The git hooks start runs in the background on commit or push.
shipcheck statusandshipcheck logsshow the stored result and logs of a commit.
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.