Run CLI tools on Windows through Docker-backed executable shims — without installing the runtimes on the host.
ContainerBin makes commands such as python, pip, pipx, node, npm, npx,
uv, uvx, go, cargo, rustc, dotnet, ruby, gem, bundle, jq, yq, terraform and ffmpeg look like ordinary
Windows executables while their real implementations run inside disposable
Linux containers on Docker Desktop. Your Windows installation stays clean: no
Python, pipx, Node, Go, Rust, uv, .NET SDK or Ruby on the host — just one small Go binary,
cb.exe.
PS D:\Work\demo> pip install requests
PS D:\Work\demo> python -c "import requests; print(requests.__version__)"
2.32.3
PS D:\Work\demo> ffmpeg -i "D:\Video\input.mkv" "D:\TEMP\output.mkv"
PS D:\Work\demo> terraform -chdir=.\tf validate
Success! The configuration is valid.
⚠️ ContainerBin is not a security sandbox. It is a convenience layer that runs the Docker images you configured, with the host paths your commands reference bind-mounted in and selected environment variables passed through. The registry and lockfile are part of the trust boundary. Read docs/security-model.md before pointing it at anything sensitive.
ContainerBin is developed using an AI-assisted, agentic workflow. The
maintainer owns requirements, design decisions, acceptance testing, and
releases; implementation and review make extensive use of coding agents. AI
involvement is intentionally preserved in commit and PR history rather than
hidden — see the Co-Authored-By trailers throughout the git log.
PowerShell / cmd / any Windows process
│ runs python.exe, jq.exe, terraform.exe, ...
▼
NAME.exe (hardlink to cb.exe in one PATH directory)
▼
cb.exe dispatches on argv[0]
│ registry profile (container-bin.toml)
│ argv normalization + conservative Windows→container path mapping
│ image lock resolution (container-bin.lock)
▼
docker run --rm ... repository@sha256:digest | sha256:local-image-id
▼
real Linux CLI/runtime in an ephemeral container
- Process compatibility: stdin/stdout/stderr, exit codes, piping,
redirection, working-directory semantics and interactive vs. captured
execution are preserved. Third-party software that probes for a working
python.exe(validated with Claude CLI) accepts the shim as a real interpreter. - Persistent state where it matters:
pip installandnpm installresults survive across invocations in Docker named volumes, even though every container is disposable. - Reproducible images:
cb lockpins registry images to an immutablerepository@sha256:digestand locally built images to their exact Dockersha256image ID. Updates are explicit (cb update), never a side effect of a mutable tag moving.
| Environment | Status |
|---|---|
| Windows 10/11 x64 + Docker Desktop (Linux containers) + PowerShell | Supported — this is the validated configuration |
| cmd.exe invocation of shims | Works for the common cases; less battle-tested than PowerShell |
| WSL2 | Not yet supported. The selected native-Linux frontend has an explicit fail-closed runtime boundary plus fixed-layout and install/config lifecycle commands; runtime/Docker wiring and real Docker Desktop WSL qualification remain. See docs/wsl.md |
| Windows 11 ARM64 | CI/release-artifact/update-path qualified only, not supported yet. Native tests/build/dispatch run on GitHub-hosted ARM64 hardware, the release workflow produces a reproducible ARM64 archive, and self-update selects and verifies that archive by GOARCH; real Docker Desktop ARM64 E2E qualification remains |
| Linux / macOS hosts | Not supported. The program is Go and cross-compiles, but shim installation, path mapping and doctor checks are Windows-specific |
| Windows containers | Not supported; images are Linux images |
- Windows 10 or 11 (x64)
- Docker Desktop running in Linux containers mode
- A directory on
PATHwhere the shims will live
-
Download
cb.exefrom Releases (or build it yourself — see CONTRIBUTING.md) and place it in a dedicated directory, e.g.D:\Tools\container-bin\. -
Put that directory near the front of
PATH(System Properties → Environment Variables, orsettings→ "Edit environment variables"). ContainerBin deliberately never editsPATHfor you. -
Disable the Windows App Execution Aliases for Python if present (Settings → Apps → Advanced app settings → App execution aliases → turn off
python.exe/python3.exe). Otherwise the Microsoft Store stub can shadow the ContainerBin shim depending on PATH order.cb doctorwarns about this. -
Run:
cb setupThis writes the default registry (container-bin.toml), creates one
NAME.exe hardlink per configured tool next to cb.exe, and runs
cb doctor to verify Docker, PATH, shims, registry and lock state.
- Pin your images:
cb lock| Shim | Image | Provider |
|---|---|---|
python, python3 |
python:3.13-slim |
python (project /venv in a named volume) |
pip, pip3 |
python:3.13-slim |
python |
node, npm, npx (default aliases) |
selected Node family | stateful |
node24, npm24, npx24 |
node:24-slim |
stateful (node24 state group) |
node22, npm22, npx22 |
node:22-slim |
stateful (node22 state group) |
go, gofmt |
golang:1.24 |
stateful (go124 state group) |
rustc |
rust:1.98.1-slim-bookworm |
stateless |
cargo |
rust:1.98.1-slim-bookworm |
stateful (rust198 state group) |
uv, uvx |
ghcr.io/astral-sh/uv:0.12-python3.13-trixie-slim |
stateful (uv012-py313 state group) |
pipx |
ghcr.io/astral-sh/uv:0.12-python3.13-trixie-slim |
stateful (pipx117-py313 state group; pinned pipx==1.17.4) |
dotnet |
mcr.microsoft.com/dotnet/sdk:10.0 |
stateful (dotnet10 state group) |
ruby, gem, bundle |
ruby:4.0-trixie |
stateful (ruby40 state group) |
jq |
ghcr.io/jqlang/jq:latest |
stateless |
yq |
mikefarah/yq:latest |
stateless |
terraform |
hashicorp/terraform:latest |
stateless (-chdir path semantics) |
ffmpeg |
lscr.io/linuxserver/ffmpeg:latest |
stateless |
container-bin.toml lives next to cb.exe and describes every tool
declaratively:
Bootstrap commands do not read or validate that file: cb version (also
--version/-V), cb help (also --help/-h and bare cb) and cb config
remain available when the registry is missing, corrupt, or newer than the
installed binary. This keeps version-skew diagnosis usable before repair.
[tools.terraform]
image = "hashicorp/terraform:latest"
provider = "stateless"
path_equals = ["-chdir"] # -chdir=HOST_PATH is rewritten safely
env_prefixes = ["TF_", "AWS_", "ARM_"] # only these host vars enter the containerSemantics include command, args_prefix, path_next, path_equals,
path_last, path_last_if_any, env_names, env_prefixes, env_set,
project_markers, state_group, project_volumes, shared_volumes,
project_root_mode, host_mounts, cwd_mode, and the explicit
default_family / default_version / default_alias relationship. Unknown
keys fail validation instead of being silently ignored, and a
schema_version newer than the binary supports fails closed.
Each project_markers entry must be one non-empty path element: separators,
. / .., invalid UTF-8, and control characters are rejected at load time.
Edit the file, then run cb install to reconcile shims.
Tool names use lowercase letters, digits, -, and _. Names that collide
with ContainerBin or Windows devices are reserved. This includes the private
cb-update-helper dispatch name. Release binaries named cb-v followed
immediately by a digit (for example, cb-v1.2.3.exe) also remain reserved for
the management CLI; ordinary names that merely start with cb-v but have no
digit there, such as cb-vault, are valid tool shims and dispatch to their
registered profile.
For an image whose entrypoint is already the desired command, cb add appends
a minimal stateless profile and reconciles the shim without pulling or running
the image:
cb add jq-corp --image registry.corp.example/devtools/jq:1.8.1
# For an intentionally local image, declare that identity explicitly:
cb add jq-local --image jq-local:dev --localIf a lockfile exists, it becomes intentionally incomplete until you run
cb update jq-corp or cb lock; execution fails closed in the meantime. A
profile added with --local must be locked explicitly with
cb update --local jq-local or cb lock --local jq-local; the flag never
infers local identity from Docker metadata.
State, environment allowlists, path rules, command overrides, and mounts still
require an explicit reviewed registry edit followed by cb install.
The existing role field is also used as an ownership boundary:
cb expose writes role = "exposed", and cb unexpose only removes profiles
carrying that marker whose command and tool name agree and whose command stays
beneath a declared shared-volume mount. Known package-manager stores retain
their stricter store-directory and companion-volume checks.
Profiles generated by older versions have no marker and deliberately fail
closed; recreate one with cb uninstall TOOL followed by
cb expose SOURCE TOOL to migrate it.
A project may add project-local tools in .container-bin.toml at its canonical
project root. Merely cloning or entering a repository never activates that
file. ContainerBin parses it independently, rejects any tool/default collision
with the global registry, and requires an external trust record bound to both
the canonical root and the SHA-256 digest of the exact overlay bytes. Even a
comment-only edit deliberately invalidates trust rather than guessing whether
the change was security-relevant.
Review and approve an overlay explicitly:
cb inspect --project # deterministic image, environment, state, shim and digest review
cb trust --check # the same read-only review; never writes or installs a shim
cb trust # interactive: type "trust" to approve the exact root + digest
cb trust --yes # explicit non-interactive pre-provisioning after separate review
cb untrust # revoke the current root; leftover shims become inertThe mutating trust commands re-read the overlay after approval and again after shim installation, before recording trust. If its location or bytes change during that window, the command fails and any installed shims remain inert.
Trust records live outside the repository in the OS user configuration
directory (%APPDATA%\ContainerBin\project-trust.toml on Windows). The file is
strictly parsed, atomically replaced, and never read from the project. Moving a
project or changing its overlay requires review and trust again. A discovered
overlay whose project-root reparse chain cannot be resolved is rejected because
its canonical trust identity would be ambiguous. Until trust succeeds,
non-bootstrap commands fail closed; cb version, cb help, cb config,
cb self-update --check, cb doctor, cb inspect --project, cb trust, and
cb untrust remain available for diagnosis and recovery.
The initial overlay model is intentionally add-only. It permits new concrete
tool profiles, exact env_names, literal env_set values and project-scoped
project_volumes. It rejects [defaults.*], default-alias metadata,
host_mounts, env_prefixes, and cross-project shared_volumes. Overlay
tools also cannot use the legacy python provider because it implicitly mounts
the shared cross-project pip cache; use an explicit stateful profile with only
project-scoped volumes instead. Overlay images remain subject to the
administrator machine policy and normal image-lock rules; project trust cannot
weaken either one. An overlay tool's workspace bind mount and project-volume
identity are pinned to the trusted overlay root;
tool-specific markers cannot widen the mount to an ancestor repository or make
sibling overlays share state. Because project overlays share the global image
lockfile, cb lock inside a trusted overlay refreshes the current effective
images while preserving and revalidating entries belonging to other overlays;
plain global cb lock remains a complete refresh that drops stale entries.
cb trust installs only the
reviewed tool-name shims. cb untrust leaves those files in place because an
unrelated trusted project may use the same dispatch name; without a matching
trusted overlay they cannot resolve a tool and fail closed. Registry mutation
commands continue to target the global registry; cb expose rejects a
project-local source rather than silently promoting it into global config.
host_mounts lets a trusted profile declare fixed host paths that are always
bind-mounted into the container, regardless of whether they appear in the
command line. This is provider-agnostic: it works for stateless, python
and stateful profiles alike.
[tools.token-meter]
image = "example/token-meter:latest"
provider = "stateless"
host_mounts = ["%USERPROFILE%\\.claude:/root/.claude:ro", "%USERPROFILE%\\.codex:/root/.codex:ro"]Entries follow the SOURCE:/CONTAINER_PATH:MODE shape used by
project_volumes/shared_volumes, extended with a required third :MODE
segment. ro and rw are accepted; there is no default — every mount's
write access must be explicit in the registry line.
%USERPROFILE% is the only host variable container-bin expands; no other
%...% token is recognized or guessed. The source may also be a literal
Windows absolute path using a backslash after the drive letter
(e.g. D:\Video). The forward-slash drive form (D:/Video) embeds the :/
source/target delimiter and is rejected as ambiguous, so always use X:\....
Targets under /workspace, /cb, /venv and /root/.cache/pip are reserved
for container-bin's own project workspace and managed state mounts (the last
two are the python provider's fixed venv/pip-cache paths) and cannot be
claimed by host_mounts, on any provider.
project_volumes and shared_volumes may intentionally use paths under
/workspace and /cb, but cannot target /venv, /root/.cache/pip or their
descendants because those paths are owned by the python provider.
A
host_mountsentry grants the configured Docker image direct access to the named host files or directories. ContainerBin is not a security sandbox; arwmount exposes those host files to any code running in the container, exactly as adocker run --mountwould. Userowhen the tool only needs to read, review everyrwmount, and keep the registry and shim directory under your control.Exposed profiles created by
cb exposedo not inherit that source'shost_mounts; host access never propagates implicitly to an auto-generated shim. A profile that genuinely needs a host mount must declare it explicitly.
cwd_mode defaults to "project", which preserves the normal behavior of
walking up from the current working directory to find project markers and
bind-mounting the project into the container at /workspace. Set it to
"isolated" for tools that may be launched from an arbitrary working directory
that is not itself meaningful — for example, a background service or a GUI/MCP
launcher that inherits C:\Windows\System32 as its CWD and invokes a shim from
there. In isolated mode, ContainerBin skips project-root detection entirely,
sets --workdir /root, and does not bind-mount the host CWD at all. Any
argument that looks like a host path is still mapped, but because no path can be
"inside the project" it always uses the existing external-mount path and lands
under /cb/mounts/N. cwd_mode = "isolated" cannot be combined with:
project_volumes(a project-scoped volume conceptually requires a project identity);project_markers(dead configuration once project-root detection is skipped);provider = "python"(the python provider has its own project/compat venv split that isolated mode would otherwise silently collapse onto the shared global compat environment).
shared_volumes, host_mounts and environment allowlisting all work exactly as they do in
project mode. Exposed profiles created by cb expose do
not inherit that source's cwd_mode.
ContainerBin translates Windows paths in arguments to container paths and creates narrowly scoped bind mounts:
- absolute paths (
D:\Video\input.mkv), explicit relatives (.\x,..\x), and existing relatives (data\foo.json) are mapped; - paths inside the project root map into the workspace mount;
- paths outside it get their own narrow bind mounts (
ffmpeg -i "D:\Video\a.mkv" "D:\TEMP\b.mkv"produces two separate mounts — never a whole drive); - an argument whose final element is
...is never treated as a path, so tool package patterns such as./...reach the tool unchanged; - plain strings are never guessed to be paths. FFmpeg's
-iis not forced to be a path because valid inputs include URLs, pipes, devices and lavfi expressions. Tools that need forced path semantics declare them (path_equals = ["-chdir"]for Terraform).
Several Windows path forms are not supported, for different reasons: UNC
paths (\\server\share\...) and \\?\ long-path prefixes are not recognized
as paths and pass through unmapped; subst drives and mapped network drives
are mapped like any other drive letter, but Docker Desktop cannot share them;
and a junction inside the mounted project tree is not traversable from inside
the container, because bind mounts do not follow reparse points. See
docs/windows-paths.md for the full classification.
PowerShell natively splits terraform -chdir=.\tf validate into
-chdir=, .\tf, validate before the process ever sees it. ContainerBin
detects this for declared path_equals options and rejoins the argv —
validated against real Terraform.
Use cb trace TOOL ARGS... to see raw → normalized → mapped argv and the
mounts that would be created, without running anything.
AI agents and automation launchers must not assume that a working-directory change survives into a later shell call. See AI agent and automation invocation for the canonical execution contract and supported instruction files.
Python: for a detected project (markers: pyproject.toml,
requirements.txt, setup.py, setup.cfg, .git), ContainerBin provisions a
persistent per-project /venv named volume, plus a shared pip cache volume.
pip install requests persists; different projects get isolated environments.
Outside any project, a compatibility "global" environment serves programs that
just invoke python/pip from anywhere.
Node: node24/npm24/npx24 share the node24 state group. The
unversioned node/npm/npx shims are aliases to one complete versioned
family; Node 24 is selected initially. Switch all three together without
changing the stable versioned shims:
cb default
cb default set node 22
cb default set node 24cb inspect node and cb trace node ... show the concrete profile currently
selected. Alias resolution reuses that profile's image, state group and volumes;
it does not copy tool configuration. Destructive profile commands fail closed
on aliases: cb uninstall and cb unexpose require the concrete profile name
instead of deleting an alias shim while leaving its family metadata intact.
cb install and cb setup migrate stock
schema-v1 Node profiles automatically. A customized legacy node/npm/npx
profile is not assigned a version by guesswork: the upgrade stops and asks you
to give it an explicit versioned name and alias metadata. Project
dependencies live in a project-scoped named volume mounted at the project's
node_modules (the host may show an empty node_modules mountpoint directory
— contents live in the volume). There's a shared npm cache and a persistent
npm global prefix. Projects are mounted with their real basename
(D:\TEMP\node-demo-3 → /workspace/node-demo-3) because tools like
npm init derive metadata from it. Node 24 is the initial default runtime, but it is
not a guarantee that every npm package is ABI-compatible with it. For packages
whose native addons need a different Node ABI, node22/npm22/npx22 are a
second, independent Node-major runtime with their own node22 state group,
fully isolating project node_modules, the npm cache and the npm global prefix; upgrading an existing installation adds these profiles automatically, but they are not yet locked, so run cb lock or cb update --all before using them.
rustc and cargo use the official rust:1.98.1-slim-bookworm image and
share one image-lock entry. Cargo's registry and Git caches persist in shared
named volumes. cargo install writes to a separate persistent
/cb/cargo-global volume that is on the container PATH, so installed Cargo
subcommands remain usable through cargo. Run cb expose cargo <binary> to
create a standalone Windows shim for an installed executable. That directory
intentionally precedes the Rust toolchain directories:
installing a binary named cargo or rustc shadows the image's toolchain
command inside this profile, so audit what you install into the shared store.
Cargo build output deliberately stays in the host project tree rather than a
Docker volume. Cargo uses project_root_mode = "outermost", so a member
Cargo.toml or nested rust-toolchain does not hide a parent workspace from
the container mount. Run Cargo from that project (or a subdirectory), as usual.
Paths supplied to --target-dir and --manifest-path are mapped into the
container, including their --option=PATH forms. Path-valued host
variables such as CARGO_HOME, CARGO_TARGET_DIR and RUSTUP_HOME are not
forwarded because their Windows values are not meaningful inside Linux;
registry-specific Cargo variables and selected non-path settings are forwarded
explicitly.
Existing installations gain these profiles on cb install. Because the new
image is not present in an older lockfile, run cb update cargo (or regenerate
the lock with cb lock) before first use in locked mode.
uv and uvx use Astral's uv 0.12 image line with Python 3.13. Each
project gets a persistent environment mounted at /cb/uv-project-env; the uv
package cache, installed tool environments and tool executables are shared
across projects in separate managed volumes. The cache and environments are on
different filesystems, so the profile deliberately sets UV_LINK_MODE=copy
instead of letting uv attempt hardlinks and warn on every sync.
The project environment's /cb/uv-project-env/bin intentionally comes first
on the uv profile's PATH, followed by the shared tool-bin directory. This
lets project commands resolve normally, but it also means installing another
uv executable into that environment shadows the image's pinned uv; avoid
doing that unless the override is deliberate.
Automatic Python downloads are disabled. A project that requires a different
interpreter fails explicitly instead of silently downloading an untracked
runtime; use a separately configured uv image/profile for that interpreter.
Index, offline, TLS and proxy settings are forwarded from a narrow allowlist,
while path-valued Windows settings such as UV_PROJECT, UV_CACHE_DIR and
VIRTUAL_ENV are not.
uv tool install ruff persists its environment and executable, and uvx ruff
reuses the shared cache. Run cb expose uvx ruff to create a standalone
ruff.exe Windows shim backed by those same managed volumes. Prefer uvx as
the expose source because its environment contains only global tool state;
uv also carries project-only VIRTUAL_ENV and UV_PROJECT_ENVIRONMENT
settings whose project volume is deliberately not inherited by exposed tools.
This is the pipx-style global Python-tool workflow; arbitrary pip environments
and generic shared-volume paths are not exposed implicitly.
Equals-form global path options (--cache-dir=, --directory=, --project=,
and --config-file=) are translated to their container paths. The mapper stops
forced option handling at --, so same-named options intended for a command
launched by uv run or uvx are not mistaken for uv options; ordinary
path-shaped arguments still receive the generic mapping.
Existing installations gain uv and uvx on cb install. An older lockfile
does not include their image, so run cb update uv (or regenerate the lock with
cb lock) before first use in locked mode.
pipx is the classic-Python global application workflow. It is a separate
stateful profile: installed application environments live under
/cb/pipx/home, their executables live under /cb/pipx/bin, and the pinned
pipx launcher cache lives under /cb/pipx/launcher-cache. These directories
share one managed state volume so their relative links and cached launcher
remain portable together. That volume is not the Python provider's project
/venv or pip cache, and it is not shared with uv's tool store.
The locked uv/Python image launches the exact pipx==1.17.4 release with
uvx. First use therefore needs package-index access to populate the dedicated
launcher cache; later invocations can use that cache offline. The image lock
pins the launcher image, while package-index trust and the pipx package download
remain governed by the profile's narrowly forwarded uv/pip index, TLS and proxy
settings. Automatic Python downloads are disabled and pipx uses the Python 3.13
interpreter already in the locked image. A volume-local lock serializes pipx
commands that share this state. After every command, including failed commands,
a fail-closed wrapper validates every pipx-owned symlink, changes links that
resolve within the state volume to relative links, and copies only the known
image interpreter into its launcher cache, application venvs, and the pip
backend's shared-libraries venv. This
includes explicit --backend pip installs and pipx's forced pip backend for
the pip package while still rejecting any other external link target. This
keeps cb-pipx117-py313-state portable
through selected-volume backup/restore without relaxing archive link validation.
pipx run environments remain ephemeral because PIPX_VENV_CACHEDIR is not
placed on the managed volume; each pipx run may resolve and download its
application again and therefore requires the configured package index to be
reachable.
pipx install cowsay==6.1
cb expose pipx cowsay # explicit deterministic selection
cowsay "hello from pipx"
cb expose pipx # expose every other eligible app in the store
cb unexpose cowsayThe generated application profiles preserve the pipx image, state group,
volumes and environment policy. Exposure discovery reads only the managed bin
directory within the state volume through the selected locked profile; it does
not search a project venv, the host PATH, uv's store, or other container
directories. Each discovered executable must also resolve inside the managed
pipx state volume, so an external link left by a failed install is rejected.
Discovery takes a shared lock on the same volume-local lock used exclusively by
pipx commands, so it never scans a partially updated application store.
Plain pip remains for project dependencies, so scripts in /venv/bin are
deliberately not eligible for global exposure.
Existing installations gain pipx on cb install. Because it shares the same
image reference as uv, a lock that already contains that reference can resolve
it; otherwise run cb update pipx (or regenerate the lock with cb lock) before
first use in locked mode.
dotnet uses Microsoft's .NET 10 LTS SDK image. NuGet packages, user-level
NuGet configuration and global .NET tools persist in shared managed volumes;
/root/.dotnet/tools is on the container PATH. Telemetry and first-run setup
noise are disabled by the profile. NuGet package-source
credentials and selected runtime/network controls are forwarded explicitly,
while path-valued Windows settings such as DOTNET_ROOT, DOTNET_CLI_HOME and
NUGET_PACKAGES are not.
For dotnet watch on a Docker Desktop bind mount, set
DOTNET_USE_POLLING_FILE_WATCHER=1 on the host if filesystem notifications do
not cross the Windows/Linux boundary reliably; the profile forwards that
non-path opt-in.
Project bin and obj directories deliberately remain in the host project
tree instead of Docker volumes, so build output is visible to editors and
other Windows processes. The SDK runs on Linux: framework-dependent IL remains
portable, but native apphosts and self-contained publishes target Linux unless
you explicitly select a Windows runtime identifier such as -r win-x64.
dotnet tool install --global TOOL persists under /root/.dotnet; inspect it
later with dotnet tool list --global. Run cb expose dotnet <binary> to
create a standalone Windows shim backed by the same managed .NET home. The
global-tools directory intentionally comes first on the profile's PATH, so a
global tool named dotnet would shadow the SDK command inside that profile.
Existing installations gain dotnet on cb install. An older lockfile does
not include the SDK image, so run cb update dotnet (or regenerate the lock
with cb lock) before first use in locked mode.
ruby, gem and bundle use the full official Ruby 4.0 image rather than the
slim variant, because development workflows frequently need its compiler and
system headers for native extensions. They share a Ruby-ABI-specific ruby40
state group and one image-lock entry.
gem install rake writes to a persistent shared gem home that is on the
container PATH, so later Ruby invocations can load the gem or find its
executables (for example, ruby -S rake). Bundler dependencies live in a
per-project /cb/bundle volume, while its download cache is shared. Gemfile
and Gemfile.lock remain in the host project; installed Linux gems and native
extensions remain in Docker volumes rather than leaking onto Windows.
The managed gem bin directory intentionally precedes the image toolchain on
PATH. A gem that installs an executable named ruby, gem or bundle
therefore shadows that command inside these profiles, so audit executables
added to the shared gem home.
Only selected non-path Ruby/Bundler settings, repository credentials and proxy
variables cross into the container. Host values such as GEM_HOME, GEM_PATH,
BUNDLE_PATH and BUNDLE_GEMFILE are deliberately ignored because Windows
paths are meaningless in the Linux image. Run cb expose ruby <binary> to
create a standalone Windows shim for an executable installed by RubyGems. Use
the ruby source for exposure: it mounts the same gem home without forwarding
the gem/bundle profiles' RUBYGEMS_API_KEY, HTTP_PROXY_USER, or
HTTP_PROXY_PASS credentials to arbitrary installed gem code.
Existing installations gain all three profiles on cb install. An older
lockfile does not include their image, so run cb update ruby (or regenerate
the lock with cb lock) before first use in locked mode.
npm install -g cowsay
cb expose npm
cowsay "hello from a container"
go install golang.org/x/tools/cmd/stringer@v0.36.0
cb expose go stringer
stringer -help
cargo install just
cb expose cargo just
just --version
uv tool install ruff
cb expose uvx ruff
ruff --version
pipx install cowsay==6.1
cb expose pipx cowsay
cowsay "hello from pipx"
dotnet tool install --global dotnet-ef
cb expose dotnet dotnet-ef
dotnet-ef --version
gem install rake
cb expose ruby rake
rake --version
# For a custom stateful profile whose shared_volumes includes
# "tools:/opt/acme", expose one explicit executable without store guessing:
cb expose --shared-file acme tools /opt/acme/bin/acme-lint
acme-lint --versioncb expose takes a stateful source profile with one supported global binary
store: the npm prefix (npm, npm22, ...), Go's shared /go/bin (go),
Cargo's install root (cargo), uv's tool-bin directory (uv, uvx), pipx's
managed bin directory (pipx), .NET's global tool home (dotnet), or the
RubyGems home (ruby, gem, bundle). It
adds registry profiles that inherit the source image, state_group,
project-root markers and mode, shared volumes, and environment policy, then
creates Windows shims — cowsay.exe, stringer.exe, just.exe, ruff.exe,
dotnet-ef.exe, or rake.exe appears on PATH without Node, Go, Rust, Python,
.NET, or Ruby touching the host. To
expose a binary installed under the Node 22 runtime, use
cb expose npm22 <binary>; for go install output, use
cb expose go <binary>; for cargo install, use
cb expose cargo <binary>; for uv tool install, use
cb expose uvx <binary>; for a pipx application, use
cb expose pipx <binary>; for a global .NET tool, use
cb expose dotnet <binary>; for a Ruby gem executable, use
cb expose ruby <binary>.
Managed-store discovery uses the already-locked local source image with pulls
and networking disabled, a read-only container root, and read-only mounts for
the selected store and any required companion volume. It uses an explicit shell
entrypoint except for pipx, whose locked Python image runs the embedded
lock-aware scanner directly. The pipx wrapper creates its reserved lock before
the first state mutation; discovery reports no applications if that lock does
not exist yet, otherwise it scans under a shared lock. No discovery path mutates
package-manager state. Other source images must provide a POSIX-compatible
sh; distroless images without one cannot use automatic discovery.
For a custom stateful profile, cb expose --shared-file TOOL VOLUME FILE
selects one logical name from that profile's shared_volumes and one absolute
container file beneath the selected mount. The file's basename becomes the
Windows shim name. ContainerBin does not search other volumes or PATH, and it
rejects traversal, mount escapes, invalid/reserved or flag-shaped shim names,
directories, symlinks (including parent-directory escapes), and files without
the executable bit. Discovery requires the named
Docker volume and source image to exist already, mounts only that volume
read-only, disables networking, prevents image pulls, and runs with a read-only
container root. The source image must provide a POSIX-compatible sh;
distroless images without one cannot use automatic discovery. The generated
profile then inherits the same deliberately limited source fields described
below. A volume mounted at one of the supported package-manager store targets
must use the normal cb expose TOOL [BINARY ...] mode so its store-specific
companion-volume and ownership checks still apply.
That inheritance set is deliberately fixed. Generated profiles do not copy
the source's project_volumes, host_mounts, or cwd_mode; a custom source
using those fields must treat the generated profile as a separate access policy
and edit it explicitly before use. In particular, host access never propagates
implicitly to an auto-generated shim, as described above. Source-command
argument rules (args_prefix and path_*) and default-family metadata are not
copied either: an exposed binary has its own argv semantics and is always a
concrete profile, never a runtime alias.
With no binary arguments, every valid executable in that source store is considered. Explicit names are safer on a long-lived store. Invalid and reserved Windows shim names are ignored, and names that differ only by case fail closed because they cannot coexist on Windows. When explicit names are given, each name that is not present in the selected store is reported instead of being silently ignored alongside successful matches.
Generated profiles carry role = "exposed" as explicit provenance.
cb unexpose requires that marker plus a matching command/name and containment
beneath a declared shared-volume mount. Known package-manager stores also have
to satisfy their store-directory and companion-volume rules, so a hand-authored
profile is not deleted merely because its command resembles generated state.
Exposed profiles are keyed by binary name only, so a binary already exposed
from one runtime cannot also be exposed from the other under the same name —
cb unexpose it first if you need to switch which runtime backs it.
cb unexpose cowsay removes the shim and profile without deleting the
underlying npm state. Registry mutations are validated and written atomically;
a failed validation refuses the update.
cb lock # pull registry images and write the complete lockfile
cb lock --check # verify lock completeness and local availability
cb lock --local mytool # lock mytool's current image ID; repeat for more local tools
cb update jq # explicitly refresh one image
cb update --all # explicitly refresh everything
cb update --local mytool # switch/refresh one entry as a local image ID
cb update --registry mytool # switch/refresh one entry from its registryRuntime behavior is fail-closed:
- no lockfile → backwards-compatible UNLOCKED mode;
- lockfile present → exact
repository@sha256:digestor localsha256image-ID execution; - an image configured in the registry but missing from the lock → execution
fails and asks for
cb update TOOLorcb lock.
Lock schema 1 remains the default digest-only format for images outside an
image-trust rule. Schema 2 adds structured repository-signature evidence to an
entry: evidence version,
keyless/key mechanism, canonical repository, exact locked digest,
signer/administrator-key SHA-256, keyless issuer where applicable, the
authenticated bundle SHA-256, canonical UTC verification time, verifier
identity/hash and the complete effective machine-policy fingerprint. Partial,
duplicate, malformed, cross-repository or local-image evidence is rejected.
For an online policy-covered repository, cb lock and cb update resolve the
exact digest first, execute only the authenticated staged cosign snapshot,
download bounded signature bundles for that immutable reference, and locally
reverify each bundle against the exact digest, cosign predicate, and configured
identity/key. The lockfile is promoted to schema 2 only when exactly one bundle
passes. Zero or multiple matching bundles, verifier failure, malformed output,
or unavailable policy material aborts the refresh without a digest-only
fallback. Project-scoped refresh preserves unrelated schema-2 evidence. Runtime
executes a policy-covered digest only while its schema-2 evidence matches the
current canonical repository, exact digest, signature mechanism/network mode,
verifier, signer/key identity, issuer and complete machine-policy fingerprint.
Missing or stale evidence fails with policy.image_trust_unverified and must be
refreshed explicitly with cb update or cb lock; cb lock --check reports the
same denial. Old ContainerBin builds reject schema 2 as unsupported; there is
no silent down-conversion.
Tools sharing an image share one lock entry. The Node 24 family
(node24, npm24, npx24, its aliases when selected, and anything exposed
from npm24) rides the single node:24-slim entry. The Node 22 family
(node22, npm22, npx22, its aliases when selected, and anything exposed
from npm22) rides a separate node:22-slim lock entry.
Use cb lock --local TOOL for an image produced by docker build -t or loaded
from an archive. The option is explicit because current Docker engines can
report RepoDigests for both local and pulled images; ContainerBin refuses to
guess which identity you intended. Repeat --local TOOL for each local image
in a full lock operation. Because locks are keyed by configured image, related
tools sharing that image switch together.
Plain cb update TOOL preserves an existing entry's identity mode: rebuild the
same local tag, then update to record the new ID. If that tag is missing, the
update fails instead of silently pulling a registry image. Use the explicit
--local or --registry override to switch modes. Images with non-matching,
foreign RepoDigests still fail closed in registry mode; ContainerBin does not
guess that a retagged registry image should be treated as a local build.
An image-ID lock is deliberately host-local: it makes execution immutable on that Docker daemon, but it does not make the image portable or pullable. Keep the Dockerfile/build inputs or export the image separately for recovery.
Administrators can constrain resolved user configuration through a fixed,
machine-owned policy at C:\ProgramData\ContainerBin\policy.toml (Windows) or
/etc/container-bin/policy.toml (native Linux/WSL). A missing policy preserves
unmanaged behavior. A present but unreadable, invalid, expired, unsupported or
insufficiently protected policy fails closed before non-bootstrap work.
Schema 1 can require an exact image lock, reject local image-ID locks unless
explicitly allowed, and allowlist canonical registry/repository boundaries.
Policy schema 2 can also require a strict detached Ed25519 signature over the
exact container-bin.toml bytes, with machine-owned key validity, revocation
and overlap rotation. Signed registries are read-only to cb; updates must be
provisioned with a matching signature by the administrator. Policy schema 3 can
add repository-bound image-signature requirements. cb lock and cb update
now authenticate and privately stage the pinned verifier and key bytes, run
online verification against the resolved exact repository digest, independently
validate bounded JSON results, record the result as schema-2 evidence, and
authorize runtime use only while that evidence remains fresh against the exact
digest and current effective policy. Offline rules and private-registry
credential bridging remain fail closed.
Lower-precedence registry or command-line choices cannot weaken policy. See
enterprise machine policy for the schema,
ownership rules, normalization behavior and stable diagnostic codes.
cb state # volumes classified CURRENT / SHARED / COMPAT / OTHER / ORPHAN
cb gc # dry-run for current project state
cb gc --apply # delete only explicitly selected current project state
cb gc --orphans # dry-run: labeled volumes whose project path no longer exists
cb gc --orphans --applyManaged volumes carry labels (cb.managed=true, cb.kind, cb.owner,
cb.project_path, cb.project_hash) enabling genuine orphan detection.
Legacy/unlabeled volumes are never guessed to be orphans and never
auto-deleted.
cb backup # zip of registry + lock into backups\
cb restore BACKUP.zip # dry-run: validates and reports
cb restore BACKUP.zip --apply # atomic replacement after validation
# Explicit, checksummed named-volume backup (names come from `cb state`)
cb backup BACKUP.zip --state cb-node24-npm-global cb-go124-gobin
cb restore BACKUP.zip --state # validate/check destinations only
cb restore BACKUP.zip --state --apply # restore state, then registry/lockState backup never sweeps Docker volumes. Every selected name must carry
consistent cb.managed, kind, owner, and project-identity labels, and no running
container may mount it. The versioned manifest records those labels plus each
tar stream's size and SHA-256. Restore revalidates every archive before changing
Docker, never remaps a project path, and refuses label-mismatched or non-empty
destinations. Tar extraction happens only inside a network-disabled helper
container with a read-only root; no archive path is extracted onto Windows.
Keep ContainerBin tool invocations stopped for the entire restore: the command
rechecks running volume users immediately before import, but no filesystem API
can reserve a Docker volume against a new container starting in the remaining
check-to-extract window.
The immutable helper image must already be local; ContainerBin never pulls it as a backup side effect:
docker pull docker.io/library/alpine:3.23@sha256:fd791d74b68913cbb027c6546007b3f0d3bc45125f797758156952bc2d6daf40Backups do not include Docker images, registry credentials, Docker Desktop
configuration, or host project files. Output archives are created exclusively:
choose a new filename instead of overwriting an existing backup. Volume data can
itself contain package credentials or other secrets, so store and transfer the
archive as sensitive data even though ContainerBin requests owner-only file mode.
When valid and bounded, the detached container-bin.toml.sig envelope is
included; a required signed-registry snapshot is re-authenticated before
backup. An invalid optional envelope is skipped with a warning in unmanaged
mode.
See proxies, private registries, and air-gapped operation for mirror identity rules, disconnected image preparation, and the complete state-backup boundary.
cb install, cb add, cb setup, cb backup, cb restore, cb expose, cb unexpose,
cb uninstall, cb lock and cb update serialize through
container-bin.mutation.lock next to cb.exe. A second concurrent
mutation waits up to 5 seconds for the lock, then fails with a clear
message if the holder is still active.
A cb process interrupted with Ctrl-C while holding the lock exits 130
and releases the lock automatically. A cb process that is hard-killed
(e.g. by SIGKILL or the task manager) while holding the lock may leave a
stale container-bin.mutation.lock behind; delete it manually before the
next mutating command.
cb doctor # Docker CLI/engine, container mode, registry schema,
# lock completeness, PATH, shims, python resolution,
# shim directory permissions, reparse points,
# network storage, managed volumes
cb bugreport # version, Windows/PowerShell version, registry inventory,
# doctor output and docker/lock state in one paste-ready block,
# with best-effort secret redaction (review before posting)
cb self-test [--json] [--release] # offline end-to-end test using already-local locked images;
# reports every check instead of stopping at the first failure;
# --json emits a machine-readable report for CI;
# --release adds host/environment facts to the report
# python /venv persistence, external path mapping, Node 24/22
# project state, jq relative paths, terraform -chdir normalization
cb trace ... # dry-run argv/mount mapping for one command
cb inspect TOOL
cb env
cb list
cb default
cb default set node 22
cb self-update --check # read-only stable-release selection; no download or file changes
cb self-update --apply --gh-executable C:\Path\To\Signed\gh.exe
cb wsl prepare --check # native WSL2 only; read-only fixed-layout validation
cb wsl prepare --apply # create missing fixed-layout directories, then revalidate
cb wsl install --check # read-only native install/config/shim plan
cb wsl install --apply # install this native binary, registry and fixed symlinkscb bugreport assembles cb version, the Windows and PowerShell versions
(on Windows), a compact registry inventory and cb doctor output into one
block that is easy to paste into an issue. It applies a small, fixed set of
best-effort redactions (KV-style secrets, AWS access key IDs, GitHub tokens
and Bearer tokens) as defense-in-depth, but the real safety property comes
from the inputs: it never dumps os.Environ(), raw PATH values, lockfile
contents or per-tool inspect detail. Review the report before posting it
publicly — redaction is best-effort, not a general secret scanner.
cb self-test intentionally pulls nothing; it proves your existing locked
setup works end to end, then cleans up its temporary project volumes. It now
runs every check and reports all of them, instead of stopping at the first
failure. The Node 22 checks run when the node22 profile is registered. An
older registry without that newer default gets an actionable skip rather than
a false failure; run cb setup to append the current default profiles.
cb wsl prepare --check is a read-only exception to the still-gated native
WSL frontend. It derives the fixed distribution-local layout from the current
Linux account, UID, distribution name and machine identity, validates
ownership, permissions, symlink boundaries and filesystem locality, and lists
missing directories. --apply explicitly creates only those directories and
revalidates the result. It does not install cb, create shims or config, access
Docker, or enable ordinary commands. See docs/wsl.md.
cb wsl install --check adds a read-only plan over the same fixed layout. It
loads only the root-owned /etc/container-bin/policy.toml policy path and the
distribution-local registry path, authenticates the registry when policy
requires it, and reports required registry, managed-binary, management-shim
and registry-derived tool-shim actions. It does not recover a .bak, create a
lock or change the filesystem. When the primary registry is missing but a
validated backup is present, the plan reports recover rather than create.
cb wsl install --apply first prepares the layout, then serializes the complete
install transaction on the fixed registry lock. For an unmanaged registry it
recovers an interrupted valid backup, creates/upgrades the registry at mode
0600, atomically installs the exact running native executable at
~/.local/lib/container-bin/cb, and creates only missing fixed symlinks without
replacing foreign objects. A signed-registry policy disables automatic registry
creation/upgrades and requires an already provisioned authenticated registry.
The bootstrap executable must itself be a bounded, current-user-owned regular
non-symlink file with safe executable permissions. This command still does not
contact Docker or enable ordinary WSL tool execution; the runtime and real E2E
gates remain. See docs/wsl.md.
cb self-update --check compares a release-qualified Windows/amd64 or
Windows/arm64 build with the latest stable release and reports the exact
artifact, archive, checksum and provenance policy. It does not download assets
or change any files. Development builds fail closed because their installed
version cannot be proved.
cb self-update --apply --gh-executable ABSOLUTE_GH_EXE is the explicit
mutating mode. Invoke it through the installed cb.exe, not a tool shim or a
versioned copy. The supplied native GitHub CLI must be a regular absolute file
with a valid GitHub, Inc. Authenticode signature; ContainerBin never searches
PATH. Set GH_TOKEN or GITHUB_TOKEN for the fixed github.com attestation
request. A target equal to the installed version exits without downloading or
changing files.
Apply uses private same-volume staging, exact checksum plus GitHub build-provenance verification, and a rollback-safe Windows replacement transaction. Only after all remote verification succeeds does ContainerBin copy its already-proven installed binary into a private helper, transfer the bounded request, and exit. The helper waits up to two minutes for the parent, re-verifies the staged artifact and installed identity, serializes with registry/shim mutations, replaces only the management executable and shims proven to contain the installed bytes, then runs version and shim-identity checks. Failure restores the prior complete managed set where possible.
The parent command returns after the authenticated helper launches because the
running cb.exe must exit before it can be replaced. The helper writes the
final success or failure to the same console. Run cb version afterward when
automation needs a separate confirmation of the installed result.
A hard process or host crash can leave a private .container-bin-update-*
staging, helper or rollback directory, or a .cb.exe-update-*.tmp file beside
cb.exe.
ContainerBin does not wildcard-delete these names on a later run because a name
alone does not prove ownership; inspect the object before removing it manually.
Stable selection is the default. Use --prerelease to select the highest
canonical prerelease among the 30 most recent published releases, or
--version vX.Y.Z to inspect one exact published release; those two selectors
are mutually exclusive. Selecting a version older
than the running build is rejected unless --allow-downgrade is explicit. The
architecture comes only from Go's native GOARCH; any other platform fails
explicitly before network access.
--json prints one JSON document to stdout and nothing else (no progress
output, no interleaved tool output) — safe to pipe into a script or CI step.
schema_version is 1; future additions will increment it only if they
change the meaning of an existing field, not for new additive fields.
{
"schema_version": 1,
"cb_version": "v1.2.3",
"generated_at": "2026-08-18T12:00:00Z",
"checks": [
{ "id": "docker", "status": "pass", "message": "docker 27.0.0" },
{ "id": "python-image-local", "status": "pass", "message": "image present" }
],
"passed": 15,
"failed": 0,
"skipped": 0,
"ok": true
}With --release (and only with --release), an environment array is added
immediately after checks; with --json alone the key is omitted entirely, so
plain cb self-test --json output is unchanged. Environment entries use the
same { "id", "status", "message" } shape and the same pass/fail/skip
vocabulary as checks, but they are informational only: they never count toward
passed, failed, skipped, or ok. The seven stable environment IDs, in
order, are:
windows-version— raw Windows build string (e.g.Microsoft Windows NT 10.0.26200.0). This does not distinguish "Windows 10" from "Windows 11" by name; only the build number differs (Windows 11 requires build ≥ 22000). A friendlier caption would need a slower WMI/CIM round-trip, so the raw build number is deliberate.powershell-version— PowerShell version string.docker-engine-version— the Docker Engine version reported bydocker version; this is a Docker Engine version, not the separate Docker Desktop application version shown in Docker Desktop's Settings → About.docker-os-type—docker infoOSType (linuxpasses;windowsfails because ContainerBin runs Linux images only).cwd-reparse-point— whether the current working directory resolves through a junction/symlink.shim-dir-network-storage— whether the shim directory is on a fixed/removable/network/UNC drive.cwd-network-storage— whether the current working directory is on a fixed/removable/network/UNC drive.
The four PowerShell-dependent entries (windows-version, powershell-version,
shim-dir-network-storage, cwd-network-storage) are skipped on non-Windows
hosts; docker-engine-version, docker-os-type, and cwd-reparse-point are
not PowerShell-dependent and still run.
A skip status covers two different situations that share the same status
value but not the same message shape: a genuine "could not determine" (e.g.
not on Windows, Docker unreachable) is always messaged skipped: ..., while
a warn verdict from the reused cb doctor verdict functions — a real,
actionable qualification finding such as a UNC/mapped-drive shim directory or
a reparse-point-backed working directory — is messaged warn: ... instead.
Both report as skip (this schema does not add a fourth status value), but a
CI consumer that cares about the difference can distinguish them by the
message prefix.
Each entry in checks has status of pass, fail, or skip. A skip
means a dependency of that check did not pass (e.g. docker itself failed,
or the tool isn't registered) — message names the reason. A tool missing
from container-bin.toml reports its own check as fail, not skip: an
unconfigured tool was never actually verified, so ok cannot be true while
one is missing. The checks array always contains exactly these 15 IDs, in
this order: docker, python-image-local, python-persist-write,
python-persist-read, python-external-path, node-image-local,
node-modules-write, node-modules-read, node22-image-local,
node22-modules-write, node22-modules-read, jq-image-local,
jq-relative-path, terraform-image-local, terraform-chdir.
| Code | Meaning |
|---|---|
0 |
Success. |
| tool's own code | The invoked tool's own exit status passes through unchanged. |
2 |
Usage error: unknown subcommand. |
120 |
cb infrastructure failure — registry, lock, doctor, or command errors; the message printed to stderr explains which; not distinguishable from a containerized tool that exits 120. |
130 |
Interrupted — Ctrl-C while a mutating command holds the registry lock; not distinguishable from a containerized tool that exits 130. |
pythonopens the Microsoft Store / does nothing — disable the App Execution Aliases (see Installation) or move the shim directory ahead ofWindowsAppsin PATH.cb doctordetects both problems.image "X" is not lockederror — you edited an image in the registry while a lockfile exists. That's fail-closed behavior working; runcb update TOOLorcb lock.- A path argument wasn't mapped — only recognizable Windows path shapes are
mapped (see path mapping above). Check with
cb trace TOOL ARGS...; declarepath_next/path_equalssemantics for the tool if needed. - Docker not reachable — start Docker Desktop;
cb doctorshows what cb sees.
Short version: ContainerBin executes what its configuration tells it to.
Whoever can write container-bin.toml, container-bin.lock, or the shim
directory controls execution. Path mapping is deliberately conservative, env
passing is allowlist-only, mounts are as narrow as possible, and lock
violations fail closed rather than falling back. Full details, including what
is and isn't a vulnerability: docs/security-model.md
and SECURITY.md.
See docs/architecture.md for the full dispatch
pipeline, the provider model (stateless / python / stateful), volume naming,
and the reasoning behind apparently odd behavior (PowerShell argv repair,
FFmpeg's unforced paths, empty node_modules mountpoints, shared lock
entries, legacy Python compatibility state). The shell/process semantics of
that pipeline are in docs/shell-contract.md. Startup
benchmark methodology and the disposable-container tradeoff are in
docs/performance.md.
- Windows x64 + Docker Desktop (Linux containers) only. WSL2 runtime detection, fixed-layout preparation and the native install/config/shim lifecycle are present, but native WSL execution remains gated until runtime, state, Docker Desktop and real WSL qualification slices land. Windows ARM64 has native non-Docker CI coverage, but no published artifact or support claim.
- First invocation of a tool after
cb lockmay still need images present locally (cb lockpulls them;cb self-testnever pulls). - Container startup adds latency compared to native binaries (typically hundreds of milliseconds; interactive REPLs work but feel it).
- Concurrent
cbcommands that mutate the registry are serialized throughcontainer-bin.mutation.lock; a second concurrent mutation fails fast instead of waiting, and a lock file left by a killed process must be deleted manually. - Go shims run a Linux Go toolchain:
go buildproduces a Linux binary by default. SetGOOS=windows(allowed by the profile) to build a Windows executable, e.g.$env:GOOS="windows"; go build.go testmust remain native to the container because a Windows test binary cannot run inside it. cb exposesupports the npm global prefix, Go's shared/go/bin, Cargo's managed install root, uv's tool bin, pipx's managed application bin, .NET's global tool home, and RubyGems executables, plus an explicitly named executable beneath any declared shared volume; plain pip project environments and unmanaged pipx stores are not supported.
Larger ideas (more package-manager integrations, self-update, signing) are
tracked in the
roadmap issue.
Accepted product/security dispositions and the current implementation queue are
recorded in
docs/roadmap-decisions.md.
Detailed entry gates, implementation requirements and acceptance evidence for
the remaining actionable items are in
docs/roadmap-implementation-requirements.md.
The main.go decomposition listed here previously is done; see
docs/architecture.md for the resulting package layout.
Contributions welcome — see CONTRIBUTING.md (containerized build/test instructions; no Go installation required) and CODE_OF_CONDUCT.md.
Licensed under the Apache License 2.0.
Release binaries are built by a tag-triggered GitHub Actions workflow with
SHA256SUMS checksums and GitHub build provenance attestation. The checksum
can detect accidental corruption or a mismatched download, but the manifest is
not itself an authentication mechanism. Compare the downloaded binary hash
with the cb.exe entry in SHA256SUMS:
Get-FileHash .\cb.exe -Algorithm SHA256Then establish artifact authenticity by verifying its GitHub provenance attestation:
gh attestation verify cb.exe --repo AviBackToBlack/container-binThe release keeps the existing raw cb.exe asset for Windows amd64.
Architecture-specific ZIP archives are named
container-bin-VERSION-windows-amd64.zip and
container-bin-VERSION-windows-arm64.zip; each archive contains its target
binary under the required management name cb.exe. ARM64 is archive-only so
users never receive an architecture-qualified executable name that would be
misinterpreted as a tool shim. Verify the exact executable or archive you
download. The ARM64 archive is release-provenance coverage, not a support
claim: full Windows ARM64 support still requires real Docker Desktop
qualification.
cb self-update --check selects the raw cb.exe on Windows amd64 and the
ARM64 archive on Windows arm64 directly from Go's native GOARCH; unsupported
architectures fail explicitly. The verifier authenticates the selected asset
before extracting the ARM64 cb.exe, accepts the legacy two-entry checksum
manifest for pre-ARM64 amd64 releases, and requires the canonical three-entry
manifest for dual-architecture releases.