If you use proximo to reach your local-dev apps at https://<name>.test on the host, the toolbox makes those same URLs work from inside the container — for any client (curl, git, Node, Python, Playwright/Chromium, …), with proximo's certificate trusted so no -k / ignoreHTTPSErrors is needed. This file covers why that needs toolbox plumbing, how DNS + CA trust are established, and the bridge-backed lifecycle commands.
.test is unreachable from a sibling containerproximo makes any labelled Docker container reachable at https://<name>.<tld> (default .test): it runs Traefik publishing :80/:443 on the host, installs a host resolver mapping *.<tld> → 127.0.0.1, and trusts a local CA in the host OS + NSS stores. That works for the host browser. It does not work from inside a toolbox container: 127.0.0.1 there is the container's own loopback, not the host where Traefik listens, so DNS resolves but the connection refuses. proximo also never injects its CA into arbitrary containers, so even a reachable endpoint fails TLS verification.
proximo)config.Config.Proximo is a *bool, resolved into the Proximo Availability Gate by proximo.Resolve(host, cfg): an explicit true/false wins; omitted (nil) auto-detects — on iff proximo's root CA exists on the host (proximo install wrote it under ~/.proximo). So a host with proximo installed gets .test reachability in every shell with zero per-repo opt-in, and a host without it pays nothing. proximo: false opts a project out; proximo: true forces it on even when the CA is absent (the mount then soft-skips). *bool (vs a plain bool) is what makes nil mean "auto" rather than "off" — the same tri-state shape as bridge, but with an auto rather than always-on default.
internal/proximo supplies both. The toolbox CLI runs on the host alongside proximo, so it can discover routed hosts directly from Docker labels — no enumeration in .toolbox.yaml, no upstream proximo change, no shared Docker network.
| Concern | Mechanism | Seam |
|---|---|---|
| DNS | Every running container's proximo.hosts label value is read; each hostname is pinned to the Docker host-gateway and appended to HostConfig.ExtraHosts. host-gateway resolves to the host where Traefik publishes :443, bypassing Docker networks entirely. These --add-host pins are fixed at create; the in-container proximo-hosts command re-syncs them at runtime for stacks started later (see lifecycle). |
discovery: container/lifecycle.go augmentProximoHosts (needs the Docker client); pure parser: proximo.ExtraHosts |
| Cert | proximo's root CA (path queried via proximo config ca-path — the stable contract from filippolmt/proximo#20; fallback ~/.proximo/tls/ca.pem, proximo's current state home, filippolmt/proximo#17) is bind-mounted RO at /etc/ssl/proximo-ca.pem. entrypoint.sh then establishes seamless trust for every client (see below). NODE_EXTRA_CA_CERTS (Node uses its own bundle) and TOOLBOX_PROXIMO_CA (path pointer for the certifi gap) are also exported. |
mount: the gate's CAMount injected in mountplan.Merge; env: the gate's Env appended in sessionplan.Plan; trust: entrypoint.sh proximo block |
entrypoint.sh runs a proximo block gated purely on [ -f /etc/ssl/proximo-ca.pem ] (the mount is the signal — no extra env). It is idempotent and every step is best-effort (|| true) so a trust failure never aborts boot:
| Client | Trusted via | Notes |
|---|---|---|
curl / git / wget / python (ssl, urllib) |
system bundle | sudo cp into /usr/local/share/ca-certificates/proximo.crt + sudo update-ca-certificates (passwordless sudo; refreshed only when the cert changes via a cmp guard) |
| Chromium / Firefox (incl. Playwright's bundled browsers) | NSS | certutil -A -t C,, -n proximo into $HOME/.pki/nssdb (libnss3-tools, base apt layer). ~/.pki is a HOME subdir, not a bind-mount → ephemeral, rebuilt from the mounted CA every shell |
| Node / Playwright (node API) | NODE_EXTRA_CA_CERTS |
additive, set by the gate's Env |
| python-requests | — | uses certifi, not the system store; set REQUESTS_CA_BUNDLE="$TOOLBOX_PROXIMO_CA" (this is the one non-seamless client) |
The generic CA-certificate folder (~/.toolbox/certs/) shares this same system bundle: when any cert is dropped there, its entrypoint block re-points NODE_EXTRA_CA_CERTS / REQUESTS_CA_BUNDLE at the full /etc/ssl/certs/ca-certificates.crt — a superset that still contains proximo's CA, so proximo trust is unaffected. Reach for that folder for any non-proximo CA (corporate proxy, internal runtime); proximo's CA is wired automatically and needs nothing dropped there.
proximo up|down|status|errors|skill works inside toolbox shell via the bridge: the image ships /usr/local/bin/proximo (internal/build/assets/bin/proximo, sibling of the editor shims) which POSTs {"command": "<sub>", "args": [...]} to the daemon's /proximo endpoint; the daemon execs the host proximo binary and returns {"exit": N, "output": "…"}, which the shim prints and propagates. Running the real binary in-container cannot work — and the failure is silent, not loud: proximo materializes its compose stack under ~/.proximo and bind-mounts those paths, which the host Docker daemon resolves host-side where they don't exist, while the colliding compose project name makes up recreate the working host stack instead of refusing. Hence no proximo binary in the image and no catalog row: absence is the only robust boundary (ADR-0004).
proximoAllowlist (internal/bridge/proximo.go) = up/down/status/errors/skill; everything after the subcommand is forwarded verbatim, so a flag added upstream needs no toolbox change. The shim knows nothing about proximo's flags — classification lives only in the daemon. Two rules close the host-write path. --out/-o (upstream's spelling, on the errors dom and errors transcript leaves) is rejected with a 400 — via the bridge it writes to the host filesystem, and redirecting the shim's stdout in the container is the right way to capture output. And errors dom is refused outright, flag or no flag: it always writes an HTML file, defaulting to os.TempDir()/proximo-dom-<id>.html, so no flag rule can see it. Through the bridge that file lands where the container cannot read it, which makes the subcommand useless here anyway; errors transcript writes to stdout without --out and stays bridged. Everything else passes, --image on up included: pulling an arbitrary reference is the same trust the container already holds over the mounted Docker socket. install/uninstall/trust need host root → refused by the shim with a run-on-host hint, never bridged; an unattended container→host-root path is one a prompt-injected agent inherits. config is refused too: its one mutating form (config tld) rewrites the host resolver, and its queries describe a host the container cannot change.skill runs with the agent home rewritten. proximo skill install writes files an in-container agent must read, so the daemon runs that one verb with HOME=$HOME/.toolbox and CODEX_HOME=$HOME/.toolbox/.codex at --scope global — exactly the sources mountplan.defaults binds to /home/toolbox/.claude and /home/toolbox/.codex, so one run installs for Claude and Codex together. --scope is appended only to the install/uninstall leaves, which are the only ones upstream registers it on — a bare proximo skill would otherwise fail with unknown flag. An explicit --scope the caller passed is left alone rather than silently overridden, and a caller-supplied home that is not an existing host directory fails the request instead of falling back to a default. Restart the agent session afterwards to pick the skill up. → Proximo Execution Modes[ -f /etc/ssl/proximo-ca.pem ] — the same predicate entrypoint.sh self-gates its trust block on, and the in-container shadow of the host-side gate. A host without proximo, a workspace with proximo: false, or a container created before proximo was installed (the CA bind is fixed at create — toolbox stop and reopen) each get one refusal naming all three causes, and get it even when the bridge is not installed. The check sits after the subcommand dispatch, so a host-only verb still gets its own run-on-host hint. TestBridgeContract_ShimMatchesGo pins that path against proximo.CATarget. → Proximo Availability Gateerrors transcript is unredacted. Upstream documents it as "may contain credentials", and this route carries it from the host's in-memory buffer to the stdout of an agent inside the container. Treat the output as secret-bearing. Note the gate is the shim's alone: the bridge token is readable inside the container by design, so a direct curl to /proximo bypasses it, and a workspace that opted out with proximo: false is not protected from a deliberately hostile container process. That is the bridge's standing trust model — /credential already forwards host git credentials — not a property this endpoint weakens.requestTimeout. First up pulls the stack images, so /proximo gets proximoTimeout (120s) and pushes the connection write deadline past the server's WriteTimeout: 10s via http.NewResponseController (per-response, other routes keep tight limits). cmd.WaitDelay = 2s stops CombinedOutput from hanging on pipes inherited by compose children after a deadline kill. Shim curl --max-time 135 sits above the server budget. A non-zero exit is data (propagated), not an error; only infra failures (binary missing, deadline) are 502./opt/homebrew/bin and ~/go/bin; resolveProximoBinary does exec.LookPath then probes /opt/homebrew/bin/proximo, /usr/local/bin/proximo, $HOME/go/bin/proximo. No binary → ErrProximoNotInstalled, surfaced verbatim by the shim; it names the host command to run, because install is host-only by construction — the daemon would have to exec a binary that isn't there. The child proximo process gets the same treatment: its PATH is augmented (appendPathDirs + proximoChildPathDirs — dir of the resolved binary, the well-known bin dirs, ~/.docker/bin, ~/.orbstack/bin; append-only, existing entries win) so proximo's own docker lookup also survives the minimal service PATH — otherwise proximo status bridged from inside reports "docker is not installed" on a host that runs Docker fine. Lives in daemon code → active on daemon restart, no plist/unit regeneration.toolbox bridge install"; new daemon + old image → no shim, nothing to break. Daemon restart (to pick up the endpoint) = rerun toolbox bridge install.ExtraHosts to the current container (create-time discovery, below) — hosts pinned at create become reachable immediately; .test apps started later are picked up automatically by the proximo-hosts watcher (next bullet), no re-shell.proximo-hosts, automatic)--add-host/ExtraHosts is computed once at ContainerCreate and is immutable for the container's life, so a proximo stack started after toolbox shell is invisible to those pins: <name>.test falls through to the host resolver, resolves to 127.0.0.1 (right for the host, wrong inside the container) and refuses. The in-container proximo-hosts command (internal/build/assets/bin/proximo-hosts) is the runtime complement: it re-discovers the proximo.hosts labels from the host daemon over the mounted /var/run/docker.sock (DooD — docker ps -q + docker inspect, whose .Config.Labels is a map), resolves the host-gateway IP (IPv4-first from the always-pinned host.docker.internal, with a default-route fallback), and rewrites a marker-delimited managed block in /etc/hosts (sudo, passwordless) pinning every routed hostname to that gateway. /etc/hosts is mutable at runtime, so reachability tracks the running stacks with no recreate. It is idempotent; the markers are emitted even when nothing is routed, so a brought-down stack self-clears.
It runs automatically. When proximo is enabled (the CA mount is present), entrypoint.sh launches proximo-hosts --watch in the background: an initial sync, then a re-sync on every start/die of a proximo.hosts-labelled container (docker events — a streaming long-poll, near-zero idle cost; the loop reconnects if the daemon restarts). So stacks brought up or down mid-session are pinned (or unpinned) with no manual step. Running proximo-hosts by hand stays available to force a sync. The watcher logs to ~/.toolbox-state/proximo-hosts.log.
--add-host discovery runs at container create only. ExtraHosts is fixed at ContainerCreate (same as port bindings — see publishing ports). New .test hosts that come up after toolbox shell are invisible to the create-time pins, but the proximo-hosts --watch daemon (started by entrypoint.sh, above) re-syncs them at runtime, so no recreate is needed. Stopped proximo stack → augmentProximoHosts warns and degrades to "names unreachable" rather than failing the shell.CAMount is added in Merge (not defaults()), so the canonical default-mount set and the smoke-test init.d/completion bijections are untouched (the proximo trust step lives in entrypoint.sh, not an init.d script, so it ties to no catalog tool). A missing CA file (proximo not installed) soft-skips the mount with a resolveAll warning; the gate's Env withholds the trust variables on that same absence — one resolved CAExists, read by both — so Node never points at an absent NODE_EXTRA_CA_CERTS, and the entrypoint block no-ops when the mount is absent.certutil (libnss3-tools, base apt layer) and the lifecycle shim ships at /usr/local/bin/proximo; smoke-test.sh asserts both. Everything else is host-side and image-hash-neutral.proximo config flag steers mounts/env/ExtraHosts only; the shim is unconditional in the image (it degrades with an install hint when the bridge state dir is absent).