Development Preview build — APIs and content may change. Visit ocx.sh for the current release.
Skip to content

FAQ

Versioning

Build Separator

Semantic Versioning uses + to delimit build metadata (e.g., 1.2.3+20260216). The OCI Distribution Specification restricts tags to [a-zA-Z0-9_][a-zA-Z0-9._-]{0,127} — the + character is not allowed. This is a known ecosystem-wide issue (distribution-spec#154, open since 2020, unresolved). The + also encodes as a space in URL query strings, making it unsafe even if registries accepted it.

ocx uses _ as the build separator so that every version string is a valid OCI tag by construction:

{major}[.{minor}[.{patch}[-{prerelease}][_{build}]]]

The grammar is fully unambiguous: . separates components, - introduces a pre-release, _ introduces build metadata.

Same convention as Helm

Helm adopted the same +_ normalization when it moved chart distribution to OCI registries. See helm/helm#10250 for the original discussion.

Tolerant input: typing + works and auto-normalizes to _. For example, ocx install cmake:3.28.1+20260216 installs the tag 3.28.1_20260216. This normalization happens at the earliest boundary — the identifier parser — so all downstream code sees _ only.

See Versioning in the user guide for the full tag hierarchy and cascade behavior.

Dependencies

No Version Ranges

ocx dependencies are pinned by OCI digest, not by version ranges. There is no "give me any Java >= 21" logic. The publisher records the exact build they tested against, and that is what you get.

This is a deliberate design choice. Version ranges introduce a resolution algorithm — typically SAT-solving or Minimum Version Selection — that produces different results depending on what is available in the index at resolution time. Two machines with different index states can resolve the same dependency specification to different binaries. This directly contradicts ocx's reproducibility guarantee: the same metadata should always produce the same result.

Why not Minimum Version Selection?

Go modules and Bazel's Bzlmod use Minimum Version Selection (MVS) — the highest minimum version requested by any dependent wins. MVS is deterministic and polynomial-time, but it still requires an index to enumerate available versions. ocx dependencies work offline with no index at all: the digest is the pin. Future tooling will help publishers discover when their pinned dependencies have newer builds available, keeping the update decision explicit.

Automatic Security Patches

When a dependency receives a security fix, the publisher must release a new version of their package with the updated digest. ocx does not automatically substitute a newer build — doing so would break the reproducibility guarantee.

This tradeoff matches the Nix model: a security patch means rebuilding every affected derivation. The difference is that ocx has no build system — the publisher re-pins the digest and pushes a new tag.

Shared Dependencies

When multiple installed packages depend on the same package at the same digest, the dependency is stored once in the object store (content-addressed deduplication). Each dependent creates a back-reference, so the shared dependency is not garbage collected until all dependents are removed.

When two packages depend on the same tool at different digests, both versions are installed as separate objects. If both contribute to the same environment, scalar variables follow last-writer-wins semantics and ocx emits a warning. Use ocx deps --flat to see the evaluation order and ocx deps --why to trace conflicting paths.

See Dependencies in the user guide for the full picture: automatic installation, environment composition, garbage collection, and inspection commands.

macOS

Code Signing

macOS requires all executable code to carry a valid code signature. On Apple Silicon the kernel terminates unsigned binaries immediately (Killed: 9). On Intel, Gatekeeper blocks them when a quarantine flag is present.

When a publisher never signed their binaries before packaging, the extracted files will be unsigned and macOS will refuse to run them. Signatures that were present before packaging survive the tar round-trip — they are part of the binary content, not extended attributes.

ocx handles this automatically: after extracting a package, it recursively walks the content directory, detects Mach-O binaries, and signs each one individually with an ad-hoc signature. Quarantine flags are stripped. No configuration required.

Same approach as Homebrew

Homebrew solves the identical problem with the same technique — per-file ad-hoc signing without bundle sealing — see codesign_patched_binary in their source. ocx applies signatures after extraction rather than after patching, but the codesign invocation is equivalent.

What ocx runs under the hood

Quarantine removal (applied to the entire content directory first):

sh
xattr -dr com.apple.quarantine <content_path>

For each Mach-O binary found in the content directory (recursive walk, symlinks not followed):

sh
codesign --sign - --force --preserve-metadata=entitlements,flags,runtime <binary>

entitlements, flags, and runtime are preserved from the original signature. requirements (the original certificate's Team ID constraint) is intentionally dropped — preserving it would cause dyld "different Team IDs" errors when loading third-party frameworks. Hardlinked files (same inode) are signed only once.

For package publishers

Signing binaries before packaging is ideal — the signatures survive tar archiving and ocx will leave them intact. But it is not required: ocx applies ad-hoc signatures automatically for any unsigned binary it encounters.

Disabling

Set OCX_NO_CODESIGN to a truthy value to skip automatic signing:

sh
export OCX_NO_CODESIGN=1

Manual Signing

If a binary still fails to launch after installation, sign it manually:

sh
codesign --sign - --force --preserve-metadata=entitlements,flags,runtime /path/to/binary
sh
xattr -dr com.apple.quarantine /path/to/content
Can I disable macOS code signing enforcement entirely?

macOS enforces code signatures through AMFI, which runs independently of Gatekeeper. Disabling it requires Recovery Mode, disabling SIP, and setting a boot argument — a configuration Apple does not support that significantly weakens system security.

The macOS Developer Mode setting (since Ventura 13) relaxes restrictions for development tools like Xcode and Instruments, but does not exempt unsigned binaries. Killed: 9 still occurs on Apple Silicon with Developer Mode enabled.

Ad-hoc signing via ocx is the simplest solution — no certificates, no system changes.

Windows

Executable Resolution

Windows does not treat scripts the same way Unix does. On Unix, any file with a #!/bin/sh shebang and the execute bit set can be launched directly. On Windows, the kernel's CreateProcessW API only searches for .exe files — it ignores .bat, .cmd, and other script types entirely.

Package metadata often exposes tools as shell scripts (.bat on Windows). When ocx exec runs a command, it needs to find these scripts by consulting the PATHEXT environment variable, just like a Windows shell would.

ocx resolves this automatically using the which crate: before spawning the child process, it searches PATH with PATHEXT-aware extension matching. If resolution fails — for example because PATHEXT is not set in a stripped-down CI environment — ocx logs a warning and falls back to the bare command name, letting the OS attempt its own lookup.

PATHEXT must be set

In environments with a minimal set of environment variables (containers, CI runners, custom shells), PATHEXT may not be present. Without it, ocx cannot resolve .bat or .cmd scripts by name. If you see Could not resolve 'bun' via PATH, ensure PATHEXT is set — the default Windows value is .COM;.EXE;.BAT;.CMD;.VBS;.VBE;.JS;.JSE;.WSF;.WSH;.MSC.