Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Toolboxes

What is a toolbox?


A Thorium toolbox is a portable, self-contained collection of tools — image and pipeline configurations bundled together so they can be shared between Thorium instances. A toolbox is described by a single toolbox.json manifest that lists every image and pipeline (along with their Thorium configs), optionally alongside the container image files themselves for offline transfer.

thorctl toolbox provides the full lifecycle for working with toolboxes:

CommandPurpose
thorctl toolbox initScaffold config.toml, image, and pipeline files for a new toolbox
thorctl toolbox buildGenerate a toolbox.json from a directory of manifests
thorctl toolbox exportPull images/pipelines out of a running Thorium instance into a toolbox
thorctl toolbox importImport a toolbox.json (local file or URL) into a Thorium instance
thorctl toolbox removeDelete a previously imported toolbox’s pipelines and images
thorctl toolbox diffShow what importing an on-disk toolbox would change, git-diff style
thorctl toolbox build-imagesBuild (and --push) the container images in a toolbox locally

Which command/flag for which task

You want to…Use
Start a brand-new toolbox by handinit toolbox (creates config.toml + tool stubs), then build
Add one image/pipeline stub to an existing toolboxinit image/init pipeline (positional path = where it lands); pass -c <config.toml> to resolve/validate against the toolbox without moving files
Capture tools from a running instanceexport (writes the whole repo + toolbox.json)
Append more tools into an existing toolbox repoexport -p …/-i … with -o <toolbox-dir> or -c <toolbox-dir>/config.toml (which defaults the output to that dir); the existing config.toml is reused automatically
Refresh every tool already in a toolbox from the instanceexport -c <toolbox-dir>/config.toml --overwrite (no -g/-p/-i) — re-pulls and updates all existing tools in place
Seed a new toolbox’s settings from another toolbox’s config.toml into a different dirinit toolbox -c …, or export -c <other>/config.toml -o <new-dir> (explicit -o required)
Place one resource’s files in a specific direxport -i group/name=dest / -p group/name=dest (placement only)
Fold an exported image into an existing Dockerfile direxport -i group/name=<dir-with-Dockerfile> (auto-sets build = true)
Set the default export layoutinit toolbox --image-path … --pipeline-path … (writes export_*_path in config.toml)
Update a matched tool that differs (export)--overwrite — updates in place (image: Thorium config only, keeps manifest.toml build settings); without it a differing tool is skipped with a warning
Overwrite per-tool files--overwrite (init, import-conflict mode)
Overwrite config.toml--overwrite-config (export, init toolbox)
Update existing cluster network policies--update-network-policy (import)
Move images to an air-gapped instanceexport --with-images, then import --image-path-prefix …

Toolbox layout


A toolbox is a directory tree. Each tool lives in its own folder with a manifest.toml, a JSON Thorium config, and (for images) the docker build context. At the root are a config.toml and the generated toolbox.json:

my-toolbox/
├── config.toml
├── toolbox.json                # generated by `build` (and by `export`)
├── images/
│   └── clamav/
│       ├── manifest.toml
│       ├── clamav.json         # Thorium image config
│       ├── description.md      # image docs (optional); becomes the config's description
│       ├── clamav.tar.gz       # only present in --with-images bundles
│       └── Dockerfile
└── pipelines/
    └── antivirus/
        ├── manifest.toml
        ├── description.md      # pipeline docs (optional); becomes the config's description
        └── antivirus.json      # Thorium pipeline config

Tool docs (description.md)

A tool’s description can live two places: the description field inside its JSON/inline Thorium config, or a standalone description.md beside its manifest.toml. The Markdown file exists because authoring a long description as an escaped one-line string inside JSON is painful — keeping the docs in their own .md file lets you write, diff, and review them as real Markdown.

Precedence — description.md wins. When toolbox build rolls a tool into toolbox.json, it reads any description.md next to the manifest, trims trailing whitespace, and writes it into the config’s description field, replacing whatever the inline config held. If the inline value was non-empty and differed from the file, the build prints a warning so the override is visible. If description.md is absent (or present but empty), the inline config’s description is left untouched. A config that is resolved from a URL at import time (config_from pointing at a URL) can’t be edited during build, so a description.md beside it is only warned about, never injected.

