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.


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-failedRoute 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.ymlRoute 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.ndjsonThe 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.
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.953254Which 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 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, CI will not run on the commits Claude pushes. The documentation names this in its troubleshooting section, and the fix is to let the action authenticate as the GitHub App instead.
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 }}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.
Key takeaways
- act tests the workflow file,
gh workflow runtests the real pipeline,latchkey runtests the code on a clean machine. - act does not ship GitHub's runner image, so a green act run says nothing about the hosted environment.
- Watch a dispatched run with
gh run watch --exit-statusso the agent gets an exit code rather than prose. claude-code-actionis the reverse direction: the agent inside Actions, triggered by@claudeor by a prompt input.
Frequently asked questions
Can I run GitHub Actions workflows locally before pushing?
How do I test AI-generated pull requests in CI?
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?
Why does CI not run on the commits Claude pushes?
GITHUB_TOKEN. 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.