Saphan StudioDocs
Infrastructure

Adding a vendor CLI to an image

Four ready build files, the three properties they all share, and what each CLI does behind a proxy — because two of the four ignore the variable most people reach for first.

For whoever builds the images a fleet dispatches onto.

After reading it you can put any supported vendor CLI into any class image, pin its version, and know how that CLI behaves behind a proxy before you put it behind one.

Before you start: a container runtime and the class image you are building on.

One build file per vendor, and they work on any class

The product ships one build file per supported vendor. Each takes the base image as a build argument and works unchanged on any of the six classes, because every class now carries the same Debian and the same Node.

vendorshapeversion
Claude Codenpm packagepinned, overridable at build time
Qwen Codenpm package — needs Node 22 or newerpinned, overridable
Factory Droidnpm packagepinned, overridable
OpenAI Codexnpm packagepinned, overridable

Three properties they all share, each for a reason:

  1. The version is pinned and overridable. An unpinned global install makes your image's contents a function of the day you built it — the opposite of the digest pinning the rest of the set uses.
  2. They end as the unprivileged user. The install needs root; the agent never runs privileged.
  3. They verify the CLI during the build. A broken install fails your build instead of failing a run at three in the morning.

Codex looks like the odd one out and is not. It ships a native binary, so it is easy to assume it cannot come from npm. It can, and its npm package works the way Claude Code's does: a small launcher plus a per-platform package carrying the binary for your architecture. If your build host cannot reach npm, the codex build file's footer shows the three lines that install a binary you supply instead.

None of the four runs curl … | sh inside the build, and that is deliberate. A vendor's own installer is the right command on a laptop and the wrong one in an image: a build that downloads an unpinned artefact produces an image nobody can reproduce, which defeats the digest pinning everything else here relies on.

Installing a CLI on a laptop or a bare host is a different question, and there the vendor's own installer is the right answer. Take it from the vendor's own page rather than from here — install URLs move, and a stale one in documentation is worse than no line at all.

What each CLI does behind a proxy

Read this before you put a CLI behind one, because two of the four ignore the variable most people reach for first. Every row names the version it was measured on — proxy behaviour is a property of a release, not of a brand.

vendor · version measuredALL_PROXY (SOCKS)HTTP(S)_PROXY
claude 2.1.266 · LinuxIGNORED — goes directhonoured, and fails closed
codex 0.149.1 · Linuxhonoured, fails closed — retries indefinitelyhonoured, same
qwen 0.22.2 · macOS, 0.22.3 · LinuxIGNORED — goes directhonoured, and names the dead proxy
droid 0.213.0 · Linuxignored — ⚠ a diagnostic reading only, see belowread and dialled — diagnostic only

Two of the four ignore ALL_PROXY entirely. If your only control is a SOCKS proxy declared that way, those two reach the network directly and your boundary is not doing what you think.

The droid row is the only diagnostic one. It measures what that vendor's own doctor subcommand does, which is a different question from what a governed run does. And ⛔ that subcommand exits successfully even with failing rows — read its rows, never its exit status.

⇒ The control that does not depend on a CLI's cooperation is egress control: a per-run proxy the run cannot route around, rather than an environment variable it may choose to honour.

What a container bounds, and what it does not

A container is not the network boundary. It bounds the toolchain and the filesystem a session sees. What a session may reach is egress control, and which confinement a container adds beyond the image is measured per machine rather than assumed — Sandboxing.

And one reason to reach for a container anyway: where a vendor's own sandbox collides with the engine's confinement, running that vendor in one of these images makes the container the single boundary. It is a recommendation for a stronger boundary, not a replacement for how that machine runs today — Sandboxing has the per-vendor detail.

On this page