toolbox export always writes a description.md for each exported resource — its content is the resource’s description, or an empty file when the description is unset/empty (never the literal text null, which would otherwise become the description on re-import). toolbox init scaffolds a stub. Descriptions surface in the Thorium UI and thorctl images get, so they’re how users — and AI agents — decide which tool to run.

Toolbox config (config.toml)

Names the toolbox and the registry its images are stored in. The registry may be left empty (for example when exporting a toolbox you intend to push to a registry you don’t know yet).

name = "My Toolbox"
registry = "ghcr.io/org/repo"
# registries = []                 # additional registries to tag images in
# image_path_prefix = ""          # default registry path prefix for bundled images
# export_image_path = "images"    # where `export` places image dirs (relative to the toolbox root)
# export_pipeline_path = "pipelines"
# bundled_images = true           # set automatically by `export --with-images`

# toolbox-wide default base-image config (see "Overriding base images" below)
# [base_image]
# image = "ubuntu:22.04"
# image_arg = "IMAGE"
# token = "BASE_REGISTRY_TOKEN"   # CI/CD variable NAME (pass-through; unused by build-images)
# user = "BASE_REGISTRY_USER"     # CI/CD variable NAME (pass-through)
# allow_override = true

export_image_path / export_pipeline_path set where export writes each tool’s directory (default images/<name> and pipelines/<name>); set them at scaffold time with thorctl toolbox init toolbox --image-path … --pipeline-path …. They only affect where export places files — build discovers manifest.toml files at any depth regardless — and a bundled image’s tarball travels with its tool directory (each image records that directory in toolbox.json, so import finds the tarball wherever it was placed).

Image manifest (manifest.toml)

name = "clamav"
type = "image"
config_from = "clamav.json"       # local file or a URL
build_path = "./"
image_name = "tools/github.com/cisco-talos/clamav"
version = "latest"
# build = true                    # set false to reference an already-published image
# exported_image_path = "..."     # a build=false image's real registry url (set by export)
# network policy definitions this image references (local files or URLs)
# network_policies_from = ["restricted.policy.json"]

# per-tool base-image config (merged over the config.toml default; per-tool fields win)
# [base_image]
# image = "alpine:3.19"
# image_arg = "IMAGE"
# token = "BASE_REGISTRY_TOKEN"   # CI/CD variable NAME (pass-through; unused by build-images)
# user = "BASE_REGISTRY_USER"     # CI/CD variable NAME (pass-through)
# allow_override = true
Reusing another image’s container (image_from)

Thorium toolbox images can run the same container image but with different runtime config (args/env/entrypoint). Instead of giving each its own container, an image can declare image_from to reuse another toolbox image’s container image by name and version:

name = "clamav-verbose"
type = "image"
config_from = "clamav-verbose.json"   # this image's own runtime config (args, etc.)

[image_from]
name = "clamav"                       # reuse this image's container image
version = "latest"                    # defaults to "latest" if omitted

image_from is resolved at build time: the referenced image’s container url is written into this image’s config, so on import both are created as distinct Thorium images sharing one container. Notes:

  • It forces build = false — the image is never built (build_path, [base_image], and exported_image_path have no effect when image_from is set).
  • The reused url takes precedence over any exported_image_path or image url in the config.
  • This image’s own config must be local (a config_from that is a URL can’t have the reused url embedded at build time).
  • The referenced image must be a real image the toolbox knows about (with a container image of its own). An image_from that points at a missing image, forms a cycle, or targets an image that has no container image fails toolbox buildtoolbox.json is not written, so the broken image and any pipelines that reference it never reach an instance.

Pipeline manifest (manifest.toml)

name = "antivirus"
type = "pipeline"
description = "Antivirus scanners (licenses may be required)"
version = "latest"
config_from = "antivirus.json"

[images.clamav]
version = "latest"

How container image tags are built

