Skip to content
ocx
install

Install ocx

This page installs ocx and puts it on your PATH. Pick your shell and paste the command:

Terminal window
curl -fsSL https://setup.ocx.sh/sh | sh

Open a new terminal and check the install with ocx about. It prints the installed version, the detected platform and the detected shell:

Terminal window
ocx about

Prefer not to pipe a script into your shell? Download the binary and run ocx self setup instead, as described under Manual installation.

The one-liners above are thin per-shell bootstraps served by setup.ocx.sh. Each one detects your platform and resolves the latest release from the dist.json manifest, with no GitHub API call in the install path. It downloads the archive and verifies its SHA-256 against the manifest. Then it hands off to ocx self setup.

That command does the content-addressed self-install and writes the per-shell $OCX_HOME/env.* shims. It also adds a managed activation block to your shell profile. Last, it registers a session PATH for the processes no profile reaches.

There is one installer per shell. A bash-only one-liner either fails to parse in fish, Nushell, Elvish or PowerShell, or leaves the profile unwired. Each script is written in its own shell’s dialect, so the one-liner parses natively and the managed block lands in the right profile file.

The installer takes a few knobs. Pass them as flags after -s -- (POSIX sh), or as environment variables that work across every shell’s argument-passing quirks.

Skip every PATH modification. If you manage your PATH yourself, in a CI environment or a dotfile framework, pass --no-modify-path. The installer then touches neither your shell profile nor the session PATH:

Terminal window
curl -fsSL https://setup.ocx.sh/sh | sh -s -- --no-modify-path

You can also set OCX_NO_MODIFY_PATH=1. Either way, add the two directories the installer would have registered to your PATH yourself, toolchain bin first: ~/.ocx/toolchain/active/bin, then ~/.ocx/symlinks/ocx.sh/ocx/cli/current/content/bin.

The flag persists. It writes [shell] modify_path = false to config.toml, so a later ocx self setup run skips both surfaces too until you change the key by hand.

The setting resolves through a four-rung ladder, most specific first:

  1. --no-modify-path on ocx self setup.
  2. OCX_NO_MODIFY_PATH, where a truthy value turns modification off.
  3. [shell] modify_path in config.toml.
  4. The floor, where modification is allowed.

There is deliberately no --modify-path flag. The opt-out fails safe toward touching less of the machine, so setup only ever writes this key as false. To turn modification back on, set the key to true or remove it by hand.

The key gates the managed activation block in your profiles and the session PATH registration. It does not gate the env shims ($OCX_HOME/env.*), which are written regardless. It merges scalar-wins-if-set across tiers: the nearest tier that sets it wins, in either direction. A managed tier therefore overrides your own $OCX_HOME/config.toml.

[shell]
modify_path = false

The opt-out suppresses both PATH surfaces on all three platforms. One surface is the managed activation block in your shell profiles. The other is the session-level registration: the Windows user environment, a systemd environment.d drop-in on Linux and a login LaunchAgent on macOS. The writers are never called. Each location still appears in the run summary with outcome skipped_opt_out, so you can see what was not touched.

OCX_NO_MODIFY_PATH follows the truthy rules. Only 1, y, yes, on and true, case-insensitive, enable it. An unrecognized non-empty value such as skip logs a warning and is read as false, so both PATH surfaces are written. An empty string is also false.

With the opt-out set, you give up reaching what no profile reaches. Activation through $OCX_HOME/env.* takes effect only in interactive PowerShell and POSIX shell sessions that source the login profile. The cmd.exe shell, desktop launchers, IDEs and background services never do. To make ocx-managed binaries visible to those, put the two directories above on PATH yourself. Use System Properties or Group Policy on Windows, an environment.d drop-in on Linux, or a LaunchAgent on macOS.

Leave completions to another framework. Set OCX_NO_COMPLETION=1 and ocx self activate skips the shell-completion injection at every shell start. The PATH prepend and the global toolchain environment still run. Use it when completions come from a framework such as oh-my-zsh or a Nix-generated completion store that OCX should not overwrite.

ocx shell completion generates the script regardless, unless it is called with --if-enabled. In that case this variable is the first rung of the policy that decides whether anything is printed.

