Skip to content

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. Reads ocx.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 any ocx.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.

TierHow to activateFile
ProjectCWD walk finds ocx.toml; or --project <path>; or OCX_PROJECTnearest 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:

CommandWith --global
ocx addAdds binding to the global file
ocx removeRemoves binding from the global file
ocx lockRe-locks the global file
ocx upgradeAdvances a binding in the global file
ocx pullPre-warms packages declared by the global file
ocx runComposes env from the global file + its lock
ocx envEmits 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:

--selfSurface emittedUse case
off (default)Interface surface — vars where has_interface() is trueHuman or CI script using the package
onPrivate surface — vars where has_private() is trueGenerated 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 -g flags, after all expansion, 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.