Commands

kapsl is a single binary. Everything is driven by flags: the only positional argument is the tool to run.

Running a tool

kapsl [OPTIONS] TOOL [ARGS]...
kapsl python --version
kapsl [email protected] --version
kapsl rg "TODO" .
kapsl jq '.name' data.json
echo '{"x":1}' | kapsl jq '.x'

Flags must appear before the tool name. Anything after the tool is passed to the tool verbatim, so kapsl jq -e '.foo' correctly sends -e to jq (it is not kapsl's -e/--env).

Use -- to mark the end of kapsl flags explicitly:

kapsl --cap net -- rg -- --hidden pattern

Version pinning

Append @tag to pin to a specific image tag:

kapsl [email protected] --version
kapsl node@18 --version
kapsl node@22 --version

kapsl man <tool>

View a tool's real upstream man page. man is an ordinary composable tool — kapsl man <tool> composes it with <tool>'s own image and finds the page via a real man command inside the container:

kapsl man ls
kapsl man cp

kapsl honors $MANPAGER/$PAGER the same way a real man command does. Only tools whose images ship a staged man page (from the upstream package's own -doc subpackage) have one — this is a catalog-by-catalog migration, not yet every tool.

kapsl --info <tool>

Print a one-line synopsis of a tool, sourced from the global index and — if the image is pulled locally — enriched with labels from the image (sh.kapsl.description, sh.kapsl.version, sh.kapsl.provides, sh.kapsl.security.*):

kapsl --info curl

kapsl --update

Check https://index.kapsl.sh/index.json for a new version and, only if one exists, download it:

kapsl --update

The check is a conditional GET: the cached copy's ETag is sent back, so an unchanged host answers 304 and nothing is re-downloaded. --update reports which of the two outcomes happened — tool index updated (v…) or tool index already up to date — rather than claiming an update that did not happen. This refreshes the index used by --search, --info, and tool resolution. See Tool Index.

kapsl also performs that same check automatically on a schedule — no explicit --update needed for day-to-day use. The schedule is keyed on the last check, not the last download, so a host that answers 304 (or fails) is still re-checked on time instead of immediately. Before running a tool, if the last check is older than index_max_age_secs (default: 1 day, [global] in kapsl.toml), kapsl kicks one off in the background; it never delays the run: it's bounded to a few seconds and abandoned if it doesn't finish before the command completes, falling back to the cached data either way.

One tighter schedule: when a tool name resolves nowhere (not in your kapsl.toml, not in the cached index), kapsl checks the host for a new index before it errors — a newly published entry is usually why the name was missing. That check runs at most once every 30 minutes, recorded next to the cached index, so a genuinely unknown tool costs a bounded retry rather than a fetch on every run. Skipped entirely with --offline.

Installing a shim

kapsl --install rg

Creates ~/.local/bin/rgkapsl. After that, rg works exactly as kapsl rg — kapsl reads argv[0] to learn the tool name.

Listing shims

kapsl --list

Removing a shim

Remove the symlink manually:

rm ~/.local/bin/rg

Searching the tool index

kapsl --search dns
kapsl --search python

Reclaiming disk

kapsl --status                  # what kapsl is storing, by category
kapsl --clean                   # leaked scan scratch + kapsl-env-* images
kapsl --clean cache --dry-run   # show what a cache clean would remove
kapsl --clean cache             # every regenerable cache (asks first)

kapsl --clean [TARGET] takes a comma-separated target list:

TargetRemovesAsks first
scratchleaked per-run scan scratch not owned by a running scanno
imagesenvironment images built by kapsl (kapsl-env-*)no
(default)scratch + imagesno
cacheall of ~/.cache/kapsl — grype database, tool index, policy, scan verdicts, freshness stampsyes
allcache + images + scratchyes
decisionsaccepted scans, trusted projectsyes, always

decisions is never part of all: reclaiming disk and revoking every security decision you have made are different operations, and decisions occupy a few kilobytes. Base images are never touched by any target.

Add --dry-run to print the plan and stop, or --yes to pre-answer the confirmation. Under --non-interactive (or with CI set, or no terminal) a target that would prompt fails closed rather than assuming yes — pass --yes explicitly if a pipeline really means it.

Three things --clean refuses to remove, and reports instead: a symlinked index.json (a deliberate developer setup pointing into a checkout), files kapsl did not write, and scratch belonging to a scan running in another terminal.

Using environment files

kapsl -e @pip:requirements.txt python script.py
kapsl -e @uv python script.py
kapsl -e @npm:package.json node app.js
kapsl -e @pip:. python script.py    # auto-detect

Watch a file and rebuild on change:

kapsl --watch -e @pip:requirements.txt python script.py

See Environments for details.

Kubernetes

kapsl --k8s python --version
kapsl --k8s --namespace production python script.py
kapsl --k8s --node worker-1 htop

See Kubernetes for what is and isn't supported in k8s mode.

kubectl plugin

When kapsl is invoked as kubectl-kapsl, it automatically uses Kubernetes mode:

ln -s ~/.local/bin/kapsl ~/.local/bin/kubectl-kapsl
kubectl kapsl python --version