Choose the profile files. --profile PATH (repeatable) names the shell profiles that receive the managed activation block. --no-profile writes none. The choice resolves through three rungs: the flags, then [shell] profiles, then the floor, which auto-detects the usual profile files for the current shell.

Absence and an empty list are different answers. An unset key re-detects the profiles on every run. An empty list, the shape --no-profile writes, means no profile blocks at all. A set list replaces auto-detection and is never unioned with it.

Passing either flag writes the key, so the choice applies to every later ocx self setup run, not only the one that typed the flag. --no-profile does not suppress the env shims or the session PATH registration. Only modify_path = false (or --no-modify-path) does that. Tiers merge list-wins-if-set: the nearest tier that sets profiles replaces the whole list, a managed tier included. A tier that leaves the key unset never clears a list a lower tier set.

[shell]
profiles = ["/home/alice/.bashrc", "/home/alice/.zshrc"]

Pin a version. The OCX_INSTALL_VERSION env knob is the portable way to install an exact release. <VERSION> is the semver string with no leading v:

Terminal window
OCX_INSTALL_VERSION=0.5.0 curl -fsSL https://setup.ocx.sh/sh | sh

For CI, the pinned, immutable URL is the most explicit form:

Terminal window
curl -fsSL https://setup.ocx.sh/sh/0.5.0 | sh

PowerShell needs the script compiled to a scriptblock so -Version binds to its parameter:

Terminal window
& ([scriptblock]::Create((Invoke-RestMethod 'https://setup.ocx.sh/pwsh'))) -Version 0.5.0

Track prereleases. To follow prereleases instead of stable, use the next channel on any per-shell prefix, such as https://setup.ocx.sh/sh/next or https://setup.ocx.sh/pwsh/next.

Behind a corporate CA. The OCX_INSTALL_CA_BUNDLE env knob points the installer at a corporate root for its own downloads. Export OCX_EXTRA_CA_CERTS beside it. Then ocx self setup persists the certificate text as extra_ca_certs_pem in config.toml, so every later command trusts it too:

Terminal window
export OCX_INSTALL_CA_BUNDLE=/etc/pki/corp-root.pem OCX_EXTRA_CA_CERTS=/etc/pki/corp-root.pem
curl --cacert "$OCX_INSTALL_CA_BUNDLE" -fsSL https://setup.ocx.sh/sh | sh

The PowerShell installer’s own bootstrap download has no CA-bundle option. Unlike the POSIX installer’s --cacert above, nothing on the PowerShell side hands the corporate root to the script’s HTTPS fetch of the release archive. An OCX_INSTALL_CA_BUNDLE set for it only draws a warning, because the script forwards it as SSL_CERT_FILE, which no Windows client reads. That is why the block above does not set it.

