- docs
- Reference
- Execution Records
Execution Records
The record OCX writes immediately before it starts a binary, and the [records] sink that receives it. How recording is switched on, who may set it and what the fields are for is in Execution records.
Record shape
Section titled “Record shape”A record describes what OCX was about to run and not what happened, so it has no exit code and no duration. It is written on the code path that resolves the environment and finds the target executable, immediately before the child process starts.
{ "schemaVersion": "1", "kind": "sh.ocx.execution-record", "recordedAt": "2026-07-26T14:03:12.006Z", "ocx": { "version": "0.5.0", "binary": "/opt/ocx/bin/ocx" }, "frame": { "command": "package exec", "identity": "complete" }, "process": { "pid": 48124, "user": { "id": "1000", "name": "ci" }, "arch": "amd64", "executable": "/home/ci/.ocx/packages/internal.corp.example/sha256/aa/11bb22cc33dd44ee55ff6677889900aabbccddeeff00112233445566778899/content/bin/solver", "working_directory": "/scratch/job-88213" }, "host": { "name": "batch-node-17" }, "os": { "type": "linux" }, "executable": { "sh.ocx.provenance": "ocx-package", "sh.ocx.kind": "binary", "sh.ocx.package": "pkg:oci/solver@sha256:aa11bb22cc33dd44ee55ff6677889900aabbccddeeff00112233445566778899?arch=amd64&repository_url=internal.corp.example%2Fsolver" }, "scope": { "tier": "package", "cleanEnv": true, "requested": ["internal.corp.example/solver:2024.3"] }, "resolution": { "offline": false, "remote": false, "frozen": false, "requestedPlatform": "linux/amd64+libc.glibc", "registries": ["internal.corp.example"], "mirrors": {}, "autoInstalled": ["internal.corp.example/solver"] }, "packages": [ { "name": "solver", "uri": "pkg:oci/solver@sha256:aa11bb22cc33dd44ee55ff6677889900aabbccddeeff00112233445566778899?arch=amd64&repository_url=internal.corp.example%2Fsolver&tag=2024.3", "digest": { "sha256": "aa11bb22cc33dd44ee55ff6677889900aabbccddeeff00112233445566778899" }, "annotations": { "sh.ocx.role": "root", "sh.ocx.platform": "linux/amd64+libc.glibc", "sh.ocx.visibility": "public", "sh.ocx.resolved-from": "tag" } } ]}| Key | Holds |
|---|---|
schemaVersion, kind, recordedAt | The schema version, the constant sh.ocx.execution-record and the UTC time of the write |
ocx | version and binary of the writing ocx |
frame | command that launched the frame, identity (complete or degraded) and, on a degraded frame, identityNote |
process | pid, user, parent, arch, executable and working_directory of the process that runs the binary |
host, os | host.name and os.type |
executable | sh.ocx.provenance, sh.ocx.kind and sh.ocx.package of the resolved executable |
scope | tier, cleanEnv, and the requested identifiers or the project’s projectRoot, lock and groups |
resolution | How the closure was resolved: see Resolution |
packages | One descriptor per package that composed the child’s environment |
- No completion event: on Unix OCX replaces its own process image with the child (
execvp(2)), so nothing runs afterward that could write one
Process
Section titled “Process”process describes the process that runs the binary, and three of its fields are best-effort. A field that cannot be determined is omitted and never guessed.
process.user.id: from the kernel, cannot be forged by the caller’s environmentprocess.user.name: read from$USER/$LOGNAME, can be forged. Omitted withuserwhen the host has no passwd entryprocess.parent: the pid of the process that launched ocx, when it is identifiable. Omitted for a bare shell invocationprocess.arch: the architecture of the running ocx binary, in OTel’shost.archvocabularyprocess.executable: the resolved executable, as a flat stringprocess.args: absent by design, command-line arguments are never recorded
Executable
Section titled “Executable”executable describes the resolved executable, and sh.ocx.* is the namespace for everything OCX-specific. sh.ocx.provenance and sh.ocx.kind are present whenever the executable lives inside the OCX store, root or dependency alike.
sh.ocx.provenance:ocx-packagefor an executable inside the store, a shim target included.externalwhen the executable was not supplied by ocxsh.ocx.kind:binaryfor the leaf executable,launcherwhen the resolved path is an entrypoint launcher,shimwhen nothing has been downloaded yetsh.ocx.package: the purl of a root package. Omitted when the executable comes from one of that root’s dependencies, whose identity is recorded in full inpackages[]withsh.ocx.role: "dependency"
Resolution
Section titled “Resolution”resolution records how the package closure was resolved. Optional fields follow one rule: state a fact, never assert its absence.
{ "resolution": { "mirrors": { "docker.io": { "registry": "https://mirror.corp.example" }, "ghcr.io": { "index": "https://index-mirror.corp.example" } } }}offline,remote: whether--offlineand--remotewere in forcerequestedPlatform: what OCX asked resolution to find, in its ownos/arch[+libc.*]grammar. The request and not the outcomeregistries: the content registries the frame’s resolved packages carry, not the index endpoint a version was looked up through. A frame that fetched nothing names a host only when this machine’s index copy holds its routing pointermirrors: per-host map keyed by the upstream host a[mirrors]entry rewrites.registrycovers OCI blob and manifest traffic,indexcovers index-tree lookups, and a host declaring both carries both keysmirrorsshape: only rewritten hosts appear.{}when[mirrors]is not configured. Omitted at the launcher frame. Anyuser:password@userinfo is stripped from both endpointsinsecureRegistries: the hosts ofregistriesdeclared plain-HTTP for this resolution, through aninsecure = trueentry orOCX_INSECURE_REGISTRIES. Absent when none werefrozen: mirrors--frozen, which freezes package-tier resolution only.truemeans the package closure could not have added a new pin during this run. The patch tier is frozen separately byocx patch freezeandOCX_PATCH_SNAPSHOTnoVerify:truewhenOCX_NO_VERIFYdisabled the install-time signature gate for this invocation. Env-tier only, a posture and never an outcome.falsesays nothing disabled the gate. Whether a package triggered it is an install-time fact this record does not carry. The per-command--no-verifyflag is never recordedmanagedConfig: the source of the managed-config tier in force and its applied digest when known. Absent when no managed tier appliesautoInstalled: the packages this invocation materialized on the spot. Depends on cache state, so the same command reports it only the first timepatchSnapshot: see Patch companions
scope.tier is package for an OCI-tier command, project for a project-tier ocx exec and launcher for a launcher re-entry, which carries no other scope field. A project-tier record also carries lock and the ocx.toml binding and group each package was selected under.
{ "scope": { "tier": "project", "cleanEnv": false, "projectRoot": "/scratch/job-88213", "lock": { "path": "/scratch/job-88213/ocx.lock", "declarationDigest": { "sha256": "9c1f0b3a77d2e4518ab6c0f92d3e7a41b8c5d6e0f1a2b3c4d5e6f708192a3b4c" } }, "groups": ["default"] }}lock.declarationDigest: hashes theocx.tomldeclarations the lock was generated from and not the lock file’s contents. It answers “was this run driven by theocx.tomlI expect” and never “what actually ran”sh.ocx.binding: theocx.tomlbinding the package was selected under, on package annotations onlysh.ocx.group: the[group.<name>]that supplied it,defaultfor the top-level[tools]table
Packages
Section titled “Packages”packages[] is field-for-field an in-toto ResourceDescriptor (name, uri, digest, annotations), so it drops into an SLSA resolvedDependencies list without reshaping.
{ "name": "cmake", "uri": "pkg:oci/cmake@sha256:3f7a2b9c5d1e8f04a6b3c7d2e9f1a5b8c4d6e0f2a3b7c9d1e5f8a0b2c4d6e8f0?arch=amd64&repository_url=index.ocx.sh%2Focx%2Fcmake", "digest": { "sha256": "3f7a2b9c5d1e8f04a6b3c7d2e9f1a5b8c4d6e0f2a3b7c9d1e5f8a0b2c4d6e8f0" }, "annotations": { "sh.ocx.role": "root", "sh.ocx.binding": "cmake", "sh.ocx.group": "default", "sh.ocx.platform": "linux/amd64+libc.glibc", "sh.ocx.visibility": "public", "sh.ocx.entrypoints": ["cmake", "ctest", "cpack"] }}uri: a purl of typepkg:ociwhose version is the sha256 digest. Atagqualifier appears only when one was resolveddigest: a bare lowercase-hex map keyed by algorithm, never asha256:-prefixed string. The manifest digest is the join key toocx package sbomandocx package verify --attestationsh.ocx.role:root,dependencyorcompanionsh.ocx.platform: roots only, the platform the manifest leaf actually resolved to. A flat single-image package selectsanyunder any request. A dependency’s descriptor anduricarry no platform and no arch qualifiersh.ocx.visibility: the surface the package was composed onsh.ocx.resolved-from:tagwhen the digest was reached through a tag rather than named directly. Absent for a digest-pinned identifier, a lock-resolved package and alauncher shimframesh.ocx.binaries,sh.ocx.entrypoints: arrays of strings, one per binary or entry point admitted ontoPATH, present only when the package declares at least one. These are the same claimocx package env’s arrays exposesh.ocx.composition:deferredon a package that composed but never materializedsh.ocx.identity:syntheticon a degraded frame, which also carries nouri
Where field names come from
Section titled “Where field names come from”Every borrowed field name is pinned by this page and not by the standard it came from. An upstream rename therefore does not change what OCX emits.
| Record part | Source | Notes |
|---|---|---|
packages[] shape (name, uri, digest, annotations) | in-toto ResourceDescriptor | Sigstore, Tekton Chains and GUAC all consume in-toto shapes |
packages[].uri | purl, pkg:oci type | Version is the sha256 digest; tag is advisory, matching OCX’s digest-is-identity model |
process.pid, process.working_directory, host.name | ECS and OTel agree | Both standards name these fields the same way |
os.type | OTel | OTel’s top-level attribute, member spelling darwin/linux/windows/…; ECS nests the field under host.os.type and expects macos, not darwin |
process.parent.pid, process.user.name, process.executable | ECS | ECS types process.executable as a flat string; OTel’s object form would collide with the sh.ocx.* fields nested under it |
process.arch | OTel | An OCX extension inside ECS’s process block, which has no process-level architecture field, carrying OTel’s host.arch value vocabulary (amd64, arm32, arm64, …) |
schemaVersion, recordedAt, frame, scope, resolution | OCX | No existing standard describes “a CLI invoked a binary with these packages” |
sh.ocx.* annotations (role, binding, group, platform, visibility, kind, provenance, resolved-from, binaries, entrypoints, composition, identity) | OCX | Everything OCX-specific lives in this namespace so the standard-shaped fields stay standard-shaped |
Frames
Section titled “Frames”An entrypoint launcher re-enters OCX, so one invocation through a launcher writes two records, one per frame. Both carry the same package content digest, which is the join key.
Outer frame (exec / package exec) | Inner frame (launcher exec) | |
|---|---|---|
frame.command | exec or package exec | launcher exec |
frame.identity | complete | complete when the parent forwarded the package name, otherwise degraded |
executable["sh.ocx.kind"] | launcher — the resolved path is the launcher shim | binary — the actual leaf executable |
packages[0].digest.sha256 | same content digest as the inner frame | same content digest as the outer frame |
- Sink:
ocx launcher exectakes no--records-*flags and inherits the sink through the forwardedOCX_RECORDS_DIRandOCX_RECORDS_NAME
See Two records per launcher invocation for how the frames join and count. Launcher frames in records covers degraded frames and why previews write no record.
Shim frame
Section titled “Shim frame”A package composed with lazy-mode = always puts a generated shim on PATH, and the hidden ocx launcher shim frame is the only place its content is downloaded. The first invocation of a deferred package therefore writes two records.
Outer frame (exec / package exec) | Shim frame (launcher shim) | |
|---|---|---|
frame.identity | complete | complete, always, unlike launcher exec |
executable["sh.ocx.kind"] | shim — nothing has downloaded yet | binary — the now-materialized leaf executable |
packages[].annotations["sh.ocx.composition"] | deferred, on the package that composed but never materialized | absent — the package is real by the time this frame writes |
resolution.autoInstalled | absent for this package — the outer frame only composed it | present — this frame performed the pull |
Patch companions
Section titled “Patch companions”A patch companion appears in packages[] as an ordinary entry with sh.ocx.role: "companion", beside "root" and "dependency". resolution.patchSnapshot names the patch snapshot pinned for the invocation when one is in force.
{ "resolution": { "patchSnapshot": { "sha256": "e2f4a6b8c0d1e3f5a7b9c1d3e5f7092c4d6e8f0a2b4c6d8e0f2a4c6e8f0a2c4e" } }}sh.ocx.visibility: the surface the companion was composed on.privateunder--selfwhen it matched only roots.interfacewhen it matched only dependencies or the view is a consumer’s.publicunder--selfwhen it matched both a root and a dependency- Dependencies: a companion’s own dependencies are recorded with
sh.ocx.role: "dependency", and a package the roots or an earlier entry already reached keeps its first position and role patchSnapshot: absent, never null or empty, when no snapshot is in force. Set byocx patch freezeorOCX_PATCH_SNAPSHOT
The sink
Section titled “The sink”The sink is always a directory. OCX writes one self-named file per record into it with a create-exclusive temp file and a no-clobber rename.
- Collision: a naming collision retries under a fresh random filename and never overwrites an existing record
- Creation: OCX does not create the directory.
dirmust already exist and be writable. A missing directory fails like a permissions problem, with an I/O error naming the path - Durability: publication is atomic but not synced to disk, so a power loss can lose the most recent records
- Network sinks: a collector on another client can briefly observe a short, still-being-written file. It should retry before treating a JSON parse failure as corruption
- Mode: each file is owner-only (
0600on Unix), so a collector under a different account cannot read it without explicit access - Debris: a crash between publish and cleanup can strand a hidden
.tmp*file. A collector that lists the directory rather than globbing*.jsonmust skip every.tmp*entry - Authenticity: a record is not signed and the sink is not authenticated. On a shared sink a collector should reject a record whose owning uid disagrees with
process.user.idand treatprocess.user.nameas display only - Symlink pin: the configured sink is resolved to its real location once per invocation and re-checked before every write. A sink that no longer resolves to itself refuses the write with the configured failure posture. The posture is a warning by default or exit
78underrequired = true, classified as a configuration fault - Limits of the pin: it compares a canonical path and not directory identity. It is not shared across the outer and inner frame of an entrypoint invocation. It is an integrity control and not a security boundary
- Paths: records contain absolute paths, including the invoking user’s home directory (
process.executableand the packagecontent/andentrypoints/paths), so size sink permissions and retention accordingly
Filename grammar
Section titled “Filename grammar”The default filename is <utc-basic-ms>-<pid>-<8 hex random>.json, which sorts lexicographically in chronological order. A name template over a closed placeholder set overrides it.
| Placeholder | Expands to |
|---|---|
{time} | 20260726T140311482Z — same basic UTC form as the default |
{pid} | the process ID of the process that runs the binary |
{rand} | 8 hex characters |
{host} | hostname, reduced to a filename-safe form |
{host}reduction: keeps onlyA-Z a-z 0-9 . _ -and replaces everything else with_. A hostname that reduces to.or..expands to nothing, exactly as an undeterminable hostname does- No
{command}placeholder: the invoked binary is already in the record asprocess.executable - Errors: an unknown placeholder is a configuration error at resolve time. So is a template that renders to anything other than a single plain filename, such as one carrying a path separator
Configuring [records]
Section titled “Configuring [records]”Recording is off by default: absent a sink directory at every layer, OCX writes nothing and adds no I/O to the exec path.
[records]dir = "/var/log/ocx/records"name = "{time}-{host}-{pid}.json"required = true| Key | Flag | Variable | Notes |
|---|---|---|---|
dir | --records-dir | OCX_RECORDS_DIR | The sink directory |
name | --records-name | OCX_RECORDS_NAME | The filename template; no effect unless a sink is active |
required | none | none | Config-file-only at every tier |
- Precedence:
[records]config, then the variables, then the flags onocx execorocx package exec. The highest layer wins per field - Empty variable: reads as unset, so a profile-exported value can be disabled for one invocation
- Forwarding: both variables are resolution-affecting and forwarded to every subprocess
- SYSTEM lock: a SYSTEM-scope
[records]declaration locks the whole block,dir,nameandrequiredtogether, and the variables and flags have no effect once it is locked - Lock survival: the lock survives
OCX_NO_CONFIG=1, and an unreadable/etc/ocx/config.tomlaborts the invocation with exit78. An absent system file is silent - Relative
dirin a config file: anchored at the declaring file’s directory, like[trust.sigstore] trusted_root.--records-dirandOCX_RECORDS_DIRresolve against the process’s working directory - Project tier: a project
ocx.tomlcannot set[records]. The section is stripped - Managed tier: an operator-published
[managed]configuration can setrequired = truein its[records]payload - Placeholder error: an unrecognized placeholder in
nameis a configuration error (exit78) at resolve time requiredwithoutdir: a configuration error (exit78), except a SYSTEM-scope[records]block with neitherdirnorrequired, which locks recording off for the host
required | Default | Behavior when a record cannot be written |
|---|---|---|
true | when a SYSTEM-scope (/etc/ocx/config.toml) [records] policy is present | The invocation aborts — exit 74 for an unwritable sink, exit 78 when the sink resolves through a symlink. |
false | when no SYSTEM-scope policy is present | A warning prints to stderr; the child still runs. |
- Exit codes:
requireddecides only whether OCX aborts or warns, never which code an abort uses - Windows: the sink is probed before the child is spawned and the record written after, so
required = truecan abort after the child has begun work. OCX stops and reaps that child
See Failure posture in detail and [records] in the Configuration reference.
Schema
Section titled “Schema”The published schema lives at https://ocx.sh/schemas/execution-record/v1.json and moves in lockstep with the in-band schemaVersion string inside every record. schemaVersion changes only for a backward-incompatible break, and new fields are additive.
- Consumers: a consumer written against
schemaVersion: "1"must tolerate unknown keys and not reject them - Breaking change: bumps the schema version and
schemaVersiontogether, never one without the other