When toolbox build assembles toolbox.json, each image’s container url (its image_tags and the image field embedded in its config) comes from one of three sources:

  1. image_from (highest priority) — the url is taken from the referenced image (see above); no tag is derived for this image.

  2. Pinned url — for an image with build = false that has no image_from, the url is used verbatim from its manifest’s exported_image_path (recorded by toolbox export), or failing that the image url already in its config. No derivation happens.

  3. Derived url — for a buildable image (build = true), and as the fallback for an unpinned build = false image, the tag is derived per registry as:

    <registry>/[<image_path_prefix>/]<leaf>:<version>[<tag_suffix>]
    
    • <registry> — from config.toml: the primary registry plus any registries extras (blanks and duplicates dropped). One tag is produced per registry; the first is written into the config’s image field, the rest are mirror push targets. An empty registry list means no tag is derived (the config’s own image url stands instead).
    • <image_path_prefix> — optional path from config.toml, inserted between the registry and the leaf when set.
    • <leaf> — the tool name. Pass toolbox build --use-image-path to use the manifest’s image_name (a repo-style path) as the leaf instead; with that flag, an image with no image_name yields no derived tag.
    • <version> — the manifest’s version (defaults to latest; an empty version is rejected).
    • <tag_suffix> — optional, appended to the version via toolbox build --tag-suffix (e.g. -mybranch) so feature-branch builds get distinct tags.

    For example registry = "ghcr.io/o/r" + tool name = "strings" + version = "latest" derives ghcr.io/o/r/strings:latest; with --use-image-path and image_name = "gnu.org/binutils/strings" it derives ghcr.io/o/r/gnu.org/binutils/strings:latest.

A K8s image that ends up with no container tag from any of these sources is unschedulable, so toolbox build fails and lists every such image. (Non-K8s scalers — BareMetal, Windows, Kvm, External — run without a container image and are exempt.)

Creating a toolbox from scratch


Use init to scaffold files (it never overwrites existing files unless you pass --overwrite), then build to roll them into a toolbox.json:

# scaffold a toolbox with one image and a pipeline that runs it
thorctl toolbox init toolbox -i ./images/clamav -p ./pipelines/antivirus:clamav

# generate toolbox.json (reads ./config.toml by default)
thorctl toolbox build

A toolbox is anchored on its config.toml: build crawls and writes relative to the config.toml’s directory. Both --path (the crawl root) and --output (the toolbox.json location) default to that directory, so build from a toolbox root “just works” and thorctl toolbox build -c sub/config.toml builds the toolbox in sub/ (writing sub/toolbox.json). The two are independent overrides — -o dist/toolbox.json redirects only the artifact, --path ./src redirects only the crawl — and build prints the resolved crawl root and output path when it runs. (Note: build_path is recorded relative to --output, so a --output outside the crawled tree produces ../-style build contexts — fine for import, but a build-images-portable toolbox wants the default self-contained layout.)

init runs interactively by default, prompting for the key fields and optionally opening your $EDITOR to review the full config. Pass -n/--non-interactive to accept defaults. You can also scaffold a single image or pipeline with thorctl toolbox init image ./images/clamav or thorctl toolbox init pipeline ./pipelines/antivirus -i clamav. The positional path is the destination; the stub is folded into a toolbox by build’s recursive walk wherever it lands.

init image/init pipeline take an optional -c <config.toml> to validate against an existing toolbox (a resolution source, not a placement directive — it never moves files). init pipeline guarantees a dangling-free manifest: every --order image must be declared in --images, and with -c every referenced image must already exist in the toolbox (else it errors — init never creates images) and is version-pinned from the toolbox instead of defaulting to latest. init image -c errors if an image of the same name+version already exists in the toolbox (pass --overwrite to replace). init toolbox remains the command that creates a new toolbox (config.toml + tools).

Building images locally


Forks without CI can build a toolbox’s container images directly. build-images walks toolbox.json for image entries with build enabled, builds each entry’s docker context with its first registry tag, aliases the remaining tags onto the same build, and pushes everything with --push (requires docker and registry credentials):

thorctl toolbox build-images ./toolbox.json --push          # everything buildable
thorctl toolbox build-images ./toolbox.json -i clamav       # just one image

Entries with build = false in their manifest or no image_tags are skipped with a note. Once the images are pushed, toolbox import works as usual. If an image fails to build or push, build-images reports it, keeps going with the rest, and exits non-zero at the end listing every image that failed — one broken image doesn’t block the others.

The container runtime is selected from --container-runtime if given, else the container_runtime setting in your thorctl config, else an autodetected default (docker/podman).

Two flags control how the underlying docker/podman build runs (both apply to every image built in the run):

  • --no-cache — build without the layer cache (passes --no-cache to docker/podman), e.g. to pick up a changed remote dependency a cached layer would otherwise mask.
  • --pull — force a fresh pull of referenced images before building (passes --pull to docker/podman). Like docker/podman --pull, this refreshes every image the build references, including the FROM base.
