Environment Composition
This page is the reference-level specification for how OCX assembles environment variables and selects which toolchain tier is active. For the motivation behind these design decisions, see Environment composition in the user guide.
Strict Isolation
OCX enforces a hard boundary between the global toolchain and project-tier resolution. The rule is unconditional:
Global tools never compose into, supplement, or fall back into a project's resolved environment.
This applies without exception to:
ocx run— project-tier env-composition command. Readsocx.toml+ocx.lock. The global toolchain ($OCX_HOME/ocx.toml) is not consulted, not merged, and not used as a fallback for tools the project does not declare.ocx exec— OCI-tier env-composition command. Never reads anyocx.toml, whether project or global. Takes OCI identifiers directly.
Both commands are hermetic: the environment they produce is determined entirely by their declared inputs. An undeclared tool is absent, never filled from the global set.
Why hard isolation instead of gap-fill?
Volta pioneered this model for Node.js: global tools are hidden when a project toolchain is active. The alternative — filling in tools the project does not declare from the global set — produces the reproducibility hole OCX is designed to close: collaborators without the same $OCX_HOME/ocx.toml get different resolved environments.
PATH precedence model
OCX enforces isolation by PATH precedence, not PATH stripping. The global toolchain's current/entrypoints/ directory sits on PATH at login time (via $OCX_HOME/env.sh sourced from the login profile). When a project toolchain is activated — via ocx run or ocx direnv — the project tools are prepended to PATH, shadowing any global tools of the same name.
There is no PATH strip, no # ocx: global toolchain suppressed comment, and no _OCX_APPLIED fingerprint. The per-prompt shell hook (ocx shell hook) has been removed entirely. Isolation is a static consequence of PATH ordering: project tools appear earlier in PATH than global tools.
For ocx direnv, the .envrc evaluates ocx direnv export on every directory entry. This emits only the project tools' PATH entries, which direnv prepends before the ambient PATH — global tools remain reachable for tools not declared by the project, but project-declared tools take priority.
What "hermetic" means for ocx run
ocx run reads exactly two files: ocx.toml and its sibling ocx.lock. The resolved environment consists of the tools those files declare — no more. If a tool is not in ocx.toml, it is not in the child environment, regardless of what is installed globally or what is on the parent shell's PATH.
By default ocx run inherits the spawning shell's environment and merely prepends the composed tool bin/ directories to PATH — ambient parent-shell PATH entries remain reachable after the project tools. The default is not hermetic. Pass --clean for a hermetic environment that drops the inherited environment and exposes only the composed tool set, exactly like exec --clean.
What "hermetic" means for ocx exec
ocx exec takes one or more OCI identifiers on the command line. It resolves each identifier, composes the declared environment variables from the resolved packages, and spawns the command with that environment. No ocx.toml is read — not the project file, not the global file. The entire operation is stateless with respect to project configuration.
Tier Selection
OCX has two toolchain tiers. Selection is always explicit — there is no implicit fallback from project to global.
| Tier | How to activate | File |
|---|---|---|
| Project | CWD walk finds ocx.toml; or --project <path>; or OCX_PROJECT | nearest ocx.toml ancestor |
| Global | --global flag; or OCX_GLOBAL | $OCX_HOME/ocx.toml |
The two flags are mutually exclusive — combining --global with --project exits with code 64 (UsageError).
No implicit home-tier discovery. Earlier versions of OCX fell back to $OCX_HOME/ocx.toml when the CWD walk found nothing. That behavior has been removed. The global toolchain is only active when explicitly requested. A CWD walk that finds nothing means no project tier is active — the command operates without a project context.
Root --global affects these toolchain-tier commands
--global is a root flag — it must appear before the subcommand (e.g. ocx --global add ripgrep:14). The following toolchain-tier commands are affected when --global is set:
| Command | With --global |
|---|---|
ocx add | Adds binding to the global file |
ocx remove | Removes binding from the global file |
ocx lock | Re-locks the global file |
ocx upgrade | Advances a binding in the global file |
ocx pull | Pre-warms packages declared by the global file |
ocx run | Composes env from the global file + its lock |
ocx env | Emits composed toolchain env for the global file |
Visibility Surfaces
Each OCX package declares two environment surfaces: the interface surface (what consumers see) and the private surface (what the package's own launchers see).
The --self flag on exec, run, package env, package exec, and package deps switches which surface is emitted:
--self | Surface emitted | Use case |
|---|---|---|
| off (default) | Interface surface — vars where has_interface() is true | Human or CI script using the package |
| on | Private surface — vars where has_private() is true | Generated launchers invoking ocx launcher exec internally |
Generated launchers force self_view = true internally; they do not expose --self to callers.
Composition Order
When multiple packages contribute to an environment (via ocx run -g GROUP1,GROUP2 or ocx exec PKG1 PKG2), env entries are prepended — the last tool walked has its PATH entries placed first in the resolved PATH. In -g argument order, groups listed later win PATH lookup.
For ocx run, the full order rule is:
First by group-selection order (the order of
-gflags, afterallexpansion, deduplicated); then alphabetical by binding name within each group.
See In Depth — Project Toolchain → Composition order rule for the worked example with -g ci,all,release.