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.

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.