Configuration File
The configuration file is boks.toml. boks looks for it in:
| Platform | Default path |
|---|---|
| Linux / macOS | ~/.config/boks/boks.toml |
Override the path with BOKS_CONFIG:
BOKS_CONFIG=./boks.toml boks python --version
If the file does not exist, boks uses built-in defaults.
Full example
[global]
forward_ssh_agent = true
mount_git_config = true
protected_dotfiles = ["~/.ssh", "~/.gnupg", "~/.aws"]
# pass_env is ADDITIVE to the built-in defaults (RUST_LOG, NO_COLOR, TZ, CI,
# PYTHONUNBUFFERED, ...) — you do not need to restate those. Prefer naming
# variables exactly; a glob like "AWS_*" sweeps in credentials, and
# HTTP_PROXY can carry them in its URL.
pass_env = ["MYAPP_PROFILE"]
k8s_namespace = "tools"
[tools]
rg = "boks.sh/rg:latest"
python = { image = "boks.sh/python:3.12", command = "python3" }
# per-tool pass_env: additive to [global], and the opt-in for a protected var
aws-cli = { image = "boks.sh/aws-cli:latest", pass_env = ["AWS_SECRET_ACCESS_KEY"] }
[theme]
enabled = true[global] section
| Key | Type | Default | Description |
|---|---|---|---|
runtime | string | (auto) | Container engine: "podman" or "docker". Default: podman, falling back to docker when podman isn't installed. --runtime overrides per run |
offline | bool | false | Run every invocation as if --offline were passed — for a permanently offline host. See Offline Mode |
verbosity | string | "normal" | How much boks prints to stderr. "normal" (default) shows transient progress — a spinner while pulling/scanning/building, erased on completion, so a successful run leaves nothing behind. "verbose" keeps the output: ✔ READY / ✔ BUILT milestones persist, plus background info lines. "quiet" prints basically nothing. --verbose/-v and --quiet/-q override per run. Errors and user-actionable warnings always print. |
forward_ssh_agent | bool | true | Mount SSH_AUTH_SOCK into the container |
mount_git_config | bool | true | Mount ~/.gitconfig read-only into the container when git is present (primary, an inline-package install that pulls it in, or a composed tool) — not unconditionally into every container. Set false to suppress even when git is present. |
protected_dotfiles | list | ["~/.ssh", "~/.gnupg"] | Sensitive host dotfiles made visible but access-denied inside containers (reads fail EACCES, not ENOENT) when exposed via the working-directory mount. ~-expanded against $HOME; entries must start with ~/. A tool opts into a path by declaring it as a dotfile; set to [] to opt out of all defaults. See Security Layers → Sensitive dotfile protection. |
pass_env | list | [] | Glob patterns for host env vars to pass through, additive to boks's built-in defaults (see Environment Variables) — you do not need to restate RUST_LOG, NO_COLOR, TZ, CI and friends. Can also be set per-tool ([tools.<name>] pass_env = [...]), additive to this list. For a one-off, prefer -E NAME on the command line. |
protected_env_vars | list | ["AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "AWS_SECURITY_TOKEN", "GITHUB_TOKEN", "GH_TOKEN", "NPM_TOKEN", "DOCKER_PASSWORD"] | Env-var names a pass_env glob can never satisfy — the container-env analog of protected_dotfiles. A tool opts in via its OWN [tools.<name>] pass_env (naming it in [global] pass_env is not enough); set to [] to opt out of all defaults. See Environment Variables → Protected environment variables. |
protected_project_files | list | [".env", ".env.*", ".envrc"] | Project-tree FILES (matched by name directly under the CWD only, not subdirectories) a container may never read or write — the CWD-relative analog of protected_dotfiles. .env and variants hold local secrets; denying them (reads return empty, writes fail EROFS) prevents exfiltration as raw bytes, while a tool that needs the values opts in via its own dotenv. Entries must be CWD-relative (no leading / or ~/); set to [] to opt out of all defaults. See Security Layers → Project file (.env) protection. |
k8s_namespace | string | "default" | Default Kubernetes namespace for --k8s |
k8s_max_pod_seconds | int | 43200 (12h) | Hard wall-clock cap (activeDeadlineSeconds) on how long a --k8s pod may run — a backstop if the local boks process is killed before its own cleanup runs. See Kubernetes → Pod lifetime and cleanup |
security_scan_enabled | bool | true | Vulnerability-scan images before running them |
security_scanner_image | string | ghcr.io/anchore/grype:latest | Scanner container image (pin a tag to freeze it) |
security_scan_max_age_secs | int | 86400 | How long a scan verdict is trusted; 0 scans every run |
security_scan_deny | string | "never" | Findings at or above this severity abort the run outright, no review possible (critical, high, medium, low, never). Off by default — boks doesn't refuse to run a tool you asked for explicitly; set to critical to restore a hard block |
security_scan_prompt | string | "high" | Findings at or above this severity (below the deny threshold) show ONE combined summary across every image the run touches and ask Continue? y/N/d (details); fails closed without a TTY. high's "at or above" semantics already cover critical too. Same values as security_scan_deny |
security_scanner_image_max_age_secs | int | 604800 | How often the scanner image itself is re-pulled |
image_max_age_secs | int | 604800 | Freshness window for tool images. A floating tag (python:3.13, nmap:latest) cached locally for longer than this is re-pulled before a run, so tools stay current without an update command. 0 checks every run; a very large value disables it. Digest-pinned and local images are never refreshed. A failed refresh falls back to the cached image. See Freshness below. |
index_max_age_secs | int | 86400 | Freshness window for the cached global tool index + policy artifact. A cache older than this (or missing) is refreshed in the background before a run, same "no --update needed" posture as image_max_age_secs. 0 checks every run; a very large value disables it. See Freshness below. |
Freshness
boks has no update or upgrade command — by design. A tool named without a
pinned digest is expected to stay current automatically. Tags like
python:3.13 and nmap:latest float forward on every upstream rebuild, so a
cached image goes stale unless something re-checks the registry.
image_max_age_secs (default 604800, i.e. 7 days) is that something. When a
run resolves an image that is already local but was pulled more than the window
ago, boks best-effort re-pulls it before scanning and running:
- Best-effort: if the refresh pull fails (no network, registry down), boks uses the cached image and continues — the freshness window is a convenience, never a blocker. The vulnerability scan remains the security backstop.
- Never refreshed: digest-pinned images (
tool@sha256:…, set viaboks.toml) are immutable by construction, and locally-built environment images (boks-env-*) exist in no registry. Only registry-hosted floating tags refresh. - A failed scan also triggers a refresh: if a scan finds CRITICAL vulnerabilities in a floating tag, boks re-pulls the newest version once and, if the image ID changed (the tag was rebuilt), re-scans it — a rebuild may have fixed the issue. If the tag hadn't moved, or the refresh failed, the scan failure stands and the run is blocked.
Set image_max_age_secs = 0 to check for a newer image on every run (slower,
maximally current). Set it to a very large value to effectively disable the
window.
The same idea applies to the index itself: index_max_age_secs (default
86400, i.e. 1 day) governs the cached index.json/policy.json — the data
--search, --info, and tool resolution all read. A cache older than the
window (or missing entirely) is refreshed automatically before a run, same
as boks --update but without you having to run it. This refresh never
delays the command it's attached to: it's bounded to a few seconds and
simply abandoned — falling back to whatever's cached — if it hasn't finished
by the time the run would otherwise be done. Skipped entirely with
--offline.
Unknown keys are rejected with an error — see Config Validation.
[theme] section
boks's own terminal branding: the orange left-bar status lines (always on,
host-side, not configurable here — see the security docs), plus a colored
PS1 and man-page (less/groff) color scheme injected into every
container.
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | true | Inject boks's colored PS1 and man-page (LESS_TERMCAP_*/GROFF_NO_SBIT) env vars into every container. Harmless for tools that never read PS1 or view man pages. Set false to opt out — e.g. if your own shell dotfiles already set PS1 and you'd rather boks not set it first. |
Both the default PS1 and the boks status lines automatically degrade for
the terminal they're running in: truecolor (COLORTERM=truecolor/24bit)
or a widely-supported 256-color fallback otherwise, and the ▍ bar glyph
or a plain \| when the host locale (LANG/LC_ALL) isn't UTF-8.
[tools] section
Maps tool names to container images. See Tool Mappings for the full syntax.
[tools]
rg = "boks.sh/rg:latest"
python = { image = "boks.sh/python:3.12", command = "python3" }[package_providers] section
User-defined package providers, so you can teach boks a package manager without a code change. Each entry is a pair of templated install commands; boks wraps them in the same build path a built-in provider uses (same base image, scan gate, and cache). See Custom Providers for the full model and worked examples.
[package_providers.mypm]
install_command_inline = "RUN mypm install {packages}"
install_command_file = "RUN mypm install -r /tmp/{file}"| Key | Required | Description |
|---|---|---|
install_command_inline | yes | Dockerfile RUN for an inline package list (tool:pkg1,pkg2). Must contain {packages}, replaced by the space-joined names |
install_command_file | yes | Dockerfile RUN for a file-based install (-e @name:manifest). Must contain {file}, replaced by the copied manifest's filename |
The provider name (the table key) must be ASCII alphanumeric and may not
shadow a built-in (pip, npm, apk, …). Both are enforced at load time,
along with the placeholder presence — a typo'd template fails loudly rather
than silently installing nothing.
[dev] section
Settings for working on boks rather than with it. Everything here changes where boks gets its own data from, so everything here prints a warning on every run.
[dev]
index_dir = "/Users/you/src/boks/boks-index"| Key | Required | Description |
|---|---|---|
index_dir | no | Directory holding a locally built index.json and policy.json, read instead of the fetched cache |
Point index_dir at a checkout's boks-index/ and your own index edits take
effect immediately, with no publish step. boks --update does not refresh it,
and it is under no obligation to match the published catalogue — a tool that
resolves for you may not exist for anyone else.
Why it warns every single run, and why that is not excessive. The index
decides which image every tool name resolves to, so an overridden one can point
boks python anywhere. The alternative people reach for instead — a symlink at
~/.cache/boks/index/index.json — is invisible, survives forever, and silently
outlives the checkout it points into. Both are detected and both warn:
▍ WARNING [dev] index_dir is NOT managed by boks — it resolves to
/Users/you/src/boks/boks-index. It will not be refreshed, and
may not match the published catalogue.
The failures this prevents all have the same shape — boks reading a different
file than you believe it is reading — and they surface a long way from the
cause. Real examples: hours of edits to an index that was never being read
because the symlink pointed at a different checkout; a "verification" that
passed against the wrong file and confirmed nothing; and a
catatonit: failed to exec pid1 that named neither the index nor the version
involved. A warning you only see once you already suspect the index is worth
nothing.
BOKS_INDEX_DIR
Environment override for the same directory, taking precedence over
[dev] index_dir:
BOKS_INDEX_DIR=/path/to/boks-index boks python --version
It exists for test suites and CI, which must not depend on whatever
happens to be in a developer's cache. boks's own integration tests set it to
the checkout's boks-index/, so a run resolves against the tree it was built
from and nothing else — the same commit then passes or fails identically on
every machine, and an index change is exercised by the suite in the commit
that makes it.
It warns like any other override, naming BOKS_INDEX_DIR as the source so a
reader knows to look at the environment rather than the config file.