DEV Community

Cover image for How to run CI from Claude Code
Kay
Kay

Posted on Originally published at latchkey.dev

How to run CI from Claude Code

To run CI from Claude Code you have three honest routes: trigger the GitHub Actions workflow with gh and watch it, replay the workflow steps locally with act, or run the test command itself on a fresh runner and stream the log back into the session. Which one you want depends on whether you are testing the workflow, the code, or the machine the code runs on.

Three routes from an agent session to a CI answer: gh dispatch, act locally, a fresh runner
The three routes answer different questions. Pick by what you are unsure about: the workflow file, the code, or the machine.

Recorded runner log: node v20.20.2 on Linux, TAP output reporting 2 tests passed, 0 failed
The recorded run from content/repro/run-github-actions-from-claude-code.sh on a Latchkey latchkey-small runner, 2026-09-20.

The agent has just finished an edit and the only question that matters is whether CI passes. Pushing a branch to find out is a slow answer: you spend a commit, a workflow run and several minutes of wall clock before the first line of output comes back, and if the answer is no you do it again.

Three routes shorten that loop, and they answer different questions. This page gives each one a command you can paste, says what it proves and what it does not, and ends with the reverse direction: Claude Code running inside GitHub Actions rather than driving it from outside.

Route 1: dispatch the workflow and watch it from the shell

If the thing you are unsure about is the workflow itself, run the real workflow. The GitHub CLI is already in most agent sessions, and it turns a workflow run into a foreground command with an exit code, which is the shape an agent can branch on.

gh workflow run needs the workflow to declare on: workflow_dispatch; without that trigger GitHub refuses the dispatch. The run id is not returned reliably by the dispatch call, so the second line looks it up, and gh run watch --exit-status then blocks until the run finishes and exits nonzero if it failed. Ask for the failed log only, not the whole run: a full Actions log is tens of thousands of lines and most of it is setup noise the agent will read as context it has to reason about.

The lookup is the fragile part: a dispatch takes a moment to appear in the run list, so a query issued immediately can return the previous run. Filter by branch, and check that the run you watched is the one you started.

gh workflow run ci.yml --ref "$(git branch --show-current)"
sleep 3
run_id=$(gh run list --workflow ci.yml --branch "$(git branch --show-current)" \
  --limit 1 --json databaseId --jq '.[0].databaseId')
gh run watch "$run_id" --exit-status --compact
gh run view "$run_id" --log-failed
Enter fullscreen mode Exit fullscreen mode

Every flag here is from gh --help output run on 2026-09-20 with gh version 2.89.0: --ref on gh workflow run, --workflow, --branch, --limit, --json and --jq on gh run list, --exit-status and --compact on gh run watch, and --log-failed on gh run view.

Route 2: replay the steps locally with act

act reads your workflow files and runs each step in a Docker container on your own machine. It is the fastest way to find out whether the YAML is shaped correctly: whether the job matrix expands the way you meant, whether an expression evaluates, whether step ordering and if conditions do what you expect. It needs Docker running, because that is how it executes every step.

What it does not give you is the GitHub-hosted runner. act's own documentation is blunt about it: "These default images do not contain all the tools that GitHub Actions offers by default in their runners", and "Many things can work improperly or not at all while running those image". A job that passes under act can still fail on ubuntu-latest because the preinstalled toolchain, the disk and the memory ceiling are all different, and a job that fails under act is sometimes only missing a tool the real image has.

Treat a green act run as evidence about the workflow file, not about your code or the environment.

act -l
act -j test
act pull_request -W .github/workflows/ci.yml
Enter fullscreen mode Exit fullscreen mode

Route 3: run the command itself on a fresh runner

Most of the time the agent does not want to know whether the workflow file parses. It wants to know whether the tests pass on a clean Linux machine with a fresh dependency install, which is the thing that differs from the laptop and the thing that breaks. Running the command directly skips the workflow layer entirely.

latchkey run packs the working tree, ships it to a fresh runner, executes one bash line, streams the log back to your terminal and exits with the job's outcome. That status is the verdict, so keep it: a pipe replaces it with the parser's status, and the complete event's own exit_code field reads a cancelled or expired job as an ordinary test result. There is no branch to push and no workflow run to spend.

The full flag list, what the runner does with the upload and what it costs are on run tests on a fresh runner from your terminal.

latchkey run 'npm ci && npm test'
verdict=$?   # the CLI's own exit status is the verdict
latchkey run --size large --timeout 3600 'npm ci && npm run build && npm test'
# Reading the JSON stream without throwing that status away:
latchkey run --output json 'npm test' >events.ndjson
verdict=$?
jq -r 'select(.event == "complete") | "\(.state) \(.exit_code)"' <events.ndjson
Enter fullscreen mode Exit fullscreen mode

The run this page was written from

