Tool mappings
The [tools] section of ~/.config/kapsl/kapsl.toml is where a tool name means an image. It is the only place, besides the signed catalogue, that this decision is made — and unlike a project overlay, only you can change it: a cloned repository never touches your config.
The annotated tour of every section lives in boks.example.toml in the repository; this page is the what is half. The full field reference is Configuration file.
Mapping a name to an image
Simple form — a name to an image, using the image's default CMD/ENTRYPOINT:
[tools]
rg = "kapsl.sh/rg:latest"
Table form, for anything beyond the image:
[tools]
python = { image = "kapsl.sh/python:3.12", command = "python3" }
dig = { image = "kapsl.sh/dns:latest", command = "/usr/bin/dig" }| Field | Description |
|---|---|
image | OCI image reference (one of image/alias) |
alias | Another tool name this entry resolves through (one of image/alias) |
command | Binary to run instead of the image's default — for multi-tool images (dig/nslookup/host from one image) or wrapper entrypoints |
capabilities | Auto-enabled escalations: net, rw, browser, clipboard, pid. Each is only auto-enabled when you did not pass the corresponding flag — --cap ro still beats a rw capability |
dotfiles | Host dotfiles mounted at /kapsl/home/<rel>; also the opt-in for a protected path (below) |
pass_env | Env-var glob patterns passed through, additive to [global] pass_env; also the opt-in for a protected name (below) |
dotenv | .env-family files injected as variables, read host-side; the opt-in for the protected-project-files deny list (below) |
seccomp | Override the network class the seccomp shim confines this tool to: net-denied, net-connect, net-listen, unconfined — the values kapsl --info prints. No image required alongside; there is deliberately no CLI flag, because relaxing a confinement boundary should cost opening a file someone can review |
description | Shown in kapsl --search |
Dotfiles: kind and readonly
[tools]
gh = { image = "kapsl.sh/gh:latest", dotfiles = ["~/.config/gh"] }
vim = { image = "kapsl.sh/vim:latest", dotfiles = [{ path = "~/.vimrc", kind = "file" }] }
Each entry mounts a host path at /kapsl/home/<rel> (where the container's HOME points), so tool config persists across runs. A bare string is kind = "dir"; kind = "file" bootstraps a missing path as an empty regular file instead — correct for ~/.vimrc, wrong for ~/.aws.
readonly = true mounts :ro instead of the default :rw — for paths a tool only ever needs to read:
[tools]
ssh-tool = { image = "…", dotfiles = [{ path = "~/.ssh", kind = "dir", readonly = true }] }
The tool can read; a write fails EROFS; the host file is never touched either way.
The three opt-ins, for the three deny lists
Every deny list in the security model has exactly one opt-in, and it lives in the tool's own entry — not in a global setting:
| Deny list | Default | Opt-in |
|---|---|---|
protected_dotfiles (~/.ssh, ~/.gnupg, ~/.aws, …) | access-denied inside containers, even via the CWD mount | declare the path in the tool's dotfiles |
protected_env_vars (AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, …) | never satisfied by a pass_env glob, not even AWS_* | name it exactly in the tool's own pass_env |
protected_project_files (.env, .env.*, .envrc) | read returns empty, write fails EROFS | declare the file in the tool's dotenv — the values are injected, the file's bytes stay unreadable |
[global]
pass_env = ["AWS_*"] # does NOT grant AWS_SECRET_ACCESS_KEY — it's protected
[tools]
aws = { image = "kapsl.sh/aws:latest", pass_env = ["AWS_SECRET_ACCESS_KEY"] }
For dotenv: variables already passed through the real environment are not overridden (real-env-wins, standard dotenv convention), and the opt-in does bypass protected_env_vars for a protected name present in the file — the tool explicitly asked for that file's contents. The curated index declares sensible defaults (node layers .env + .env.local; python/ruby/php get .env); a tool without dotenv sees the denied empty file.
Aliases
An alias entry resolves another tool name through the normal chain and applies its own overrides on top. This is how named variants are made:
[tools]
python-net = { alias = "python", capabilities = ["net"] }
The alias's own command and capabilities win over its target's; aliases may not point at other aliases. Combined with shims, variants feel native:
kapsl --install python-net
python-net -c "import urllib.request; …" # containerized, network enabled
python -c "…" # still fully isolatedTool groups
A [groups] table names a set of tools under one name, usable anywhere a composed tool can be named: an -e compose segment, a project's compose list, a tool's index-declared default_compose.
[groups]
mytools = ["ls", "rg", "jq"]kapsl -e mytools bash # ls, rg, and jq all reachable on PATH inside bash
A group is not itself a tool — no image/bin, can't be run directly. Groups don't nest. The index ships built-in groups — coreutils is what bash's own default_compose uses, so a bare kapsl bash script.sh already has ls/cat/cp/sed/grep/… available — and your own entry of the same name shadows the built-in. Composing a group grants none of its members' auto-capabilities, same as composing a single tool.
Resolution order
For a tool name, kapsl checks:
- Your local config —
[tools]here (an alias resolves through its target). - The signed catalogue — the fetched, signature-verified index.
- Error. A name in neither is not a tool:
kapsl --info elixir
ERROR unknown tool 'elixir'. It is in neither the catalogue nor your
local config, so kapsl has nothing to report about it.
Override a catalogue tool by adding it to your local config; there is no fallback image to drift onto.
[enforce]: the floor
Precedence, lowest to highest: built-in defaults → your [tools] → a trusted .kapslrc → CLI flags. A project file usually should win over [tools] — it is the more specific scope, and you reviewed it at the trust prompt. What it can't express is "I set this limit and I mean it":
[enforce.gawk]
seccomp = "net-denied"
[enforce.<name>] takes precedence over everything, .kapslrc included. [tools] states a default; [enforce] states a floor. Only seccomp is honoured here today — any other field in an [enforce] entry is a load-time error rather than a silent no-op, because an [enforce] entry that looks binding and isn't would be the worst possible failure for this table.