Environment variables

A container's environment is built, not inherited. Nothing from the host crosses in by default, except a short, curated list — and every variable that does is either a control knob or something a tool needs to behave the same way it would on your terminal. Secrets never cross unless a tool's own declaration says it reads them.

What crosses by default

The built-in list, in full:

VariablesWhy they are here
TERM, COLORTERM, NO_COLOR, CLICOLOR, CLICOLOR_FORCE, FORCE_COLORTerminal and colour. NO_COLOR closes a real inconsistency: kapsl already honours the host's setting for its own output, while the tool producing the output never saw it.
LANG, LC_ALL, LC_*, TZLocale and time. LC_* is the only glob in the list, and it is safe because POSIX closes the family.
CI, SOURCE_DATE_EPOCHCI detection and reproducible builds.
RUST_LOG, RUST_LOG_STYLE, RUST_BACKTRACE, RUST_LIB_BACKTRACEEcosystem-specific, and they belong here rather than on a tool: the Rust tools (rg, ruff, fd, bat, delta, …) are standalone compiled binaries declaring no dependencies, so no rust tool is ever composed into them to hang these on. Attaching them to cargo would mean kapsl rg silently stopped honouring RUST_LOG.
DEBUG, VERBOSEGeneric verbosity knobs, honoured across every ecosystem, so there is no single tool to attach them to.
POSIXLY_CORRECTRead by glibc's own getopt, so it changes argument parsing for every GNU program rather than one family. It is presence-based (the value is ignored), so it cannot carry a secret, name a path, or redirect egress. What it does change is real: with it set, getopt stops permuting arguments, so ls foo -l treats -l as a filename. Forwarding it means a kapsl-run tool parses its arguments the way the same tool would on the host — which is the whole point of this list.

kapsl also sets a small number of values itself: HOME, the XDG_* base directories and TMPDIR all point at /kapsl/home, an ephemeral tmpfs inside the container — not your home. A tool that writes ~/.python_history succeeds, and the file vanishes with the run. The CA bundle and terminfo pointers go to /kapsl/etc, and only for a composable primary (a tool that runs other tools).

The point of the list: it is closed and short, which is what makes a deliberately broad pass_env = ["*"] or ["LD_*"] safe rather than a remote-code-execution primitive.

Adding to it

Three ways, in order of permanence:

kapsl -E AWS_REGION python script.py        # one run: pass the host's value
kapsl -E FOO=bar python script.py           # one run: set a value
# kapsl.toml — persistent, every container
[global]
pass_env = ["AWS_REGION", "GOPATH"]
# kapsl.toml — per tool
[tools.mytool]
pass_env = ["MYTOOL_CONFIG"]

Patterns are glob-shaped (LD_*, AWS_*), and your pass_env adds to the built-in list rather than replacing it — the built-in list is kapsl's own opinion, not user config.

A statement about what a tool reads, never an authorisation to hand it a secret: a variable on the protected list (protected_env_vars — by default AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, AWS_SECURITY_TOKEN, GITHUB_TOKEN, GH_TOKEN, NPM_TOKEN, DOCKER_PASSWORD) is passed only when the tool's own pass_env names it. A broad global pass_env = ["AWS_*"] matches the variable, but that match alone is not enough — the global says "my world has these", the tool has to say "I read this one". A global leak that only becomes a tool leak once the tool agrees is how the list stays a deny list instead of a hope.

The full treatment of the three deny lists — environment variables, dotfiles, project files — and their per-tool opt-ins is in the security model.

kapsl's own variables

Variables that configure kapsl itself, not the container:

VariableEffect
KAPSL_CONFIGPath to the config file. A missing file at the default location is normal (fresh install); a missing file at an explicitly named path is an error, because silently running with defaults would hide the typo.
KAPSL_ARGSFlag pass-through for shims: the only way a symlinked tool receives kapsl flags. Shell-split, CLI flags win, and no tool names inside — it is flags only.
KAPSL_RUNTIMEEngine override for this invocation (podman/docker). No warning, deliberately: you named it.
KAPSL_INDEX_DIRRead the catalogue from a local directory instead of the signed index. Unsigned — it is for index development, and kapsl warns about it on every run.
KAPSL_INDEX_URLBase URL where the catalogue is fetched from, for this invocation. Same reasoning as KAPSL_INDEX_DIR — it removes the DNS and CDN layer from artifact fetching — aimed at sealed and air-gapped environments.
CIDetected, not set by you: its presence is what turns kapsl non-interactive automatically in a pipeline.