# local-ci: GitHub Actions on Your Machine, With the Failure Kept Alive

> Local CI is an FSL-1.1-MIT licensed TypeScript tool that replaces the GitHub Actions cloud API with a local one and feeds jobs to the unmodified official runner, so a failed step can be fixed and retried in place instead of pushing again. It suits teams whose CI is the slowest part of their loop.

**redwoodjs/local-ci** — Run GitHub Actions locally — pause on failure, retry in place, and keep caches on your machine.

- Repository: https://github.com/redwoodjs/local-ci
- Website: https://local-ci.dev
- Stars: 810 · Forks: 20
- Language: TypeScript
- License: NOASSERTION
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/redwoodjs-local-ci

## It replaces the API, not the runner

The architectural claim is specific and unusual: Local CI is a ground-up rewrite of the GitHub Actions orchestration layer that runs entirely on your own machine, and it does not wrap or shim the runner. It replaces the cloud API that the official GitHub Actions Runner talks to, so the same runner binary that executes your jobs on GitHub.com executes them locally. The emulated surface is named: the Twirp endpoints, the Azure Block Blob artifact protocol and the cache REST API. The practical consequence is that actions like actions/checkout, actions/setup-node and actions/cache work out of the box, with no patches, no forks and no network calls to GitHub. If your workflow runs on GitHub, the claim is that it runs here too.

## The failure stays alive so you can fix it in place

The argument the project makes is about the retry loop. Remote CI runs on every push and decides what ships, and the problem is what happens when it fails: you push, you wait, you read logs, you push again, paying the full cost of a fresh run every time, with the gatekeeper used as a debugger. Local CI is framed as a pre-flight check instead. When a step fails, the tool pauses rather than tearing down, leaving the container alive with all state intact, including environment variables, installed tools and intermediate build artifacts. Your edits on the host are synced into the container, so you or an AI agent can fix the issue and retry just that failed step. The comparison table puts it plainly: on failure, GitHub Actions and other local runners start over, while Local CI pauses, fixes and retries the failed step, and the container state on failure is kept alive rather than destroyed.

## Caching that costs a filesystem round trip instead of a network one

The caching story is the other headline. Local CI replaces GitHub's cloud cache with local filesystem caches, so package-manager stores, Playwright browsers and the runner tool cache live on the host with no upload or download. The table quotes a cache round trip of roughly 0 ms on the local filesystem against a network round trip measured in seconds for GitHub.com. The claim that dependencies which took a couple of minutes to install on hosted runners install in a few seconds on the second run follows from that. The implementation detail that matters is how each package manager is treated. Every job gets private writable node_modules, and pnpm, Yarn and Bun jobs can start from an immutable lockfile-keyed snapshot cloned with copy-on-write where the host supports it. npm jobs are different because npm ci removes node_modules by design, so they share npm's download cache instead.

## Prewarming one job so the rest start warm

For workflows with several independent jobs there is a prewarm mechanism, invoked with the `--prewarm-through <workflow:job:step-id>` option, which populates the dependency cache once before the real jobs start in parallel. The selected step must have a stable id, which is a real constraint on how you write the workflow. The mechanism runs a disposable copy of that job from the beginning through the selected step, atomically publishes a completed snapshot, and clones it into each real job's private workspace. There is also a `LOCAL_CI_PREWARM_THROUGH` setting in `.env.local-ci` to make the behaviour automatic for a repository. The documentation is careful about the default: without explicit prewarming, jobs remain isolated and safe, and the first successful job can populate the cache for later runs.

## Running it for the first time

The quick start is two npx invocations, one for a single workflow and one for everything relevant to the current branch.

```bash
npx run-local-ci run --workflow .github/workflows/ci.yml
```

```bash
npx run-local-ci run --all
```

Local CI runs against your current working tree, so uncommitted changes are included automatically and there is no need to commit or stash before running. The prerequisite is Docker with a running provider: on macOS the recommendation is OrbStack or Docker Desktop, and on Linux either the native Docker Engine or Docker Desktop. For runs-on macos- jobs on Apple Silicon Macs there are two optional tools, tart installed with `brew install cirruslabs/cli/tart` and sshpass installed with `brew install hudochenkov/sshpass/sshpass`. Without both, the macOS jobs are skipped with a reason rather than failing.

## Agent CI renamed, and the migration path