Route 3 is the one with a recording behind it. content/repro/run-github-actions-from-claude-code.sh writes a two-test fixture and runs node --test on a Latchkey latchkey-small runner. The harness invoked the CLI with --no-context, so the runner started with an empty workspace and the script created its own fixture; a reader running the same loop from a checkout gets their tree packed and uploaded instead.

What came back, verbatim, is below. The exit code was 0, and that single number is what an agent branches on: the log is for the human or for the next turn if the number is not zero.

```latchkey run, recorded 2026-09-20 on latchkey-small
runner: Linux 6.17.0-1019-aws, node v20.20.2
workspace: /home/runner/fixture
TAP version 13

Subtest: adds two numbers

ok 1 - adds two numbers


duration_ms: 1.247866
...

Subtest: is commutative

ok 2 - is commutative


duration_ms: 0.141422
...
1..2

tests 2

suites 0

pass 2

fail 0

cancelled 0

skipped 0

todo 0

duration_ms 65.953254




## Which route answers which question

Pick by what you are actually unsure about. Running all three on every edit is how a fast loop becomes a slow one.

| What you are unsure about | Route | What it costs |
| --- | --- | --- |
| The workflow file: triggers, matrix, expressions, step order | `act` | Docker on your machine, no Actions minutes, no runner parity |
| The real pipeline end to end, including secrets and permissions | `gh workflow run` plus `gh run watch` | One workflow run and its Actions minutes |
| The code and the machine: does the suite pass on clean Linux | `latchkey run` | Runner minutes at $0.0025 per minute on `latchkey-small`, no commit |

> The `latchkey-small` rate is from latchkey.dev/pricing, read on 2026-09-20; usage is billed per minute, rounded up per job.

## The other direction: Claude Code inside GitHub Actions

Everything above runs the agent on your machine and CI somewhere else. The `claude-code-action` integration inverts that: Claude Code runs inside a GitHub Actions job, on a runner, triggered by a GitHub event. Anthropic's documentation, read on 2026-09-20, describes two modes. Without a `prompt` input the action waits for the trigger phrase, `@claude` by default, in an issue or pull request comment. With a `prompt` input it runs automatically on any event, including a `schedule`.

The workflow below is the minimal interactive version from those docs. The parts that are not boilerplate are the permissions: `id-token: write` for the action's default GitHub App authentication, `actions: read` so Claude can read CI results on the pull request, and the `if` condition so a runner does not start on every comment.

One consequence is worth knowing before you wire it up. GitHub does not trigger workflows on commits made with the default `GITHUB_TOKEN`, so if you pass that token to the action, `push` workflows will not run on Claude's commits, and `pull_request` runs wait for approval. The documentation's troubleshooting section names this; the fix is to let the action authenticate as the GitHub App.



```.github/workflows/claude.yml
name: Claude Code
on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
jobs:
  claude:
    if: contains(github.event.comment.body, '@claude')
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
      issues: write
      id-token: write
      actions: read
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 1
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
Enter fullscreen mode Exit fullscreen mode

What to hand back when the job fails

An agent reading CI output is spending context on it, so hand it the smallest thing that answers the question. The verdict first, the failing step's log second, the rest never. gh run view --log-failed does that for a GitHub run; latchkey run --output json emits NDJSON whose complete event carries the job state beside the recorded exit code, which is context rather than the verdict.

The failure worth studying is the one that is not about your code at all. A registry timeout, a full disk or an out-of-memory kill is a fact about the machine, and feeding that log to an agent invites it to edit code that was never wrong. Self-healing CI explained draws that line, and the failure pages behind it, such as exit code 137 in GitHub Actions, show what each one looks like in a log.

FAQ

Can I run GitHub Actions workflows locally before pushing?

Partly. act runs each step of your workflow in a Docker container, which checks the workflow file end to end, but its images are not GitHub's runner images and its own documentation says they do not contain all the tools GitHub ships. To check the code rather than the YAML, run the test command on a fresh Linux runner instead.

How do I test AI-generated pull requests in CI?

The same way as any other pull request, with one addition: run the suite before the branch exists. An agent that runs latchkey run on its working tree finds the failure in the loop where it can still fix it silently, so the pull request it eventually opens is one that has already passed on a clean machine.

How can I debug flaky tests and intermittent CI failures?

Run the same command repeatedly on fresh machines and compare. A flake that reproduces one time in five on clean runners is a real race in your code or your fixtures; one that never reproduces there, but fails in CI, is usually about the environment, such as memory, disk or a registry.

Why does CI not run on the commits Claude pushes?

Because GitHub does not trigger workflows on commits made with the default GITHUB_TOKEN (pull_request runs wait for approval). Anthropic's GitHub Actions documentation names this in its troubleshooting section: remove github_token: ${{ secrets.GITHUB_TOKEN }} from the action step so it authenticates as the Claude GitHub App, or pass a custom app token.

References

Top comments (0)