thorctl toolbox build-images ./toolbox.json --push --no-cache --pull

Base images ([base_image])

Base-image configuration lives in one [base_image] table, available both toolbox-wide (in config.toml) and per-tool (in manifest.toml). It can override a tool’s Dockerfile base image and record the credentials an external CI/CD pipeline uses to pull a private base.

# config.toml (global default) or manifest.toml (per-tool)
[base_image]
image = "alpine:3.19"          # override the Dockerfile FROM base (optional)
image_arg = "IMAGE"            # the build-arg the Dockerfile reads; default "IMAGE"
token = "BASE_REGISTRY_TOKEN"  # NAME of a CI/CD variable (pass-through; see below)
user = "BASE_REGISTRY_USER"    # NAME of a CI/CD variable (pass-through)
allow_override = true          # gates the image/image_arg substitution; default true

Resolution. toolbox build merges the per-tool table over the global one field by field (per-tool wins) and writes the result onto each image entry in toolbox.json. The image/image_arg substitution is applied as --build-arg <image_arg>=<image> and is overridden at build-images time by the --base-image ARG=IMAGE flag, so the effective precedence is CLI > per-tool > global. Overriding the base requires a parameterized Dockerfile (a build-arg can’t replace a hardcoded FROM):

ARG IMAGE
FROM ${IMAGE}

allow_override gates the image/image_arg substitution only (default true); set it to false to keep a tool’s own base. token/user are independent of it.

token/user are opaque pass-through strings — they are intended to hold the names of CI/CD pipeline variables (do not put real secrets here; toolbox.json is usually committed). thorctl never resolves or manipulates them, and build-images does not use them (it performs no base-registry login). If a base image needs auth for build-images, run docker/podman login yourself first.

Defaults / absence. Omitting [base_image] entirely leaves the Dockerfile’s own FROM untouched (no implicit base). With an image set, image_arg defaults to IMAGE; allow_override defaults to true; a per-tool field that is omitted inherits the global value. [base_image] is ignored on image_from images (which are never built).

Exporting from a running instance


export fetches image and pipeline configs from Thorium and writes a complete toolbox directory (manifests, configs, and a toolbox.json). The real registry URL of each image is preserved exactly as it is in Thorium.

# export an entire group into a new toolbox
thorctl toolbox export -g static -o ./my-toolbox

# export specific pipelines (their images are pulled in automatically) and standalone images
thorctl toolbox export -p static/antivirus -i static/exiftool -o ./my-toolbox

# append into an existing toolbox by pointing at its config.toml — output defaults to its directory
thorctl toolbox export -c ./my-toolbox/config.toml -p static/newpipeline

The --output/-o directory defaults to the --config directory when --config is given (so pointing at a toolbox’s config.toml exports into that toolbox), otherwise ./toolbox for a brand-new toolbox. An explicit -o always wins; the defaulted directory is announced in the output.

Useful flags:

  • --group-override <group> — rewrite the group of every exported config.
  • --review — open each config in an editor to review/tweak it before writing (off by default, so configs are written as-is).
  • --with-images — also bundle the container image files (see below).
  • --strip-registry — publish a registry-agnostic toolbox: each image config’s image url is written empty and the manifest’s exported_image_path is omitted, so a rebuild derives each image path from the toolbox’s own config.toml registry/image_path_prefix rather than your pinned url. Images only (pipelines carry no url). Conflicts with --with-images (a bundled import needs the url to tag and push the saved tarball).
  • --overwrite — overwrite existing per-tool files (manifest/JSON/description/policies). It does not touch config.toml.
  • --overwrite-config — replace an existing config.toml (otherwise it is preserved).

Appending into an existing toolbox is safe by default. config.toml is the toolbox’s identity: exporting into a directory that already has one preserves it and reuses its settings (announced in the output) rather than clobbering them, so you don’t need --config. So thorctl toolbox export -p static/newpipeline -o ./my-toolbox adds the pipeline (and its images) into ./my-toolbox, and the rebuild folds everything in. Pass --overwrite-config only when you actually want to change the toolbox’s settings; if a flag like --with-images or --registry contradicts the preserved config, export warns that the flag is ignored. (--config seeds settings from another toolbox and anchors the output to that config’s directory unless -o is set — so -c X/config.toml appends into X, while seeding into a different directory needs an explicit -o. A --config that points at a missing file isn’t fatal: export warns (“config.toml not found …; creating a new toolbox there instead of appending”) and creates a new toolbox, so a mistyped or not-yet-created target shows up as a notice rather than a read error.)