The project was previously called Agent CI and is now Local CI, and the readme spells out the rename so nobody is left with a broken script. The canonical package is invoked with `npx run-local-ci`, while the executable installed into node_modules/.bin is `local-ci`. Existing @redwoodjs/agent-ci installations continue to work throughout the remaining 0.x releases, with the compatibility package forwarding the agent-ci executable to Local CI. Legacy configuration is recognised too: AGENT_CI_ prefixed environment variables, .env.agent-ci, .github/agent-ci.Dockerfile and the legacy Agent CI Docker resources. New configuration should use the LOCAL_CI_ prefix, .env.local-ci and .github/local-ci.Dockerfile. The documented commit pattern is worth copying: commit, run, fail, fix with --pause-on-failure, retry, then commit the fix, so the commit becomes a save point you can roll back to if a fix makes things worse.

## A Rust runner in the repository, not in the npm package

The repository is a hybrid. The npm package keeps npx run-local-ci on the TypeScript execution path, while a Rust runner lives in the repository for parity testing, and published npm installs do not include a native runner yet. Native npm platform packages and release archives are deferred until the release workflow builds, stages and verifies real target binaries. From a checkout you can try the Rust runner with Cargo.

```bash
cargo run -p local-ci -- run --workflow .github/workflows/ci.yml
```

```bash
cargo build --release -p local-ci
./target/release/local-ci run 
```

The Cargo workspace has three crates, local-ci, local-ci-core and local-ci-runtime, on edition 2024 with a minimum Rust version of 1.85, and the workspace lints forbid unsafe code outright while treating all clippy lints as warnings. The licence identifier in the workspace is FSL-1.1-MIT, which is a functional source licence, while the top-level licence entry in the repository is recorded as a custom one that GitHub cannot classify, so the licence file itself is the reference.

## What the monorepo holds and how it is checked

The top level shows a large workspace: apps/, crates/, packages/, experiments/, packaging/, scripts/, skills/, third-party-workflows/ and a .devcontainer/ directory, alongside a pnpm-workspace.yaml and a Cargo.toml. The package scripts show how the project checks itself, and the list is a good indicator of the engineering standards. Typechecking runs across the workspace with pnpm -r, linting is oxlint with warnings denied, formatting is oxfmt, and the Rust side has cargo check, cargo clippy with warnings denied, cargo fmt and cargo test. Two scripts are worth noting because they describe the risk of this kind of project. There is a compatibility generator, scripts/gen-compatibility-md.mjs, with a check mode, and there are golden CLI and smoke parity scripts that compare the Rust runner against the TypeScript one. A CI emulator that behaves differently from the real thing is worse than no emulator, so parity testing is the load-bearing part of the setup.

## Conclusion

local-ci fits a team whose GitHub Actions runs are the bottleneck in their edit loop, especially where caching costs a couple of minutes per run and the same dependency install happens on every push. It is the wrong choice if you have no Docker available, or if you rely on hosted-only runners such as macOS jobs without the optional tart and sshpass tooling. Before adopting it, run one existing workflow with --all and see how close the local result is to the hosted one, since the tool emulates the API surface rather than the runner.

## FAQ

### Does local-ci use the real GitHub Actions runner?

Yes. It emulates the server-side API surface, namely the Twirp endpoints, the Azure Block Blob artifact protocol and the cache REST API, and feeds jobs to the unmodified official runner binary, so the same runner that executes jobs on GitHub.com executes them locally.

### How does local-ci handle a failed step?

It pauses instead of tearing down, keeping the container alive with environment variables, installed tools and build artifacts intact. Your host edits are synced into the container so you can fix the issue and retry just the failed step.

### What do I need before running local-ci?

A running Docker provider, OrbStack or Docker Desktop on macOS and the native Docker Engine or Docker Desktop on Linux. For macos- jobs on Apple Silicon you also need tart and sshpass, and without both the macOS jobs are skipped with a reason.

### Can local-ci run against uncommitted changes?

Yes. It runs against your current working tree and includes uncommitted changes automatically, so there is no need to commit or stash before running. The documented pattern is to commit anyway, as a save point you can roll back to.

### What happened to Agent CI?

Agent CI is now Local CI. The canonical package is invoked with npx run-local-ci and the installed executable is local-ci. Existing @redwoodjs/agent-ci installs keep working through the remaining 0.x releases, and legacy AGENT_CI_ environment variables and .env.agent-ci are still recognised, while new setups should use LOCAL_CI_ and .env.local-ci.

## Sources

- [Issues](https://github.com/redwoodjs/local-ci/issues)
- [Project website](https://local-ci.dev)
- [README](https://github.com/redwoodjs/local-ci/blob/main/README.md)
- [redwoodjs/local-ci on GitHub](https://github.com/redwoodjs/local-ci)
- [Releases](https://github.com/redwoodjs/local-ci/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/redwoodjs-local-ci
