Set up Chainguard Checks and Chainguard Workspaces
How Chainguard Checks and Chainguard Workspaces work together, and how to sign up for both with chainctl develop init …
For the complete documentation index, see llms.txt.
Chainguard Checks and Chainguard Workspaces are built to work together: a coding agent changes your code in a Workspaces session, run-ci runs your checks against the change before you push, and Chainguard Checks runs the same checks on the pull request. When a check fails, the agent reads why from the session and fixes it.
In this tutorial, you walk that loop once with a small Go project. You open a session, let Claude Code add a function, check the change with run-ci, and open a pull request whose checks pass. Then you break a test on purpose, push without running run-ci, read the failure from the session with chainctl checks failures, and let Claude Code fix it.
Note: Each product is in beta.
You need the following:
chainctl 0.2.391 or later, and the Chainguard App installed on the GitHub account that owns your repository. Refer to Set up Chainguard Checks and Chainguard Workspaces.acme/hello, and a clone of it on your laptop. If you installed the Chainguard App on selected repositories only, add this one.chainctl offers to set one up when you open the session, as described in Get started with Chainguard Workspaces.You don’t need Go on your laptop. The checks and the session install it.
In the commands on this page, replace $ORGANIZATION with the name of your Chainguard organization. If you belong to only one organization, you can omit --parent.
In your clone, create a Go module with one function and its test. Create a go.mod file:
module github.com/acme/hello
go 1.24Create a hello.go file:
// Package hello builds greetings.
package hello
// Greeting returns a greeting for name.
func Greeting(name string) string {
return "Hello, " + name + "!"
}Create a hello_test.go file:
package hello
import "testing"
func TestGreeting(t *testing.T) {
if got := Greeting("Ada"); got != "Hello, Ada!" {
t.Errorf("Greeting(\"Ada\") = %q, want %q", got, "Hello, Ada!")
}
}Then create the file that defines your checks:
version: 1
environment:
packages: [go, git, claude]
checks:
vet:
cmd: go vet ./...
test:
cmd: go test ./...The file defines two checks, vet and test, which Chainguard Checks runs on every pull request. A Workspaces session uses the same environment, so its packages give the session Go, Git, and Claude Code. The project has no dependencies, so neither check needs network access.
Commit the files and push them to your repository’s default branch:
git add .
git commit -m "Add the hello project"
git push origin mainCreate a branch for your change, then open a session from your clone:
git switch -c add-farewell
chainctl develop --parent $ORGANIZATIONchainctl creates a session, uploads your branch, and opens a shell in the session. Its output includes lines similar to the following:
Creating a session for . (checkout of https://github.com/acme/hello at 1efc76b9f6c4) from its own .chainguard/ci.yaml, as Ada Lovelace <ada@example.com> …
Created acme/hello/quiet-harbor (fa101f50)
claude: served from your Claude login held by Chainguard (ada@example.com), attached or not
Syncing . as a delta over https://github.com/acme/hello@ceab2476db62 …
Attached to acme/hello/quiet-harbor [running] · github, claude, cgr.dev · Ctrl+] detach ~? helpThe session’s environment comes from your .chainguard/ci.yaml file, and the claude: line names the Claude login that Claude Code uses in the session. Your files are in /work, on your add-farewell branch.
In the session, start Claude Code:
claudeAsk it for a small change, such as the following:
Add a Farewell function to hello.go that returns "Goodbye, <name>!", and a test for it in hello_test.go, in the same style as Greeting and its test.Claude Code asks before it runs commands or edits files, depending on its permission mode. Its edits vary from run to run, but the change looks similar to the following:
// Farewell returns a farewell for name.
func Farewell(name string) string {
return "Goodbye, " + name + "!"
}func TestFarewell(t *testing.T) {
if got := Farewell("Ada"); got != "Goodbye, Ada!" {
t.Errorf("Farewell(\"Ada\") = %q, want %q", got, "Goodbye, Ada!")
}
}When Claude Code finishes, exit it with /exit.
Run your checks against the change in the session:
run-ciThe run-ci command runs each check in .chainguard/ci.yaml against the files in /work, including changes you haven’t committed. The output looks similar to the following:
run-ci: checks vet, test (advisory: the gate runs the verdict on push)
── vet[0]: go vet ./...
── vet[0]: exit 0 (180ms)
── test[0]: go test ./...
ok github.com/acme/hello (cached)
── test[0]: exit 0 (177ms)
PASS in 521msCommit the change and push your branch:
git add hello.go hello_test.go
git commit -m "Add Farewell"
git push origin add-farewellThe Chainguard App adds the credential for the push, so you don’t sign in to GitHub in the session. The push prints a link for creating a pull request on GitHub. Open the link in your browser, and create the pull request.
Chainguard Checks runs vet and test on the pull request, each in a fresh VM. When they finish, the Chainguard Checks (presubmit) check on the pull request reads All 2 check(s) passed. For what else the pull request shows, refer to How results appear.
To see what happens when a check fails, change Greeting so that its test fails, and push the change without running run-ci. In the session, change the ! in Greeting to a period:
sed -i 's/"Hello, " + name + "!"/"Hello, " + name + "."/' hello.go
git commit -am "Change the greeting punctuation"
git push origin add-farewellThe sed command matches only Greeting’s string, not Farewell’s. The push updates the pull request, and Chainguard Checks runs the checks again. This time, Chainguard Checks (presubmit) reads 1 of 2 check(s) failed, and names ci / test as the check that failed.
In the session, read why the check failed:
chainctl checks failuresThe command finds the pull request’s latest run for the commit you’re on, and prints each failed check with the end of its output:
Run: instances/<organization-id>/runs/<run-id>
Status: failed; 1 failed, 1 passed, 0 did not run
=== check "test" in workflow "ci": failed (exit 1), attempt 1 ===
step 0 "step 0": failed (exit 1)
command: go test ./...
working directory: /work
process go: exit 1 (guest-attested; the last 148 B of its output, log bytes 390-538)
> --- FAIL: TestGreeting (0.00s)
> hello_test.go:7: Greeting("Ada") = "Hello, Ada.", want "Hello, Ada!"
> FAIL
> FAIL github.com/acme/hello 0.001s
> FAILThe command works while you’re attached to the session, because the session gets its Chainguard credential from your attached terminal. For its options, refer to Inspect pull request runs.
Claude Code can read the same output. Start it again with claude, and ask it to fix the failure:
The checks on my pull request failed. Run chainctl checks failures to see why, then fix the code so the checks pass. Do not change the tests.Claude Code runs chainctl checks failures, finds the test that failed, and changes the period in Greeting back to !. It may run run-ci to confirm the fix, and point out that the fix undoes your last commit.
Run the checks in the session, then commit and push the fix:
run-ci
git commit -am "Restore the greeting punctuation"
git push origin add-farewellChainguard Checks runs the checks again, and Chainguard Checks (presubmit) reads All 2 check(s) passed.
To leave the session, press Ctrl+] to open the session menu, then press Enter or d to detach. On your laptop, find the session’s name, and delete the session:
chainctl develop list --parent $ORGANIZATION
chainctl develop delete acme/hello/quiet-harbor --parent $ORGANIZATIONThe delete command asks you to confirm, then destroys the session’s VM and its snapshot. Anything you didn’t push is gone. Then close the pull request on GitHub, or merge it, and delete the repository if you don’t need it.
run-ci covers, and how it differs from a pull request run.Last updated: 2026-11-12 16:11