Toolbox

Container lifecycle internals

Maintainer notes on how the host CLI creates, secures, and tears down the container. User-facing image knobs (image, registry_mirror, pull) are documented in configuration; port publishing and the loopback bridge under toolbox shell.

Image selection mechanics

internal/build.ResolveImage(image, registryMirror) owns the ref precedence (full image override > registry_mirror host swap > canonical default); imageplan.Ensure requires the resolved image locally and imageplan.Refresh implements the pull policy (never → no registry round-trip, alwaysimagepull.ForcePull, autoimagepull.RefreshIfStale, 1 h TTL cache). The user-visible semantics — canonical default, toolbox build escape hatch, the three config keys, and the caveat that a full image override doesn't see local toolbox build output while a registry_mirror does — live in configuration.

Codex nested sandbox

Codex is unconditionally installed, so toolbox shell creates the container with Docker seccomp=unconfined to allow Codex's built-in bubblewrap sandbox to create nested user namespaces. The flag lives in sessionplan.NestedSandboxSecurityOpt.

Container teardown

The container is disposable: when the last attached shell exits it is destroyed (all persistent state lives on the ~/.toolbox/ bind mounts). The cost of how it is destroyed used to land on the user's prompt — teardown.StopOne ran a synchronous ContainerStop + ContainerRemove, and on macOS Docker Desktop the remove blocks on unmounting ~25 virtiofs binds inside the LinuxKit VM (~1–2s). The SIGTERM grace is not the cost (PID 1 is sleep, dies instantly) and neither is zsh (~90ms); the daemon-side unmount is.

Fix: containers are created with HostConfig.AutoRemove: true (container.createAndStart). The daemon's auto-remove worker performs the unmount + delete after the container exits, asynchronously from any client call. So the exit path only has to make the container exit:

Consequence: a stopped container is auto-removed, so the runplan.ActionStart "reuse a stopped container" path effectively never fires for new containers — every toolbox shell recreates from the canonical image plus the mounted state. The latency moves off the blocking exit and onto a startup the user already expects to do work.

Rejected alternatives: a detached client-side docker rm -f (orphan process, no error feedback, races a fast re-shell); a single synchronous ContainerRemove(Force) (still blocks the client on the unmount). AutoRemove lets the daemon serialise the teardown correctly.