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" }
FieldDescription
imageOCI image reference (one of image/alias)
aliasAnother tool name this entry resolves through (one of image/alias)
commandBinary to run instead of the image's default — for multi-tool images (dig/nslookup/host from one image) or wrapper entrypoints
capabilitiesAuto-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
dotfilesHost dotfiles mounted at /kapsl/home/<rel>; also the opt-in for a protected path (below)
pass_envEnv-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)
seccompOverride 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
descriptionShown 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 listDefaultOpt-in
protected_dotfiles (~/.ssh, ~/.gnupg, ~/.aws, …)access-denied inside containers, even via the CWD mountdeclare 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 EROFSdeclare 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 isolated

Tool 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:

  1. Your local config[tools] here (an alias resolves through its target).
  2. The signed catalogue — the fetched, signature-verified index.
  3. 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.