← All writing

Personal · developer tools · GitHub Actions

I shipped ciwalk: debug GitHub Actions locally with pause, shell, and retry

Why I built a local GHA runner that pauses on failure, how AI (Cursor) accelerated the build, and what we intentionally left out of v0.1.0.

TL;DR: ciwalk runs GitHub Actions workflows on your machine in Docker. When a step fails, it keeps the container alive, drops you into an interactive shell in that same step environment, and lets you retry / continue / abort. Install with pip install ciwalk or uv tool install ciwalk. v0.1.0 is on PyPI and GitHub Releases.

This post covers the problem, the product, how we built it with AI without shipping slop, and what v0.1.0 does not do yet.

The loop I was tired of

Most CI debugging still looks like this:

  1. Edit workflow YAML or application code
  2. Commit and push
  3. Wait for a runner
  4. Read truncated logs
  5. Guess what the filesystem / env / working directory looked like
  6. Repeat

Tools like act are excellent at running Actions locally. They still tend to be all-or-nothing: the job fails, the container is gone, and you’re staring at logs again.

I wanted something closer to a debugger:

Fail → stay inside the world that failed → inspect → fix → retry this step.

That became ciwalk.

What ciwalk does

At a high level:

parse workflow YAML
        ↓
start Docker container (default: catthehacker/ubuntu:act-latest)
        ↓
bind-mount your workspace as /github/workspace
        ↓
run steps in order (stream stdout/stderr live)
        ↓
on fail (or --breakpoint): attach interactive shell
        ↓
exit shell → choose retry | continue | abort

Install

Requires Python 3.11+ and a running Docker daemon.

uv tool install ciwalk
# or: pipx install ciwalk
# or: pip install ciwalk

From GitHub:

uv tool install git+https://github.com/kiwi-07/ciwalk.git

Basic usage

ciwalk run path/to/workflow.yml --pause-on-fail
ciwalk run path/to/workflow.yml --breakpoint "Require marker file"
ciwalk run path/to/workflow.yml --job build --input NAME=value
ciwalk cleanup          # remove orphaned ciwalk-* containers
ciwalk cleanup --dry-run

Useful flags:

FlagMeaning
--pause-on-failOn failure, open a shell, then ask what to do next
--breakpoint / -bPause before a named step
--job / -jPick a job when the workflow has more than one
--imageOverride the runner image
--workdir / -CHost path mounted as /github/workspace
--keepLeave the container running after the job ends
--input / -iSet ${{ inputs.* }} values

Try the bundled demo

git clone https://github.com/kiwi-07/ciwalk.git
cd ciwalk
ciwalk run examples/broken-ci.yml --pause-on-fail

The demo fails on purpose because build/MARKER is missing. In the pause shell:

touch build/MARKER
exit

Choose retry. The step passes, the rest of the job runs. There’s a GIF of this flow in the README.

Design choices that matter

1. Same container, same step environment

The debug shell is not a fresh container. It attaches with the same bash flags, env, and working-directory as the failing step. That’s the whole point—you’re inspecting the failure world, not a guess.

2. Checkout is a bind-mount, not a network clone

uses: actions/checkout@* is treated as: mount the host workspace into /github/workspace. Fast, offline-friendly, and matches how you usually want local iteration to feel.

3. Fail loudly on unsupported Expressions

Silent empty substitution of ${{ ... }} is a footgun. In the MVP:

  • ${{ inputs.* }} and ${{ env.* }} (known keys) substitute
  • Anything else (unknown keys / github.* / etc.) errors instead of becoming ""

That policy cost some compatibility, but it prevented “works on GitHub, mysteriously blank locally” bugs.

4. continue after failure still exits non-zero

If a step failed and you continue past it, the process exit code stays non-zero. The UI should not pretend the job passed.

5. Orphan cleanup is a first-class command

If you kill -9 the CLI, Docker containers can stick around. ciwalk cleanup finds leftover ciwalk-* containers (with --dry-run to preview).

6. Live streaming of step output

Buffered logs feel broken for a debugging tool. Steps stream with exec_start(stream=True) and flushed stdout so you see progress as it happens.

MVP scope (honest)

