Exit Codes
The tool's exit code is the exit code of the invocation. That is the load-bearing property for scripts: no wrapper, no translation, no clamping.
$ kapsl python -c "import sys; sys.exit(3)"
$ echo $?
3
A warm, successful run prints nothing and exits 0. The tool's stdout and stderr are its own, unmodified. Shims behave identically — a shimmed git diff --exit-code in a set -e script works because the code survives the shim too.
kapsl's own failures
When the run stops before (or instead of) the tool, the error is kapsl's and the code is kapsl's. Nearly all of them are 1, printed in the orange gutter with the reason and, where useful, the fix:
| Case | What you see | Exit |
|---|---|---|
| Unknown tool | Unknown tool 'X' — not in the tool catalogue. plus kapsl --search X / kapsl --update hints | 1 |
| No container runtime | podman not found on PATH. plus the install line | 1 |
--offline, image not cached | 'ghcr.io/kapsl-sh/X:latest' is not available locally and --offline is set, so it cannot be pulled. Run once without --offline to cache it first. | 1 |
--offline, env not built | Failed to prepare inline environment for '...': environment '...' is not cached locally and --offline is set — cannot build it (installing packages needs network). Run once without --offline to build and cache it. | 1 |
| Env build failure | The build log in a BUILD OUTPUT box, then Failed to prepare inline environment for '...': the package manager exited with 1 | 1 |
| Scan, deny tier | A SCAN FAILED findings line, then Vulnerability scan failed — see details above | 1 |
| Scan, prompt declined | A BLOCKED — Stopped·the tool did not run line, then Vulnerability findings declined | 1 |
| Scan, prompt, no terminal | ... — requires review, but no terminal to ask on (run interactively, use --skip-scan, or adjust security_scan_prompt), then Vulnerability scan requires review, but no terminal to ask on | 1 |
| Invalid config | Invalid configuration in <path>: <what and where> — the run never starts, and there is no fallback to defaults | 1 |
--clean, refused confirmation | the plan, then --clean cache needs confirmation; pass --yes | 1 |
--clean, unknown target | unknown --clean target 'X' (expected: scratch, images, cache, all, decisions) | 2 |
A few shapes, captured:
$ kapsl definitely-not-a-tool
▍ ■ ERROR Unknown tool 'definitely-not-a-tool' — not in the tool
▍ catalogue.
▍ kapsl --search definitely-not-a-tool find similar tools
▍ kapsl --update refresh the catalogue
$ kapsl --offline pytest --version
▍ ■ ERROR 'ghcr.io/kapsl-sh/pytest:latest' is not available locally and
▍ --offline is set, so it cannot be pulled. Run once without
▍ --offline to cache it first.
$ kapsl [email protected]:nonexistent-pkg-zzz-42 d.py
▍ ■ BUILDING kapsl-env-python-3.12:uv-aeb86a13
▍ ■ BUILDING installing · 0%
▍
▍ ■ BUILD OUTPUT ──────────────────────────────────────────────────────────────
▍ │ Using CPython 3.12.14 interpreter at: /kapsl/dist/python-3.12.14/bin/python
▍ │ × No solution found when resolving dependencies:
▍ │ ╰─▶ Because nonexistent-pkg-zzz-42 was not found in the package registry
▍ │ and you require nonexistent-pkg-zzz-42, we can conclude that your
▍ │ requirements are unsatisfiable.
▍ └────────────────────────────────────────────────────────────────────────────
▍
▍ ■ FAILED build kapsl-env-python-3.12:uv-aeb86a13
▍ ■ ERROR Failed to prepare inline environment for
▍ '[email protected]:nonexistent-pkg-zzz-42': the package manager exited
▍ with 1
One case that is not a failure: declining the project trust prompt (see Troubleshooting → Trust prompt). A declined .kapslrc means the overlay is simply not applied; the tool still runs and the exit code is the tool's.
Where the code is not distinct from the tool's
Most of kapsl's failures share exit code 1 with the most common tool exit code, so the code alone does not say who failed. The gutter does: a line in the orange gutter with an ■ ERROR marker is kapsl talking; a run that ends with only the tool's own output and an exit code is the tool talking. If a script needs to distinguish them, the reliable signals are the gutter line and the message text, not the number.
The one distinct code: an unknown --clean target exits 2, so a script can tell a bad cleanup argument apart from a failure that happened while cleaning.
Signals
kapsl supervises the engine process rather than exec'ing away, so a signal death comes back as the shell expects — 128 + signal number. A container killed with SIGTERM reports 143, one killed with SIGKILL reports 137; if the engine process itself is signalled, kapsl maps the death the same way (and falls back to 1 only if the signal number is unrecoverable). The same supervisor is what restores terminal state the tool left dirty, which is why a less or a pager in a tool does not wreck your shell.
Pipelines
In a pipeline, the exit code is the shell's business, not kapsl's: kapsl tool | cmd reports cmd's code, and ${PIPESTATUS[0]} (bash) or pipestatus[1] (zsh) retrieves the tool's. Nothing about running under kapsl changes that; it is the same contract as any command on the left side of a pipe.