out-of-band
Signed receipts for CI checks that GitHub cannot run, or that already ran somewhere else.
Two reasons to sign a receipt
GitHub cannot run the job. The code needs a particular accelerator, a licensed toolchain, a database reachable only from the office network, or a lab box several people share. A hosted runner cannot get there.
The job already ran somewhere else. Re-running it in CI is slow, flaky, or returns a different number. You want the gate to accept the run you have.
Both cases leave the same gap: a required check that no workflow can produce honestly. Out-of-band closes it with a statement the machine can sign without a GitHub credential and without an inbound path.
Putting a self-hosted runner on the machine is the usual alternative. It means a long-lived daemon on shared hardware, a credential that can pick up jobs, and repository code executing on the box. Uploading logs leaves the decision to someone reading a browser tab. Skipping the check turns the gate into a suggestion.
How it works
operator host GitHub
───────────── ──────
run ──► evidence ──► sign PR comment / dispatch
│ │
└── signed block ────┴──► verify.yml
│
├── check run
├── step summary
└── sticky comment
- The operator runs the job where it has to run and signs one statement: this repository, this commit, this tree, this job, this result, this evidence digest.
- The statement travels as a pull request comment, a dispatch input, or a file.
- A workflow on GitHub verifies the signature against
.github/allowed_signerson the default branch, checks the bindings, and publishes a check run.
The repository owns the mechanism: format, signing, verification, publishing. The consumer owns the policy: which paths need a receipt, the job name, which key, and whether the check is required.
Four states, never collapsed
| Receipt | Verifier state | Check conclusion |
|---|---|---|
valid, result: pass |
verified |
success |
valid, result: fail |
failed |
failure |
| absent or invalid | unverified |
failure |
| not required by policy | not applicable |
success |
unverified means the gate could not tell. It publishes a failure, so a
required check stays closed, and the summary carries the error code. A missing
receipt is never a skip.
Quickstart
Requires Python 3.10+ and OpenSSH 8.0+ (ssh-keygen -Y sign). No runtime
dependencies.
pip install out-of-band
Generate a key on the machine that runs the job. It never leaves that machine.
ssh-keygen -t ed25519 -N "" -C "ci operator" -f ~/.ssh/out-of-band
Commit the public key in the consumer repository as .github/allowed_signers:
operator ssh-ed25519 AAAA...
Sign one commit. --commit must be a full SHA that exists in the local clone;
the tree is read from the commit, not the worktree.
oob sign \
--job smoke \
--commit "$(git rev-parse HEAD)" \
--result pass \
--evidence-dir ./evidence \
--key ~/.ssh/out-of-band \
--repo owner/name \
--out receipt.md
That writes one block: an HTML marker, one line of JSON, and an SSH signature.
Post it as a pull request comment, or pass it to a workflow_dispatch input.
For a readable comment, run oob render --block receipt.md first.
Verify locally before posting:
oob verify --block receipt.md \
--allowed-signers .github/allowed_signers \
--repo owner/name --commit "$(git rev-parse HEAD)" --expect-job smoke
Exit codes: 0 valid pass, 1 valid fail, 2 absent or invalid, 3 tool
error.
Use it in a repository
Two carriers, one verifier. The action runs as a step; the reusable workflow runs as its own job.
verified:
needs: policy
runs-on: ubuntu-latest
permissions:
contents: read
checks: write
pull-requests: write
steps:
- uses: Oaklight/out-of-band@<full-sha> # v0.5.0
with:
pr: ${{ needs.policy.outputs.pr }}
commit: ${{ needs.policy.outputs.commit }}
job: smoke
required: ${{ needs.policy.outputs.requires }}
check-name: smoke-verified
trusted-authors: operator-login
The snippet shows the gate wiring. Evidence is required by default, so add
evidence-url or evidence-file, or set require-evidence: false.
Then require smoke-verified in branch protection. The check name is public
API: renaming it silently changes what the branch protects.
Pin uses: by full commit SHA. A tag can move.
Evidence
Evidence is required by default. Supply the archive in one of three ways:
| Source | Use when |
|---|---|
evidence-file |
the archive is already on the runner |
evidence-url |
the archive is at an HTTPS address |
claims.evidence_url |
the receipt should carry its own location |
oob pack --evidence-dir ./evidence --out evidence.tgz
The verifier extracts the archive, recomputes the digest, and compares it with
the value that was signed. A mismatch is E_EVIDENCE_MISMATCH and the verdict
is unverified. A matching archive is uploaded as the oob-evidence artifact,
and the job summary shows a manifest plus bounded text previews.
Set require-evidence: false to accept the signed digest without an archive.
The GitHub token is sent only to api.github.com and github.com. Other hosts
must serve a public or signed URL.
What a receipt proves
That the holder of a listed key signed these exact bytes, binding one repository, commit, tree, job, and evidence digest.
It does not prove that the job ran correctly, that the evidence matches the digest, or that the host was uncompromised. It is an attestation, not a proof of correctness. See Trust and SPEC.md §8.
Documentation
Start with the docs map. The short version:
| If you want to | Read |
|---|---|
| Install and sign the first receipt | Get started |
| Understand receipts, trust, evidence, states | Concepts |
| Look up a command or an input | CLI, action, workflow |
| Follow an end-to-end setup | Patch gate, hardware run |
| Run it on an air-gapped host | Air-gapped hosts |
| Compare with other tools | Prior art |
Chinese documentation mirrors the English tree under docs/zh/.
Install
pip install out-of-band
Each release also carries oob-<version>.pyz, a single-file build that needs
only Python. It is the usual choice for a host that cannot reach PyPI.
Python 3.10 through 3.14 are tested. Windows works with python oob-<version>.pyz.
Project
- Security policy
- Contributing
- Changelog
- Examples
- Normative specification
- License: Apache-2.0
Non-goals
No runner, no hosted service, no GitHub App, no Sigstore, no transparency log, no artifact storage. The tool signs and verifies a statement; everything else belongs to the repository that uses it.
Metadata
Release files for out-of-band 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| out_of_band-0.5.0.tar.gz | 36.1 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| out_of_band-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 66.4 kB
Release files / out_of_band-0.5.0.tar.gz
| Download URL | out_of_band-0.5.0.tar.gz |
|---|---|
| Size | 36.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5a9d46a09fff2d40a2d94738b212c02a6f63e0df3bb5bda928fd8fe520dbfc48
|
|
BLAKE2b-256 checksum How to use checksums |
be49eeaec3cc56aec160f9f01b9e8e03d74403a0700062241c6f645ea4e22492
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logRelease files / out_of_band-0.5.0-py3-none-any.whl
| Download URL | out_of_band-0.5.0-py3-none-any.whl |
|---|---|
| Size | 30.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dd702abfdd7c573027c0af7ee9aa19ae43506264b3f04fac7b49bec74fd67331
|
|
BLAKE2b-256 checksum How to use checksums |
026d94425544c157aaf4ca257bb81e6766e5e59177aa0932d6e0789943a8ed20
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logSecurity for out-of-band 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for out_of_band-0.5.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.