Supported in v0.1.0:

  • One job per run (--job if needed)
  • runs-on: ubuntu-latest (or omit)
  • Steps: run, name, env, working-directory
  • actions/checkout@*
  • ${{ inputs.* }} / ${{ env.* }} substitution
  • Pause / breakpoint / retry flow
  • ciwalk cleanup

Not yet (intentional cuts, not forever):

  • Matrix / parallel jobs
  • Secrets and full expression language (github.*, secrets.*, …)
  • Arbitrary marketplace uses: actions
  • Job-level needs: / if: / reusable workflows as runnable jobs
  • Caching

Jobs we don’t understand are skipped loudly with a non-zero exit—never a silent green.

If you want to contribute, the highest-value areas are expressions, matrix, and a careful uses: allowlist. File issues or PRs against develop on github.com/kiwi-07/ciwalk.

How AI helped build this (without becoming the product)

I built ciwalk with Cursor as a pair programmer—not as an autopilot that “one-prompted” a repo.

What AI was good at

  • Scaffolding the CLI surface (Typer commands, flag plumbing, help text) once the UX was decided
  • Docker-py orchestration details: create/start/exec, streaming, cleanup labels/names
  • YAML parsing / models for the small Actions subset we chose
  • Test suites and smoke scripts once success criteria were explicit
  • Release choreography: README polish, Linux GitHub Actions workflow, packaging (hatchling), tag + release notes

What AI was not allowed to own alone

  • Product thesis — “like act, but debugger-shaped” came from the pain, not a model
  • Scope cuts — deliberately not cloning all of Actions on day one
  • Failure semantics — exit codes after continue, loud SKIP vs silent pass, expression behavior
  • QA — we treated the agent like a junior: prescribe smoke tests, re-run, demand fixes, then re-verify

The workflow that actually worked

A useful pattern looked like this:

  1. Write a short product brief (problem, non-goals, CLI UX)
  2. Let the agent implement against that brief
  3. Run a prescribed QA matrix myself
  4. Feed failures back as concrete bugs
  5. Re-run the same matrix until green
  6. Add Linux smoke in CI, because Docker bind-mounts on macOS Desktop are not the same as native Linux

AI compressed calendar time. It did not remove engineering judgment. The quality bar was: would I trust this on a laptop with Docker while debugging a real pipeline?

If you’re shipping with agents, the underrated skill is writing acceptance tests and invariants the agent can fail against. Vague “make CI better” prompts produce demos. Explicit “fail loud / clean orphans / stream live / keep env identical” prompts produce tools.

Architecture sketch

Rough module map under src/ciwalk/:

ModuleRole
cli.pyTyper entrypoint (run, cleanup)
parser.py / model.pyWorkflow YAML → internal model
expressions.py${{ }} substitution + hard errors
container.pyDocker lifecycle, mounts, labels
session.pyStep runner, pause/retry loop
shell.pyInteractive attach matching step env

Default image: catthehacker/ubuntu:act-latest (override with --image). Packaging is a pure Python wheel on PyPI; optional standalone binaries can ship on GitHub Releases later.

Repo defaults:

  • Day-to-day work on develop
  • Releases via PR into main, then tag (v0.1.0, …)
  • Linux smoke required on PRs to main

Release checklist we used for v0.1.0

  • Merge developmain after Linux smoke
  • Tag v0.1.0
  • GitHub Release with honest notes (limits included)
  • Publish wheel + sdist to PyPI (ciwalk==0.1.0)
  • Verify: uv tool install ciwalkciwalk --help

Links:

Who should try it today

Good fit if you:

  • Debug shell/run: steps more than third-party marketplace actions
  • Already use Docker locally
  • Want a faster loop than push-and-pray, even if the Actions subset is incomplete

Wait (or open an issue) if you need matrix, secrets, or heavy uses: action graphs on day one.

Closing

ciwalk is small on purpose. The bet is that interactive presence at the failure point is more valuable than pretending to be a full GitHub Actions emulator on the first release.

If you try it on a real workflow and it breaks in an interesting way, please file an issue—that feedback is how the roadmap gets ordered.

uv tool install ciwalk
ciwalk run examples/broken-ci.yml --pause-on-fail

Thanks for reading—and if you build with AI too: keep the taste and the tests human.

← More writing Home