Authoring
You publish to OCX in three layers: build a deterministic archive, attach metadata declaring how OCX should expose its environment, and push the result to any OCI registry. Every step in that chain has its own page in this section. This overview frames the publisher journey end-to-end and tells you which page to open for each decision you face.
TL;DR — Publish a Binary
You have a compiled binary, a Python script, or any other executable, and you want consumers to fetch and run it with ocx package install. The shortest path is three steps: lay the binary out under a bin/ directory, drop a sibling metadata.json that adds bin/ to PATH, then bundle and push.
Lay out the working directory so the bundle contents live under build/ and the metadata sidecar sits next to it. The sidecar is not bundled into the archive — ocx package push uploads it as the OCI manifest config blob, and OCX restores it as a sibling of content/ in the assembled package directory at install time:
- 📂
mytool/workdir (cwd for the commands below)- 📂
build/bundle contents — becomes content/ after install- ⚙️
bin/- 📄
mytoolyour binary, Python script, shell wrapper, …
- 📋
metadata.jsonOCI config sidecar — not part of the archive
The sidecar declares one public env entry that prepends the package's bin/ to PATH:
{
"$schema": "https://ocx.sh/schemas/metadata/v1.json",
"type": "bundle",
"version": 1,
"env": [
{
"key": "PATH",
"type": "path",
"value": "${installPath}/bin",
"visibility": "public"
}
]
}Bundle build/ into a deterministic archive, then push it to any registry you can authenticate against. The push attaches metadata.json as the manifest config blob — there is no copy inside the archive:
ocx package create build -m metadata.json -o mytool-1.0.0.tar.xz
ocx package push -n -p linux/amd64 -m metadata.json \
ghcr.io/me/mytool:1.0.0 mytool-1.0.0.tar.xzConsumers now reach it with ocx package install ghcr.io/me/mytool:1.0.0 and the binary lands on their PATH. Everything below this section refines that flow: deterministic bundling, dependency edges, env visibility, named launchers, rolling tags, multi-platform manifests, and patterns for wrapping third-party tools you do not own.
Scripted runtimes
If your "binary" is a Python script, a JAR, or anything that needs an interpreter, the script's shebang or wrapper still works — but the cleaner path is to declare the runtime as a dependency and ship a named entry point that resolves the interpreter from that dep. Skip the ambient python3 lookup; pin the version your tool expects.
The Publisher Journey
Most packages start as an upstream binary release on GitHub or a vendor's download page. The work between "I have a binary" and "consumers can ocx package install it" splits into seven decisions:
- Bundle anatomy — what goes in the archive, whether to repack the upstream layout, which compression to pick. Reach for this when you have a directory and need to turn it into a
.tar.xzready forocx package push. - Declaring dependencies — when to depend on another OCX package instead of bundling its bytes, how to pin by digest, what visibility to choose for the dependency edge. Reach for this when your tool needs to find another tool on disk at runtime.
- Env surface — which env vars to declare, how to mark each one (
private/public/interface), when last-wins matters, how to migrate packages published before the entry-points release. Reach for this when designing the contract between you and consumers — and when retrofitting oldermetadata.jsonfiles. - Entry points — when to ship named launchers instead of exposing
bin/on PATH, how to pick non-colliding names, how the launcher resolves each name against the composedPATHfrom the package'senvblock. Reach for this when your tool depends on a shared runtime (Python, Node, JVM) and needs its dep graph encapsulated so two installed tools cannot fight over a single ambient interpreter. - Building and pushing — first push,
--cascadefor rolling tags, BYO archives, cross-package layer reuse by digest. Reach for this when ready to publish. - Testing locally — verify a package works before pushing: run the same install pipeline in a temp directory, exec a command in the composed env, inspect the layout with
--keep. No registry round-trip, no new digest. - Multi-platform packages — pushing per platform under one tag, how OCX assembles the OCI Image Index. Reach for this when supporting more than one OS/arch.
- Migration patterns —
ocx_mirrorspecs, repackaging Homebrew and GitHub Releases, attaching description metadata. Reach for this when wrapping an upstream tool you do not own.
Decision Flowchart
Common questions you will hit while authoring, and the page that answers each:
| Question | Page |
|---|---|
Should I repack the upstream archive or use strip_components? | Bundle anatomy → strip components |
| Which env vars should consumers see vs. only my launchers? | Env surface → choosing visibility |
When do I need named entry points instead of PATH += bin/? | Entry points → when to declare |
When do I need a name override on a dependency? | Dependencies → name override |
| How do I verify a package works before pushing it to the registry? | Testing locally |
How do I make rolling tags (1.0, 1, latest) follow new releases? | Building & pushing → cascade |
| How do I publish for amd64 + arm64 + darwin under one tag? | Multi-platform packages |
| How do I avoid re-uploading a base layer that's already in the registry? | Building & pushing → layer reuse |
| How do I add a README or logo to a published package? | Migration patterns → describe |
How do I retrofit an older metadata.json for entry visibility? | Env surface → migrating |
See Also
- Metadata reference — every field, every constraint
- Command-line reference — package commands —
create,push,pull,test,describe,info - Storage in depth — the three-tier CAS that makes layer reuse work
- Environments in depth — composition order, edge filter, conflicting constants
- User guide — the consumer side of every decision documented here