Refreshing a whole toolbox. Running export against an existing toolbox with no --group/--pipelines/--images and --overwrite re-pulls every tool the toolbox already contains from Thorium and updates them in place — a one-shot “sync this toolbox to the instance”:

thorctl toolbox export -c ./my-toolbox/config.toml --overwrite   # refresh every tool in ./my-toolbox

It enumerates the tool list from the toolbox itself (its toolbox.json, or an on-disk crawl if that’s gone), updates only tools already present (never adds new ones, never prunes), and warns + leaves untouched any tool that no longer exists in Thorium. Without --overwrite a no-selection run errors with a hint (it inherently rewrites the existing configs).

Group as source vs destination, and renames. -g is the source group (what to read from Thorium); --group-override is the destination group (what to write under in the toolbox), defaulting to the source group. build identifies an image/pipeline by name + version, ignoring group, so a tool written under a group that doesn’t match where it already lives in the toolbox would be a build-breaking duplicate. Export handles this automatically:

# toolbox stores these under a different group than Thorium's "static2"; re-group + update in place:
thorctl toolbox export -g static2 -c ./my-toolbox/config.toml --overwrite

With --overwrite, each existing tool is matched by its name+version and re-grouped in place to the destination group (the -g group here, or --group-override if you set one) — its files are rewritten at their existing directory, reported “Re-grouping …”. Without --overwrite, a tool whose name+version already exists under a different group is skipped with a warning (suggesting --overwrite) rather than written as a duplicate. (A same name at a different version under another group is warned but allowed, since build keeps distinct versions.) Refresh-all (no selection) still fetches by the toolbox’s own group, so use -g <thorium-group> --overwrite to bridge a rename.

When appending into an existing toolbox, export reconciles against it. The existing toolbox is read from its committed toolbox.json; if that file is missing or unparsable, export falls back to crawling the on-disk tool manifests, so a deleted or stale toolbox.json doesn’t make the append re-write what’s already there. Each image and pipeline records its on-disk directory in toolbox.json, so a re-export updates a tool where it already lives instead of writing a duplicate at the default layout. Per matched tool (group/name):

  • unchanged (byte-identical config) → a no-op, and the container re-bundle is skipped when the tarball is already saved (reported “Unchanged”);
  • differs, with --overwriteupdated in place. For an image this refreshes only the Thorium config (<name>.json), description, and policy defs and keeps its manifest.toml (so hand-set build/build_path/[base_image]/image_from survive); a pipeline manifest is regenerated;
  • differs, without --overwriteskipped with a warning suggesting --overwrite, and the rest of the export continues;
  • an explicit =dir pointing somewhere other than where the tool already lives → skipped (a second copy would fail build); omit =dir to update in place.

A fresh export (no existing toolbox) does none of this. Note: an image update preserves the manifest, so a change to the image’s network-policy set in Thorium won’t re-link network_policies_from — use =dir (a full write) or remove + re-export to pick that up.

Placing a single resource (=dir). A -i/-p entry may carry a group/name=dir suffix to write that resource’s files into a chosen directory instead of the configured/default layout — for example to fold a Thorium image config into a directory that already holds its Dockerfile:

thorctl toolbox export -i static/clamav=tools/clamav -o ./my-toolbox
# writes tools/clamav/{manifest.toml, clamav.json, description.md, *.policy.json}

dir may be relative or absolute. A relative dir is interpreted against the toolbox root; an absolute one is taken literally; either is normalized and re-expressed relative to the root — so an explicit path into a toolbox elsewhere works, e.g. with -c ../../other/tb/config.toml you can write -i static/clamav=../../other/tb/tools/clamav (it lands at tools/clamav inside that toolbox). The only rule is that it must resolve inside the toolbox root; a dir that lands outside is rejected (build only includes files under the toolbox).

=dir is placement only — it never changes which pipeline or images are selected (a pipeline’s membership and order come from Thorium). Placement precedence per resource is: explicit =dirthe directory the tool already occupies in the toolbox (so re-exports update in place) → the configured/default layout. Whole-group exports and auto-pulled dependency images aren’t named, so they reuse their existing directory or the configured/default layout unless an image is also named with its own =dir.

