Deferred Tools
A project toolchain can declare a dozen tools and use three of them in any given job — a monorepo's ocx.toml might list every compiler, linter, and formatter the team owns, while a single CI step runs eslint and nothing else. ocx env and ocx exec compose the whole declared set by default, which means every tool's content downloads before the job's first command even starts.
lazy-mode changes when a tool's content downloads, not what ends up composed. A tool set to always still lands on PATH immediately — its declared names resolve, ocx package which finds it, ocx exec <name> sees it in scope — but the bytes behind it stay unfetched until the first invocation of one of its names. A job that never calls eslint never pays for it.
Composing a shim
Every env-composing command accepts --lazy-mode:
ocx init
ocx add --no-pull "kitware/cmake:4.2.0" "astral-sh/uv:0.10.0"
ocx env --lazy-mode always
ocx package which --lazy-mode always "kitware/cmake:4.2.0" "astral-sh/uv:0.10.0"
ocx exec --lazy-mode always -- cmake --version
ocx package which --lazy-mode always "kitware/cmake:4.2.0" "astral-sh/uv:0.10.0"Under always, OCX writes a small generated launcher per declared name into a shim directory and composes that directory onto PATH — the same PATH slot an eagerly-materialized package root's entrypoints/ would occupy, just pointed at a directory with no content/ yet. ocx package which and ocx pull's JSON report both say so explicitly: every entry carries a kind of package or shim, so a script can tell the two apart without probing the filesystem.
Like containerd's lazy image pulls
containerd's stargz-snapshotter does the same trade for container images: an eStargz-formatted layer lets a container start running before its files finish downloading, fetching each file lazily the first time a process opens it. OCX applies the same idea one layer up — at the tool level instead of the file level, and driven by the first process invocation instead of a filesystem read.
Resolution ladder
--lazy-mode is a policy, not a one-off flag — it resolves through five tiers, most specific first, with never (eager) as the floor:
| Tier | Source |
|---|---|
| 1 | --lazy-mode on the invoked command |
| 2 | [package."<id>"] in ocx.toml |
| 3 | [group.<name>] in ocx.toml |
| 4 | The toolchain-level lazy-mode key in ocx.toml |
| 5 | OCX_LAZY_MODE |
| — | Floor: never |
Each tier is independently optional; an absent tier means inherit from the next one down, never this tier said never. Setting lazy-mode = "always" at the toolchain level and lazy-mode = "never" on one package's own entry composes that one package eagerly while every other declared tool defers — the package-tier entry is a decision, not a gap.
First invocation
A shim's generated launcher is a small script, deliberately similar to the one an entry point writes:
#!/bin/sh
# Generated by ocx at install time. Do not edit.
exec "${OCX_BINARY_PIN:-ocx}" launcher shim '<pinned-id>' -- "${0##*/}" "$@"It execs the hidden ocx launcher shim subcommand, passing the pinned identifier the shim was built for and the name the caller typed. That subcommand runs the ordinary pull — the same three-layer fetch, extract, and assemble pipeline an eager install uses — then resolves the requested name on the freshly materialized package's own PATH and executes it. Nothing about the pull is special-cased for laziness: a lazily materialized tool is byte-identical to the same tool installed eagerly.
Once materialized, the tool's real entrypoints/ directory outranks the shim on PATH for every later invocation in that environment, so a second call never re-triggers the shim path. ocx package which reflects the same flip — its kind reports shim before the first use and package after.
Progress during the download
lazy-report controls whether that first-invocation download renders progress. It cannot be a flag on ocx env or ocx exec, because the process rendering it — ocx launcher shim — is a separate process the shell already exec'd into by the time any content moves; nothing survives from the composing command to tell it what to show. lazy-report therefore resolves independently, inside the shim process itself, from its own four-tier ladder — one tier shorter than lazy-mode's, because there is no group to consult once composition is over:
| Tier | Source |
|---|---|
| 1 | --lazy-report (only on ocx launcher shim — never typed by a user) |
| 2 | [package."<id>"] in ocx.toml |
| 3 | The toolchain-level lazy-report key in ocx.toml |
| 4 | OCX_LAZY_REPORT |
| — | Floor: silent |
progress opens a channel on the controlling terminal; where none exists — a Docker build, a CI runner, anything under setsid — it silently degrades to silent rather than failing. Errors always reach stderr regardless of this setting.
Advisories
Some metadata.json shapes only substitute cleanly once a package's content is on disk — a ${installPath}-rooted value concatenated with something else, for instance, or a claim of zero binaries on a node that never got a chance to declare any. Composing such a package as a shim can't detect these the way an eager install's file scan does, so ocx env and ocx package env instead emit an advisories array in their JSON output — warning-only, never a compose failure — naming the affected package and, where relevant, the key.
Windows
A deferred tool's shim slot on Windows is a <name>.exe — hardlinked from the same content-addressed shim-executable blob every generated Windows entrypoint launcher shares — paired with a <name>.shimref sidecar: one line naming the pinned identifier the shim was built for. lazy-mode resolves and composes the same way on Windows as on any other host; there is no platform floor that forces eager composition.
First invocation reads the sidecar, materializes the package through the same ocx launcher shim pipeline described above, and resolves the requested name on the freshly materialized package's own PATH — exactly as the POSIX shim does.
Garbage collection
A shim directory is kept alive by the same lock-pinned root set that keeps an eagerly-installed package alive — ocx clean regenerates a collected shim on the next compose, exactly as it would re-pull a collected package.
ocx clean --force is the one case where this differs from an installed package. A deferred tool has no install symlink pointing at it — only the lock pins reference it — and --force's entire purpose is to suppress the lock-pinned root set for the run. So --force collects every shim directory unconditionally, the same way it already collects an unsymlinked eager package. The next ocx env or ocx exec regenerates whatever shims that composition needs; nothing is lost, but the first post---force invocation of a deferred tool re-materializes it from scratch.