Behind an intercepting proxy, install the root into the machine certificate store for that one download. Or use Manual installation to fetch the binary yourself and run ocx self setup directly. OCX_EXTRA_CA_CERTS still reaches ocx self setup in either case, by ordinary environment inheritance. Invoke-Expression/iex runs the downloaded script in the current session, so a variable set with $env: beforehand is visible to it (ocx-sh/www-setup#23).

setup.ocx.sh serves a separate installer per shell. Pipe each URL to its matching interpreter: | sh, | nu, | fish, | elvish, or irm … | iex for PowerShell. The POSIX sh installer covers every Bourne-family shell with one script.

ShellInstaller URLPlatforms
POSIX sh (bash, zsh, ash, ksh, dash)setup.ocx.sh/sh
PowerShellsetup.ocx.sh/pwsh
Nushellsetup.ocx.sh/nu
fishsetup.ocx.sh/fish
Elvishsetup.ocx.sh/elvish

Security-conscious environments often forbid piping a network script into an interpreter. The downloaded binary is the answer. Pre-built binaries for every supported platform are published to GitHub Releases.

Download the archive for your platform, extract it and run ocx self setup. You reach exactly the same managed state the install script produces. That state is the content-addressed self-install, the $OCX_HOME/env.* shims, the managed shell-profile block and the session PATH. No shell script is piped from the network.

Each release also includes a sha256.sum file for checksum verification.

Terminal window
# Download the archive and checksums
curl -LO https://github.com/ocx-sh/ocx/releases/latest/download/ocx-x86_64-unknown-linux-gnu.tar.gz
curl -LO https://github.com/ocx-sh/ocx/releases/latest/download/sha256.sum
# Verify checksum
sha256sum --check --ignore-missing sha256.sum
# Extract and run the managed setup
tar xf ocx-x86_64-unknown-linux-gnu.tar.gz
./ocx-x86_64-unknown-linux-gnu/ocx self setup

On Windows, extract the zip and run the setup from the extracted directory in PowerShell. Then reload your profile:

Terminal window
.\ocx.exe self setup
. $PROFILE

If PowerShell’s execution policy is Restricted, the sourced env.ps1 file does not run even after setup completes. Setup reports a non-fatal advisory when it detects this. To fix it, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned. Setup never changes the execution policy itself, because that is a user security decision.

ocx self setup performs four steps in strict order:

  1. Bootstraps itself. It installs ocx.sh/ocx/cli, the stable release unless you pass a version, into the content-addressed package store and wires the current symlink. The loose binary you downloaded is only needed for this step. Afterwards the managed copy in ~/.ocx/ takes over.
  2. Writes the env shims. It creates $OCX_HOME/env.sh, env.fish, env.ps1, env.nu and env.elv. These files are byte-identical across users.
  3. Injects a source line. It adds a fenced block marker to each detected shell profile. Re-running setup is safe, because the fence is idempotent.
  4. Registers a session PATH. It writes $OCX_HOME/toolchain/active/bin and the ocx installation’s bin directory behind it into the session store of your platform.

If the bootstrap fails, for example because the registry is unreachable, the command returns a non-zero exit code and writes nothing.

Besides the flags covered under Options, ocx self setup accepts these:

FlagEffect
--toolchain-activate MODEWrites activate = "MODE" into $OCX_HOME/ocx.toml. MODE is env, bin or none, and it decides how the global toolchain reaches your shell. The file is created with only this key if it does not exist yet.
--dry-runShows what would be written without writing anything. Use it to preview which profiles are detected.
--forceOverwrites a fenced block whose contents were edited by hand.

When a job needs the same ocx build on every run, pass a version to ocx self setup. The version can be a tag, a content digest, or both:

# Pin by tag (resolves once, installs that release):
ocx self setup 0.9.2
# Pin by content digest (no tag resolution):
ocx self setup sha256:ab12cd34ef56...
# Pin by tag and assert the exact content (immutability assertion):
ocx self setup 0.9.2@sha256:ab12cd34ef56...

The tag@digest form is the strongest guarantee. If the tag ever resolves to different content, for example because a digest was republished, the command fails with exit 65 and names both digests. There is no silent drift.

To get the digest for a version you trust, capture it from a prior run’s JSON output:

digest=$(ocx --format json self setup 0.9.2 | jq -r .bootstrap.digest)
# Then write the full pin into your CI config:
echo "ocx self setup 0.9.2@$digest"

That digest round-trips. The next ocx self setup 0.9.2@<digest> call succeeds immediately (already_present) if the same version is already installed. Otherwise it downloads and verifies the exact content. When the requested version is already installed, ocx self setup is a no-op and exits 0. It rewrites the shims and profile blocks only when their content has drifted.

jq .status returns the overall run outcome. The bootstrap outcome sits under bootstrap.status and is already_present, pulled or would_pull.

Under --frozen, a tag-only pin resolves from the local index, and exits 81 if the tag is absent. Run ocx index update <package> without --frozen first, because an index update is refused under --frozen too. A digest-only pin works when the blobs are already cached. Both suit hermetic CI where the index is pre-populated.

A shell profile reaches login shells. It does not reach an IDE, a desktop launcher or a background service started outside any shell. Those inherit their environment from the session, and a PATH written into ~/.zshrc never gets there. So ocx self setup writes a session-level registration too, co-primary with the profile block rather than a fallback for it.

Two directories are registered, in this order. A global toolchain that pins ocx is what a session resolves, and the installed binary is the floor beneath it:

  1. $OCX_HOME/toolchain/active/bin, the global toolchain’s trampolines.
  2. $OCX_HOME/symlinks/<ocx cli id>/current/content/bin, the directory the installed ocx itself resolves from.

Each platform has exactly one store:

PlatformStoreMechanism
WindowsHKCU\Environment, value PathWritten as REG_EXPAND_SZ, then a WM_SETTINGCHANGE broadcast so running applications pick the change up.
Linux$XDG_CONFIG_HOME/environment.d/ocx.conf (~/.config/environment.d/ocx.conf when XDG_CONFIG_HOME is unset)One environment.d line, PATH=…:$PATH. It is a prepend, so nothing another tool put on PATH is displaced.
macOS~/Library/LaunchAgents/sh.ocx.path.plistA RunAtLoad LaunchAgent labelled sh.ocx.path, published at mode 0644 (launchd refuses an agent with “dubious permissions”) and bootstrapped into the current GUI domain.

The registration is idempotent by presence test, never by append. A second ocx self setup reports unchanged and the stored value does not grow.

A write failure is a warning, not an exit code. A store that cannot be written is reported and warned about, and setup still exits 0. Re-running retries it.

The one fatal refusal is an $OCX_HOME this platform’s format cannot spell. On Windows that is a % or ;. For environment.d it is a character the format cannot carry, and for the plist it is a ". That is checked before anything is written, so a refused run leaves the machine byte-identical and exits 78.

environment.d is read only by processes started under systemd --user. That is confirmed for GNOME and KDE Plasma Wayland. Neither LightDM (by default), SDDM nor a non-systemd desktop reads it. A Flatpak- or Snap-sandboxed application takes its PATH from the sandbox instead.

~/.pam_environment is deliberately not written, since it has been deprecated since pam_env 1.5.0. On every platform, terminals and applications that are already open see nothing until they are restarted.

There is no ocx self uninstall yet (#413), so the registration is reversed by hand. One location per platform:

Terminal window
# Open the built-in editor, remove the two ocx segments from the user Path, and
# save. Saving preserves the value's type and broadcasts the settings change,
# which is why this is the recommended route.
rundll32 sysdm.cpl,EditEnvironmentVariables
# Or from PowerShell, through the registry API rather than [Environment]:
# [Environment]::SetEnvironmentVariable(..., 'User') always writes REG_SZ,
# no matter what type the value had before. That is a permanent downgrade:
# no %VAR% any tool adds to Path later will ever expand again. The registry
# API below reads the value unexpanded and writes back the same type it read.
# Unlike the built-in editor, it does not broadcast WM_SETTINGCHANGE, so
# already-open terminals need a restart.
# Read $root from a guarded expression, never bare $env:OCX_HOME: an empty
# prefix would match every segment and strip the whole PATH.
$root = if ($env:OCX_HOME) { $env:OCX_HOME } else { Join-Path $HOME '.ocx' }
$key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Environment', $true)
$path = $key.GetValue('Path', '', 'DoNotExpandEnvironmentNames')
$kept = ($path -split ';' |
Where-Object { $_ -and -not $_.StartsWith($root, 'OrdinalIgnoreCase') }) -join ';'
$key.SetValue('Path', $kept, 'ExpandString')
$key.Close()

ocx’s own writer does not treat the two directions the same way. Registering the entries (ocx self setup) always writes REG_EXPAND_SZ, even over an existing REG_SZ value, because rustup once shipped REG_SZ here and broke %VAR% expansion for every other entry already on Path. Deregistering them preserves whatever type it read. Removal never promises to upgrade or downgrade a value it is not otherwise touching.

Both directions read the value unexpanded, so a foreign %…% reference already on Path is never flattened into whatever it resolves to at that moment. The built-in editor is the one form that both preserves the type and broadcasts the change.

Reversing the session PATH does not remove ocx. The managed shell-profile block, the $OCX_HOME/env.* shims and $OCX_HOME itself stay in place. The uninstall recipe covers them. Neither recipe touches a project’s own rendered toolchain. Delete .ocx/toolchain/, or the toolchain_dir root if one is configured, in every project where you have run ocx pull.

Binaries published from the deploy-dev workflow carry a channel: dev field and a version string with a build-metadata suffix, such as 0.3.2-dev+20260528143045. These are pre-release snapshots, not stable releases.

To identify whether your installed binary is a dev build, run:

Terminal window
ocx version --verbose

The channel: dev line appears in the verbose output when present. For scripting, read the report as JSON and pipe it through jq .channel:

Terminal window
ocx --format json version

The channel field reads "dev" for dev builds and is absent for stable releases, so jq .channel prints null there. Include this output when filing bug reports. It helps distinguish issues introduced between stable releases.