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 patternVersion pinning
Append @tag to pin to a specific image tag:
kapsl [email protected] --version
kapsl node@18 --version
kapsl node@22 --versionkapsl 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 curlkapsl --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/rg → kapsl. After that, rg works exactly as kapsl rg — kapsl reads argv[0] to learn the tool name.
Listing shims
kapsl --listRemoving a shim
Remove the symlink manually:
rm ~/.local/bin/rgSearching the tool index
kapsl --search dns
kapsl --search pythonReclaiming 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:
| Target | Removes | Asks first |
|---|---|---|
scratch | leaked per-run scan scratch not owned by a running scan | no |
images | environment images built by kapsl (kapsl-env-*) | no |
| (default) | scratch + images | no |
cache | all of ~/.cache/kapsl — grype database, tool index, policy, scan verdicts, freshness stamps | yes |
all | cache + images + scratch | yes |
decisions | accepted scans, trusted projects | yes, 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