If the =dir destination already contains a Dockerfile, export reads it as a build context you are folding the config into and writes that image’s manifest.toml with build = true (and no exported_image_path), so a later build builds the image from that context rather than referencing the original registry url:

# tools/clamav/ already holds a Dockerfile
thorctl toolbox export -i static/clamav=tools/clamav -o ./my-toolbox
# clamav.json + manifest.toml (build = true) land beside the existing Dockerfile

This auto-detect applies only to the explicit-=dir case (a default-layout dir is created fresh and never holds a Dockerfile); the default build = false reference-only manifest is used everywhere else. The detection is announced with a log line.

Importing into an instance


import reads a toolbox.json from a local path or a URL and creates the images, pipelines, and any missing groups in the target Thorium instance.

thorctl toolbox import ./my-toolbox/toolbox.json
thorctl toolbox import https://raw.githubusercontent.com/cisagov/thorium/refs/heads/main/tools/toolbox.json

If an image or pipeline already exists, the conflict is handled according to one of three modes (new resources are created in all of them):

ModeFlagBehavior for existing resources
Interactive (default)Prompts per changed resource: Edit (resolve a git-style diff in your $EDITOR), Skip, Apply (accept incoming), or Quit
Overwrite--overwriteApplies every incoming change without prompting
Skip conflicts--skip-conflictsNever touches existing resources; logs a warning per skipped resource listing the fields that differ

--overwrite and --skip-conflicts are mutually exclusive. The upfront confirmation screen (shown before an interactive merge or before creating new groups/network policies) only appears in the default interactive mode on a real terminal; passing --overwrite or --skip-conflicts, or running without a TTY (CI, pipes), skips it. --skip-conflicts is therefore the fully unattended form for CI and agents that must never overwrite local changes. --group-override <group> forces everything into a single group.

Diffing a toolbox against an instance


diff shows what an import of a toolbox would change, rendered like git diff. It accepts a toolbox.json (path or URL) or a toolbox repo directory — directories are built in-memory from their manifests, so the diff always reflects the configs as they sit on disk:

# compare a repo checkout against the instance it was imported into
thorctl toolbox diff ./my-toolbox --group-override sandbox

# gate CI on drift (exit 1 when anything differs, like git diff --exit-code)
thorctl toolbox diff ./my-toolbox/toolbox.json --exit-code

diff runs the toolbox through the same pre-processing as import before comparing, so what it shows is what an import would actually apply: structural validation, group capture, any --group-override, group-coherence validation, and collision resolution (the automatic renames import performs when two toolbox resources would collide). Resources dropped by validation are warned about and excluded from the diff, exactly as import would drop them.

Changed resources show unified hunks over their normalized configs (server-only fields like creators and bans never appear as drift). A resource present on only one side is reported on a single line rather than a full-body add/delete: resources only in the toolbox as only in toolbox/<group>/<name> (…) — not in <host[:port]>, and resources in the toolbox’s groups that the toolbox doesn’t name as only in <host[:port]>/<group>/<name> (…) — not in this toolbox. The instance is named by its host (and explicit port, if any), and changed resources use that same <host[:port]>/<group>/<name> header on their instance side. Bundled network policies are included the same way — toolbox-only ones as a single line, mismatched ones as notes (imports never update policies). A trailing summary counts changed / only in toolbox / only in <host[:port]> / unchanged.

--exit-code reflects only what an import would change. It exits 1 when there is an actionable difference — a changed resource, a toolbox-only resource that import would create, or a network policy that differs — and 0 otherwise. Resources that exist only on the instance are informational (import never deletes), so they do not trip a non-zero exit on their own.

Network policies


Images can reference Thorium network policies by name, and a toolbox carries the policy definitions so those references stay valid on import:

  • toolbox export writes each referenced policy to <image dir>/<name>.policy.json and records it in the image’s manifest.toml under network_policies_from (the same policy referenced by several images is written to each — identical duplicates dedupe on import). Each exported policy’s groups are scoped to the group of the image that references it (and rewritten by --group-override), so a bundled policy never carries groups the toolbox isn’t exporting.
  • toolbox build bundles those files into toolbox.json; URL entries are fetched at import time like config_from. A toolbox carrying two different definitions under one policy name fails validation before anything is applied.
  • toolbox import creates missing policies (with --group-override applied to their groups) before any images. A policy that already exists in the target is never updated — if it doesn’t 100% match the bundled definition, a warning names the differing fields and the target’s policy is left alone. Created policies are part of the rollback journal.

