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:

CaseWhat you seeExit
Unknown toolUnknown tool 'X' — not in the tool catalogue. plus kapsl --search X / kapsl --update hints1
No container runtimepodman not found on PATH. plus the install line1
--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 builtFailed 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 failureThe build log in a BUILD OUTPUT box, then Failed to prepare inline environment for '...': the package manager exited with 11
Scan, deny tierA SCAN FAILED findings line, then Vulnerability scan failed — see details above1
Scan, prompt declinedA BLOCKED — Stopped·the tool did not run line, then Vulnerability findings declined1
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 on1
Invalid configInvalid configuration in <path>: <what and where> — the run never starts, and there is no fallback to defaults1
--clean, refused confirmationthe plan, then --clean cache needs confirmation; pass --yes1
--clean, unknown targetunknown --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.