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:
- Edit workflow YAML or application code
- Commit and push
- Wait for a runner
- Read truncated logs
- Guess what the filesystem / env / working directory looked like
- 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:
| Flag | Meaning |
|---|---|
--pause-on-fail | On failure, open a shell, then ask what to do next |
--breakpoint / -b | Pause before a named step |
--job / -j | Pick a job when the workflow has more than one |
--image | Override the runner image |
--workdir / -C | Host path mounted as /github/workspace |
--keep | Leave the container running after the job ends |
--input / -i | Set ${{ 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 (
--jobif 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:
- Write a short product brief (problem, non-goals, CLI UX)
- Let the agent implement against that brief
- Run a prescribed QA matrix myself
- Feed failures back as concrete bugs
- Re-run the same matrix until green
- 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/:
| Module | Role |
|---|---|
cli.py | Typer entrypoint (run, cleanup) |
parser.py / model.py | Workflow YAML → internal model |
expressions.py | ${{ }} substitution + hard errors |
container.py | Docker lifecycle, mounts, labels |
session.py | Step runner, pause/retry loop |
shell.py | Interactive 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
develop→mainafter 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 ciwalk→ciwalk --help
Links:
- Repo: https://github.com/kiwi-07/ciwalk
- PyPI: https://pypi.org/project/ciwalk/
- Release: https://github.com/kiwi-07/ciwalk/releases/tag/v0.1.0
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.