Custom Providers

boks ships with built-in providers for the common package managers (pip, npm, apk, …). If you need one it doesn't cover — a niche manager, an internal tool, or a different install convention — you can declare it in boks.toml without writing Rust or opening a PR.

The idea

A provider is almost entirely data: the only thing that varies between built-in providers is the install command. A custom provider is therefore just two templated commands, and boks runs them through the same build path a built-in provider uses — same base image, same vulnerability scan, same content-hash cache. There is no plugin system, no WASM, no separate process.

Declaring a provider

Add a [package_providers.<name>] table to boks.toml:

[package_providers.mypm]
install_command_inline = "RUN mypm install {packages}"
install_command_file   = "RUN mypm install -r /tmp/{file}"

Two placeholders are substituted at build time:

PlaceholderInReplaced by
{packages}install_command_inlinethe space-joined inline package list (e.g. numpy pandas from tool:numpy,pandas)
{file}install_command_filethe copied manifest's filename (e.g. requirements.txt)

Each value is a Dockerfile RUN instruction, run as root during podman build — exactly like a built-in provider's install command. The manifest file is COPY'd to /tmp/ automatically before the file-based RUN runs.

Using it

A custom provider is invoked explicitly with -e @<name>:

# File-based: install from a manifest
boks -e @mypm:project.deps alpine@3.21 cat /tmp/installed.txt

# Inline packages: the explicit provider selects mypm for the install
boks -e @mypm alpine@3.21:pkg1,pkg2 cat /tmp/installed.txt

Note the @<version> form (tool@3.21) when you also pass inline packages: tool:3.21 would be parsed as image tool with package 3.21, since : introduces the package list. The bare tool:pkg1,pkg2 form (without -e) infers a built-in provider from the image name, so a custom provider is always selected explicitly via -e @name.

The tool named here is always an indexed tool or one you mapped in your own boks.toml — boks resolves from those two places and nowhere else, so a raw image name (alpine, ubuntu, or a registry path) is not a usable base.

Auto-detection (-e @name:. or -e @name with no file) does not apply to custom providers in this version — a custom provider has no default manifest filenames, so you always name the file (or the inline packages) explicitly.

The base image

A custom provider builds on the tool's resolved base image — the same image boks <tool> would pull, with the same freshness window (image_max_age_secs) and the same digest-based cache invalidation. When the base image is rebuilt upstream (a CVE fix, a new minor version), the cached environment rebuilds automatically on the new base.

You cannot name an arbitrary FROM image in a custom provider. This is deliberate: an arbitrary base would bypass boks's pull, freshness, and scan path. If you need a different base, pin the tool image in [tools] and the provider builds on top of it:

[tools]
mytool = "my-registry.example.com/mytool:custom"

[package_providers.mypm]
install_command_inline = "RUN mypm install {packages}"
install_command_file   = "RUN mypm install -r /tmp/{file}"

What a custom provider cannot do

These are deliberate v1 limits, not bugs:

  • No package_rules matching. Built-in providers can declare rules like "mount ~/.cache/huggingface when huggingface_hub is installed" because they ship a manifest parser. A custom provider has no parser, so package-driven dotfile mounts and capability auto-enablement don't apply to it — the same as the built-in gem/hex/luarocks providers, whose manifests are DSLs rather than data. If you need that, use the matching built-in provider (e.g. pip).
  • No auto-detection. A custom provider is never inferred from an image name and has no default manifest filenames. Always invoke it with -e @name.
  • No custom base image. See above.

Security model

A custom provider does not weaken boks's security posture:

  • The scan gate still runs. A custom-provider environment is scanned like any other built image. The only way to skip it is the existing security_scan_enabled = false or --skip-scan, which apply to every provider equally.
  • No aliasing of built-in names. A custom provider's name must be ASCII alphanumeric and may not shadow a built-in (pip, npm, …). Both are enforced at config load, so a [package_providers.pip] table is rejected immediately rather than silently overriding the curated provider.
  • No host-path backdoor. A custom provider has no index entry, so it participates in no package_rules — it cannot request dotfile mounts or capabilities. The install command runs in the build container, not on the host.

The install command itself is yours to author: boks does not sanitize the template (it's your provider, run as RUN in podman build exactly like a built-in). The inline package list is whitespace-validated upstream, so {packages} cannot carry a newline or shell metacharacter injected through the package list — but the template string is taken verbatim, by design.