Removing a toolbox


remove deletes the pipelines and images named by a toolbox manifest from the target instance — pipelines first, then images. Resources that don’t exist are reported and skipped; groups are never deleted. Pass --group-override if the toolbox was imported with one:

thorctl toolbox remove ./my-toolbox/toolbox.json --group-override sandbox

Single resources can be deleted directly with thorctl images delete <name...> -g <group> and thorctl pipelines delete <name...> -g <group> (both confirm first; -y to skip).

Offline transfer with bundled images


To move a whole toolbox — including the container images — to an air-gapped Thorium instance, bundle the image files on a connected host and push them into the offline registry on import. You do not need to know the offline registry when exporting.

1. On a connected host, export with --with-images. Each image is docker pulled and saved to images/<name>/<name>.tar.gz, and the toolbox is marked as bundled:

thorctl toolbox export -g static --with-images -o ./offline-toolbox

Bundling is best-effort: an image that can’t be pulled or saved is warned about and skipped, and the rest of the toolbox is still written (the skipped image simply ships no tarball and keeps its original registry URL). Because that leaves the bundle incomplete, export then prints an INCOMPLETE summary and exits non-zero — so a scripted export → import handoff doesn’t treat a toolbox with missing tarballs (or an omitted, dangling network policy) as a complete one. A clean export exits 0.

2. Move the ./offline-toolbox directory to the offline environment (it is fully self-contained).

3. On the offline host, import and provide the target registry base path with --image-path-prefix. Each bundled image is loaded, retagged to <image-path-prefix>/<group>/<name>:<tag>, pushed, and the imported image’s config is rewritten to point at the offline registry:

thorctl toolbox import ./offline-toolbox/toolbox.json \
    --image-path-prefix registry.offline.local:5000/team
# clamav (group "static") -> registry.offline.local:5000/team/static/clamav:latest

If you omit --image-path-prefix for a bundled toolbox, the prefix recorded in the manifest is used; otherwise you are prompted for one — and if the session can’t prompt (--overwrite, --skip-conflicts, or no TTY), the import errors instead. The on-disk bundle always keeps the original image URLs, so it remains re-importable to additional environments. Bundled imports require docker and must be run from a local path (not a URL).

Pushing the bundled images is best-effort: if an image can’t be loaded, retagged, or pushed, the import warns and continues. The image’s config is still created in Thorium pointing at the offline registry, so once you push that image manually (or re-run the import) the resource works — a single unpushable image doesn’t block the rest of the import. A run that finished with any failed pushes exits non-zero and lists them.

Note: --with-images/--image-path-prefix need a container runtime (docker/podman) on the host to actually move images. There is no up-front capability check: if no runtime is available, every image is simply skipped (warned) and the export exits non-zero as incomplete, rather than failing fast. A toolbox exported without --with-images keeps each image’s original registry URL, so importing it simply points Thorium at the existing registry — no image transport happens.

Note: KVM-scaled images round-trip their kvm settings, but the underlying VM disk image is not part of any export — Thorium has no transport for VM disks. An imported KVM image is creatable but will not run until its disk is moved to the target environment out of band.

Toolbox vs images/pipelines export


thorctl images export|import and thorctl pipelines export|import move a single image or pipeline (with its container files) between instances and are ideal for one-off transfers and fine-grained registry control (--registry, --registry-override, --migrate-registry). Reach for a toolbox when you want to package many images and pipelines together as one portable, versioned unit — to publish a curated set of tools, share them via a URL, or move an entire collection (optionally with images) to another instance.

The plain import commands use the same three conflict modes as toolbox import (interactive merge by default, --overwrite, --skip-conflicts), show the same confirmation summary (shown only on an interactive terminal; --skip-conflicts or a non-TTY session bypasses it), and auto-create the target group if it doesn’t exist. pipelines import imports the images referenced by each pipeline’s order first, then the pipeline itself, all under one rollback journal: if the import stops early (an error, or Quit in the merge editor), you are offered a rollback of everything applied so far — or pass --rollback-on-failure for non-interactive runs. Rollback only undoes Thorium state; registry pushes are left in place.