# How to run CI from Claude Code

> Run CI from Claude Code three ways: trigger GitHub Actions with gh, run the steps locally with act, or run the command on a fresh runner.

Source: https://latchkey.dev/learn/agent-ci/run-github-actions-from-claude-code  
Updated: 2026-09-20

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.

```Terminal
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
```

> 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.

```Terminal
act -l
act -j test
act pull_request -W .github/workflows/ci.yml
```

## 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](/learn/agent-ci/run-tests-on-a-fresh-runner-from-your-terminal).

```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
```

## 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, 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.

```.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 }}
```

## 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](/learn/ci-explained/self-healing-ci-explained) draws that line, and the failure pages behind it, such as [exit code 137 in GitHub Actions](/learn/failures/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`. 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

- [Claude Code GitHub Actions, code.claude.com (read 2026-09-20)](https://code.claude.com/docs/en/github-actions)
- [anthropics/claude-code-action](https://github.com/anthropics/claude-code-action)
- [act: runners and default images](https://nektosact.com/usage/runners.html)
- [GitHub CLI manual: gh run watch](https://cli.github.com/manual/gh_run_watch)
- [Latchkey CLI documentation](https://latchkey.dev/documentation/latchkey-cli)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
