Canonical source: docs/spec/packslip.md. Schemas: release/v1 and releases/v1. For examples, start with the guides.

packslip: a signed release manifest

Version 1. Predicate types https://packslip.dev/release/v1 and https://packslip.dev/releases/v1.

Authors: Jeff Dickey (@jdx) and Shunsuke Suzuki (@suzuki-shunsuke).

Purpose and scope

A publisher describes a release once, in a signed document that any consumer can verify against a trusted identity or key. The document lists artifacts and their digests, platforms, executable paths, resources, and provenance links. Artifacts may be archives, installers, bare executables, source tarballs, or other release files.

The release manifest lets consumers interpret the release without a per-vendor file name recipe or registry entry. It does not prescribe a package manager, download host, or installation directory.

packslip uses an in-toto statement inside a sigstore bundle. The statement’s predicate carries the release metadata; the bundle carries the signature and verification material. This specification defines two predicate types:

PredicateDescribes
https://packslip.dev/release/v1One release and its files.
https://packslip.dev/releases/v1A release index with mutable discovery metadata.

Reading this specification

This document defines the format and the rules for consumers that verify or install releases. For a working example or CLI instructions, start with the guides. Implementers should read the consumer rules alongside the field definitions: valid JSON alone is not enough to accept a release.

The following terms distinguish the documents and the parties using them:

TermMeaning
VendorThe project whose releases a packslip describes. It normally builds and signs its own releases.
ConsumerAnything that verifies or installs releases, such as a package manager, install script, or verification tool.
Packslip, or release manifestThe sigstore bundle published with a release.
StatementThe in-toto statement inside a bundle.
Release list, or signed listA bundle whose statement has the releases/v1 predicate.
RepackagerA third party that signs a packslip for a vendor’s files; see Repackager attestation.

Use these sections according to the task:

JSON examples abbreviate digests and commits for readability. Those placeholders are not valid release data.

JSON schemas

The schemas describe the decoded in-toto statements inside bundles:

DocumentDownloadCLI
One release (release/v1)release-v1.jsonpackslip schema
A release list (releases/v1)releases-v1.jsonpackslip schema --releases

Use them to check field shapes, required values, and value patterns. They do not validate the enclosing sigstore bundle, authenticate its signer, or apply rules that require comparing fields, downloaded bytes, or remembered state. The reference implementation validates those relationships; consumers also apply the consumer rules.

To inspect a bundle’s statement, use packslip show packslip.sigstore.json. That command decodes the payload without verifying it. See Verify a release for an authenticated check, and the CLI overview to install packslip.

Project names

A project name is a host with an optional path, such as github.com/jdx/mise, gitlab.com/group/tool, or mise.jdx.dev. Its syntax is:

A consumer may offer a short-name alias such as mise for github.com/jdx/mise. That alias is a convenience outside the format.

The name is the location and, on a forge, says who may sign:

For a known forge, a consumer derives its initial signer policy from the project the user intends to install. Deriving it from an untrusted statement would accept whichever repository the statement’s author names, without checking the user’s intended identity.

After verification, the consumer must also check that the statement names the requested project, or the same repository under another name as Repository identity and renames allows. A forge name can change hands: the name locates the project, while the forge’s repository ID pins its identity.

Monorepos

On github.com, a repository that releases several tools names each one with a subpath: github.com/oxc-project/oxc/oxlint, github.com/bazelbuild/buildtools/buildifier, github.com/biomejs/biome/crates/cli. On gitlab.com the whole path names the repository, as Project names says, so a GitLab project has no monorepo subpath.

Each tool gets its own packslip per release, with its own version, and source.tag carries the real tag (oxlint_v1.0.0, cli/v1.9.4), so consumers do not have to infer the tag from the version. The identity pin is still the repository: any workflow of oxc-project/oxc may sign a packslip for oxc-project/oxc/oxlint.

When several tools share one GitHub release, each ships its own bundle, named packslip.<subpath>.sigstore.json with / in the subpath replaced by -:

Project within the repositoryBundle name
The repository itselfpackslip.sigstore.json
oxlintpackslip.oxlint.sigstore.json
crates/clipackslip.crates-cli.sigstore.json

Consumers select by the signed statement, not by trusting the file name. They read the release’s packslip*.sigstore.json assets and keep the one whose predicateType is release/v1 and whose project matches the requested name, or the same tool in the renamed repository. A tool keeps its subpath across a rename: github.com/old/repo/tool becomes github.com/new/repo/tool, never another tool in the repository.

Bundle format

A release ships one sigstore bundle (v0.3) per project. Each bundle contains:

The log entry may be omitted only for a key-signed release made without network access; see Signing. The signature covers the DSSE payload bytes. Consumers do not canonicalize the JSON before verification. General-purpose sigstore tools can read the bundle, but consumers must also validate the packslip predicate and apply this specification’s rules.

To inspect the payload without verifying it:

packslip show packslip.sigstore.json       # Decode and pretty-print the statement.
packslip show --raw packslip.sigstore.json # Print the signed payload, then a newline.

The payload can also be decoded with jq -r .dsseEnvelope.payload BUNDLE | base64 -d.

Signing

A packslip is signed under one of two schemes, named in identity.scheme. Both produce the same bundle format. Prefer keyless signing when a supported CI identity is available.

SchemeSigning credentialConsumer’s trust anchor
sigstore-oidcA CI job’s OIDC identity.The expected identity and issuer, optionally pinned by repository fingerprint.
sigstore-keyA long-lived Ed25519 key.The public key.

Keyless signing

With sigstore-oidc, a CI job with id-token permission signs under its own identity. Fulcio issues a short-lived certificate naming the workflow that ran, and Rekor logs the signature. There is no key for the vendor to manage.

packslip create uses this scheme unless given --key. It takes the token from SIGSTORE_ID_TOKEN when set, otherwise from the CI job’s ambient credential. Supported ambient credentials include GitHub Actions, GitLab CI, and the other providers sigstore’s clients detect. The command fails if neither token source is available.

Key signing

With sigstore-key, the vendor signs with a long-lived Ed25519 key. This scheme suits vendors who release outside a CI system with OIDC, or who want consumers to pin a stable key.

packslip keygen writes the secret key as a hex seed and the public key in minisign’s public-key format. Consumers pin that public-key file, never the hint carried by the bundle. The bundle also carries the Rekor entry, whose verifier is the public key.

A key-signed bundle also supports a dependency-free signature check: its DSSE signature is a raw Ed25519 signature over the pre-authentication encoding of the payload, with payload type application/vnd.in-toto+json. That signature check does not replace verification of the Rekor entry required by consumer rule 2, unless the consumer chose to accept an unlogged bundle.

Unlogged bundles

A key-signed bundle may be produced without a log entry (packslip create --no-log) for an air-gapped release. A consumer refuses such a bundle unless it explicitly allows unlogged bundles (packslip verify --allow-unlogged in the reference CLI), and a repository should record that choice per vendor.

Workflow pinning

By default, a consumer of a keylessly signed project compares the signing workflow with the workflow of the last release it accepted. A different workflow in the same repository is a changed signer: the consumer refuses the release until a person approves it (consumer rule 3).

This default requires approval each time a vendor switches between release workflows, including when several jobs share a reusable workflow. A vendor can instead declare in its signed identity that consumers should pin the repository while allowing its signing workflow to change:

"identity": {
  "scheme": "sigstore-oidc",
  "key_id": "https://github.com/example/tool/.github/workflows/release.yml@refs/tags/v1.2.0",
  "issuer": "https://token.actions.githubusercontent.com",
  "pin_workflow": false
}

pin_workflow defaults to true. Omitting it therefore pins the signing workflow. The vendor declares this field in a signed release or release list. packslip create --no-pin-workflow and packslip releases --no-pin-workflow write false; both refuse the flag with --key. The field has no meaning for sigstore-key, which has no workflow.

With pin_workflow: false, a consumer still requires everything else of the signer: the issuer, a certificate whose repository is the one pinned (by repository ID where the forge records one, as Repository identity and renames describes), and a workflow of that repository. It stops comparing the workflow’s path with the last accepted signer.

The consumer remembers pin_workflow beside the signer. Disabling a previously pinned workflow reduces remembered trust and requires approval, as consumer rule 3 defines:

Remembered valueIncoming valueConsumer behavior
No remembered signertrue or omittedPin the signing workflow; there is no remembered trust to reduce.
No remembered signerfalsePin the repository without pinning the workflow; there is no remembered trust to reduce.
truetrue or omittedCompare the workflow with the last accepted signer; a change requires approval.
truefalseRefuse until a person approves the reduction. Then remember false, without asking again for that reduction.
falsefalseContinue without comparing workflow paths.
falsetrue or omittedResume comparison with the last accepted signer’s workflow. This is not a reduction, but a different workflow is a changed signer and requires approval.

A consumer written before this field existed ignores it, as it ignores any field it does not know, and keeps holding the vendor to its workflow. Those consumers therefore still refuse a release from a changed workflow until a person approves it, even when the vendor sets the field.

Repository identity and renames

A repository can be renamed or moved to another owner, and the forge redirects the old name to the new one: github.com/jdx/rtx became github.com/jdx/mise, and GitHub answers for the first with the second. Once the old name is free, anyone can create another repository under it, including a workflow at the same path. The name alone cannot distinguish the original repository from its replacement. The forge’s repository ID can: it survives renames and transfers, never changes, and is never reused.

Fulcio records the IDs in every certificate it issues to a GitHub Actions or GitLab CI job, from the job’s OIDC token:

ExtensionOIDGitHub claimGitLab claim
Source Repository URI1.3.6.1.4.1.57264.1.12server URL + repositoryserver URL + project_path
Source Repository Identifier1.3.6.1.4.1.57264.1.15repository_idproject_id
Source Repository Owner URI1.3.6.1.4.1.57264.1.16server URL + repository_ownerserver URL + namespace_path
Source Repository Owner Identifier1.3.6.1.4.1.57264.1.17repository_owner_idnamespace_id

Each extension value is a DER UTF8String. An empty value means the forge supplied none. For these IDs to authenticate a packslip, the Source Repository URI must identify the repository named by project. That URI names the repository whose run requested the certificate, whichever workflow signed, so the signer may be a reusable workflow of another repository. Only a certificate that records no Source Repository URI falls back to requiring the signer to be a workflow of the named repository.

The owner extensions record who owned the repository when the certificate was issued. They are not part of the project’s identity: consumers neither remember nor compare them. Only the current owner can transfer a repository, and that owner already controls what it signs.

A consumer remembers the repository ID of the first release it accepts, with its signer (see Consumer rules), and compares each later release with it:

CaseStatement and certificateConsumer behavior
Same projectThe statement names the requested project, and the certificate carries the pinned repository ID.Accept it.
Renamed or transferredThe statement names another repository on the same forge, with the same monorepo subpath, and the certificate carries the pinned repository ID.Accept it and report the name it was signed under.
Different repositoryThe certificate carries a repository ID other than the pinned one.Refuse it, even if the statement names the requested project.
No ID to compareThe certificate or remembered pin lacks a repository ID, as with certificates issued before Fulcio recorded them.Check by name alone. Refuse a statement for another name when either side has no ID to compare, as with no pin.

A rename or transfer can be checked in either direction: the user may ask for the old name while verifying a release signed after the move, or ask for the new name while verifying a release signed before it. A replacement repository under the old name has a different ID and is refused.

Signer continuity compares the workflow’s path inside the repository. For example, .github/workflows/release.yml in jdx/rtx continues as the same workflow in jdx/mise; the same path in jdx/hk continues in acme/hk after a transfer.

A pin taken from an accepted release records its signed project name and repository ID. Consumers reading pins that also store an owner ID, as an earlier revision of this section required, ignore that owner ID.

A consumer may hold several pins for a project, such as its own record and a lockfile’s commitment. Each release must match every pin; a refusal says which pin the release disagreed with.

With no pin, the consumer has only the forge’s word, and first use is trust on first use. GitHub’s GET /repos/<owner>/<repo> redirects a renamed or transferred repository’s old name to the repository and returns its id, and a consumer may take that as the expected repository ID. If the name was recreated before the consumer first saw it, the forge answers for the new repository, and the release cannot reveal that the name previously belonged to another repository.

Two mechanisms limit that first-use exposure:

The reference implementation reads the extensions as sigstore::source_repository, classifies a release into these cases in forge::check, and verifies and classifies a bundle in one call with verify_forge. forge::same_workflow compares two signers a consumer recorded, with the repository ID recorded alongside each, for consumer rule 3 when no release is at hand.

Signer fingerprints

A signer fingerprint identifies the repository and issuer used to sign a keyless project’s releases. It lets a consumer pin that repository before accepting its first release, rather than trusting the first release it finds (see Consumer rules).

A project can publish its fingerprint independently of its releases, for example in its README or install instructions. Users can then record it in a Dockerfile or a consumer’s lockfile. A fingerprint looks like this:

ps1_snirenkjwr7m5ozgcufameodnm

Computing a fingerprint

The fingerprint is ps1_ followed by the first 128 bits of

SHA-256( "packslip-signer-v1" 0x00 issuer 0x00 repository_id )

encoded as 26 characters of RFC 4648 base32, in lowercase and without padding (a–z, 2–7). In the hash input, 0x00 is a single zero byte and the quoted string is its ASCII bytes. The other two inputs come from the certificate, exactly as recorded, with no case or slash normalization:

For GitHub repository jdx/hk, whose ID is 922514152, the input is packslip-signer-v1, a zero byte, https://token.actions.githubusercontent.com, a zero byte, and 922514152, and the fingerprint is ps1_snirenkjwr7m5ozgcufameodnm.

Twenty-six base32 characters hold 130 bits, so the last character carries two padding bits, which are zero. Text whose last character sets either bit is not a fingerprint, so each fingerprint has exactly one spelling.

What the fingerprint pins

A fingerprint commits to the issuer and repository ID. It excludes values that can change while the repository stays the same, and subpaths that distinguish tools within that repository:

Excluded valueConsequence
OwnerA transfer preserves the repository ID and fingerprint. Consumers do not remember or compare owners, as Repository identity and renames defines.
Repository nameA rename preserves the ID, so the fingerprint remains valid.
Workflow and refA replaced workflow or a new release tag does not change the fingerprint. The separate signer-continuity check in Consumer rules still applies.
Monorepo subpathOne fingerprint covers every tool in the repository. The statement’s project name identifies the tool.

When a repository is deleted and someone else creates one under its name, the new repository has a new ID and so a different fingerprint. That is what makes the fingerprint a pin.

A release has no fingerprint when it is signed with a key, when its certificate records no repository ID (because it predates the extension or the issuer does not give one), or when the recorded ID is not a string of decimal digits. A project whose releases have no fingerprint is pinned with an identity and issuer, or a public key, as Consumer rules says.

The fingerprint is a public commitment, not a secret. Its 128 bits make finding another repository with the same fingerprint infeasible, while its text form fits in a README. It carries no authority by itself: the consumer trusts the project that publishes it and the place it is published. A fingerprint does not belong in a packslip, which cannot vouch for its own signer.

Checking a fingerprint

A consumer that holds a fingerprint checks it against a release after verifying the bundle (consumer rule 2), using only what the verified certificate records, never what the statement says of itself:

  1. Read the issuer and the Source Repository Identifier from the certificate. A certificate that records no repository ID matches no fingerprint, and the release is refused.
  2. Compute the fingerprint of the two and compare it with the one it holds. If they differ, the release is from another repository or through another issuer, and it is refused.
  3. Apply Repository identity and renames as for any release: the certificate’s Source Repository URI must be the repository the statement names (or, without one, the signer a workflow of it). The fingerprint says which repository; these say the statement is about it.

The consumer refuses a malformed pin rather than ignoring it or treating it as “no pin”. A valid pin must start with ps1_, have exactly 26 lowercase base32 characters after the prefix, and have zero padding bits in the last character. An unknown prefix, such as ps2_, is refused; the consumer does not guess what it commits to.

The conformance vectors for the fingerprint give inputs, their fingerprints, texts that are not fingerprints, and matches and mismatches. The reference implementation computes a fingerprint with Fingerprint::of_signer and checks it with Fingerprint::verify. The CLI prints a release’s fingerprint with packslip pin and checks a release against a supplied fingerprint with packslip verify --pin.

What a verified packslip proves

A verified packslip authenticates the named signer’s statement about the release. A downloaded file whose digest matches the statement is the file the signer described. A logged signature also has a verified integration time. An explicitly accepted unlogged signature has no such log evidence.

Two kinds of assurance require separate checks:

Resource coverage

The source of a resource determines what authenticates it:

SourceAuthentication
archiveCovered by the archive’s signed digest.
assetCovered by the asset’s signed digest.
repoPinned by the source commit.
execThe consumer verifies the executable and controls when it runs. Its output is not separately signed.

Reference CLI behavior

packslip verify checks the supplied bundle and local files passed with --artifact. It reports signing information, provenance links, resources, and host requirements. It does not fetch provenance, install resources, or maintain trust history across invocations. For a release list, it also does not enforce expiry or compare its sequence with a previously accepted list.

Without --pubkey, --identity, --identity-prefix, or --issuer, the command derives its signer policy from the GitHub or GitLab project the statement claims. As Project names explains, that default does not check the user’s intended identity. Supply the expected public key with --pubkey, or the expected identity or identity prefix together with --issuer. Only supplied identity flags are checked: --issuer alone accepts any signer from that issuer. A supplied --pin adds the repository-fingerprint check to the identity policy.

With any policy, compare the reported project and version with the request. A signer can sign other projects and older versions; packslip verify does not know which one the user asked for. See Verify a release for CLI examples.

The release statement

A release statement is an in-toto statement with predicateType https://packslip.dev/release/v1. Its subject binds file names to digests; its predicate describes the project, version, artifacts, resources, and signer.

This example describes a mise release with one Linux x86_64 archive and a separate SBOM asset. Digest and commit values are abbreviated as ...; an actual statement must supply their full values.

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    { "name": "mise-v2026.9.1-linux-x64.tar.xz",
      "digest": { "sha256": "...", "sha512": "..." } },
    { "name": "mise-v2026.9.1.cdx.json",
      "digest": { "sha256": "..." } }
  ],
  "predicateType": "https://packslip.dev/release/v1",
  "predicate": {
    "project": "github.com/jdx/mise",
    "version": "2026.9.1",
    "published_at": "2026-09-01T12:00:00Z",
    "source": { "repo": "https://github.com/jdx/mise", "commit": "...", "tag": "v2026.9.1" },
    "artifacts": [
      {
        "name": "mise-v2026.9.1-linux-x64.tar.xz",
        "os": "linux", "arch": "x86_64", "libc": "gnu",
        "size": 12345678,
        "url": "https://github.com/jdx/mise/releases/download/v2026.9.1/mise-v2026.9.1-linux-x64.tar.xz",
        "format": "tar.xz",
        "bin": ["mise/bin/mise"],
        "requires": { "glibc_min": "2.31", "libs": [] },
        "provenance": ["https://api.github.com/repos/jdx/mise/attestations/sha256:..."]
      }
    ],
    "resources": [
      { "kind": "completion", "shell": "zsh", "archive": "mise/share/zsh/site-functions/_mise" },
      { "kind": "man", "archive": "mise/man/man1/mise.1" },
      { "kind": "cli-spec", "format": "usage", "bin": "mise", "archive": "mise/share/usage/mise.kdl" },
      { "kind": "skill", "name": "mise", "repo": "skills/mise" },
      { "kind": "sbom", "format": "cyclonedx", "asset": "mise-v2026.9.1.cdx.json",
        "url": "https://github.com/jdx/mise/releases/download/v2026.9.1/mise-v2026.9.1.cdx.json" }
    ],
    "identity": {
      "scheme": "sigstore-oidc",
      "key_id": "https://github.com/jdx/mise/.github/workflows/release.yml@refs/tags/v2026.9.1",
      "issuer": "https://token.actions.githubusercontent.com"
    },
    "notes_url": "https://github.com/jdx/mise/releases/tag/v2026.9.1"
  }
}

Subjects and digests

subject lists every artifact by file name and digest, plus every separate file used as a resource’s asset. artifacts describes the installable files with those same names. The two lists have these rules:

Each subject requires a sha256 digest of 64 lowercase hexadecimal characters. A sha512 digest is optional and has 128 lowercase hexadecimal characters. An artifact’s required size is its length in bytes and is checked alongside its digest. Its optional url gives the download location.

Project, version, and source

project follows the project-name rules. version is semver 2.0.0; its prerelease part, if present, marks a prerelease and names its channel. See Versions. published_at is a UTC timestamp in RFC 3339 format.

The optional source object identifies where the release was built from. When present, it requires a repository URL in repo; commit and the vendor’s spelling of tag are optional. A resource with a repo source requires source.commit so its content is pinned. A consumer matches a requested tag against source.tag or the release-list entry’s tag, as Matching a request defines.

Platform, format, and variant

os, arch, libc, format, and variant are lowercase tokens made of letters, digits, _, -, and ., starting with a letter or digit. Vocabularies lists the documented values. A value outside those vocabularies is well-formed but does not match a host or unpack with a consumer that does not know it. A vendor uses such a value only for a platform or format this document has not named yet.

An absent os, arch, or libc means the artifact does not depend on that host property. For example:

Every artifact requires format: an archive or installer type, or raw for a bare executable. Artifacts that differ only in format carry the same build, and a consumer chooses its preferred format. A vendor must not publish two artifacts with the same os, arch, libc, variant, and format. packslip create rejects such a pair; a consumer that receives one refuses to choose between them.

variant distinguishes builds sharing os, arch, and libc, such as fips, baseline, debug, installer, or source. Unless asked for a variant, a consumer selects only artifacts without one.

Executable paths and command names

An artifact’s optional bin list names its executables. A string entry is a path from the true archive root, with no top-level directory stripped. For a bare executable, it is the artifact’s own name without its compression suffix.

When the command name on PATH differs from the file name, use an object: { "path": "bin/oxlint-x86_64", "name": "oxlint" }. Two entries may share a path under different names, allowing aliases such as pnpx for pnpm; a consumer may create links or copies.

A command name is the name as typed, without .exe. A Windows path keeps its extension, such as bin/tool.exe, and the consumer places it on PATH as name.exe. Names in requires.bin and a resource’s bin follow the same convention, so consumers compare them without adding or stripping an extension. For compatibility, the reference implementation reads an older document whose name includes .exe as if it did not.

Requirements, resources, and provenance

An artifact’s optional requires object describes what the host must provide: the minimum OS version (os_min), minimum glibc version for a gnu Linux build (glibc_min), shared libraries (libs), and external commands (bin). OS versions use the OS’s own terms, such as 12 for macOS Monterey and 10.0.17763 for Windows. See Host requirements.

The optional release-level resources list describes what ships beside the executables. Each entry has a kind and exactly one source. See Resources.

An artifact’s optional provenance list contains URLs of SLSA build provenance statements. The packslip authenticates the release statement; separately verified provenance establishes build claims at the SLSA build level its builder supports.

Signer and additional metadata

identity describes how the statement is signed and by whom, allowing a consumer to compare its pin with the received signature. For sigstore-oidc, key_id is the certificate’s subject identity: a workflow URI for CI or an email address for a person. issuer is the OIDC issuer. For sigstore-key, key_id is the key ID in uppercase hexadecimal. See Signing.

attested_by is vendor by default, or repackager. A repackager describes what it checked in evidence. See Repackager attestation.

The optional notes_url points to release notes. extensions carries additional metadata under keys naming the party that defines it. See Extensions.

Schema and validation

packslip schema prints the JSON schema, also published as release-v1.json. The schema checks types, required fields, token patterns, semver grammar, and digest lengths. The reference implementation’s validator checks the remaining rules, including project-name grammar, the fixed _type and predicateType, at least one artifact, and the cross-references between subjects, artifacts, and resources. Passing schema validation alone does not establish those relationships.

Vocabularies

The documented platform tokens follow components of Rust target triples, so a vendor can map its build matrix directly onto them:

FieldDocumented values
oslinux, darwin, windows, freebsd, netbsd, openbsd, illumos, android, ios
archx86_64, aarch64, armv7, armv6, riscv64, i686, powerpc64le, s390x, loongarch64
libcgnu, musl for Linux builds

The documented format values describe how the file is installed:

File typeFormatsConsumer behavior
Archivetar.xz, tar.gz, tar.zst, tar.bz2, tgz, tar, zip, 7zUnpack the archive.
Single compressed executablegz, xz, zst, bz2Decompress to the file named by bin.
Installerdeb, rpm, dmg, pkg, msi, msix, exe, appimageHand to the platform’s installer or a launcher.
Bare executablerawInstall the executable directly.

A package manager installing into its own directory does not unpack installer formats. A .exe that is the program itself uses raw; the exe format means a Windows installer.

Paths in bin and archive sources use / between directories, as ZIP entry names require. Windows PowerShell 5.1’s Compress-Archive instead writes \ in ZIP entry names. Consumers interpret those backslashes as slashes: tool-1.0\bin\tool.exe refers to tool-1.0/bin/tool.exe.

format is required even when the file name has no format suffix. Consumers select only formats they handle; an absent format cannot be selected. Files that are not installable artifacts, such as a shared library, header, or checksum sidecar, stay outside artifacts.

These vocabularies are open. Vendors may use well-formed tokens for types not yet named here, following the token rules; consumers skip formats they cannot open.

Field reference

Required fields inside an optional object are required when that object is present. For example, source is optional, but a supplied source must include repo. A resource must choose exactly one of archive, asset, repo, and exec.

Paths beginning with artifacts[] or resources[] below are relative to predicate; paths beginning with requires are relative to an artifact.

Statement fields

FieldTypePresenceMeaning
_typestringrequiredAlways https://in-toto.io/Statement/v1.
subject[]arrayrequiredOne entry per artifact and per resource asset.
subject[].namestringrequiredThe file name.
subject[].digest.sha256stringrequiredSHA-256 of the file, lowercase hex.
subject[].digest.sha512stringoptionalSHA-512 of the file, lowercase hex.
predicateTypestringrequiredAlways https://packslip.dev/release/v1.

Release fields

FieldTypePresenceMeaning
predicate.projectstringrequiredHost path naming the project, such as github.com/jdx/mise or github.com/oxc-project/oxc/oxlint.
predicate.versionstringrequiredSemver 2.0.0. Its prerelease part marks a prerelease and names the channel.
predicate.published_atstringrequiredRFC 3339 UTC publish time.
predicate.notes_urlstringoptionalURL of the release notes.
predicate.sourceobjectoptionalWhere the release was built from.
predicate.source.repostringrequiredSource repository URL.
predicate.source.commitstringconditionalCommit the release was built from. Required when any resource uses repo.
predicate.source.tagstringoptionalTag the release was built from, as the vendor spells it.
predicate.artifacts[]arrayrequiredOne entry per artifact; at least one.
predicate.resources[]array of objectoptionalWhat ships besides the executables. See Resources.

Artifact fields

FieldTypePresenceMeaning
artifacts[].namestringrequiredFile name, matching a subject entry.
artifacts[].osstringoptionallinux, darwin, windows, … Absent: any OS.
artifacts[].archstringoptionalx86_64, aarch64, … Absent: any architecture.
artifacts[].libcstringoptionalgnu or musl; Linux only. Absent: no dependence on one.
artifacts[].variantstringoptionalDistinguishes builds sharing os, arch, and libc: fips, baseline, debug, installer, source.
artifacts[].sizeintegerrequiredFile size in bytes. Verified alongside the digest.
artifacts[].urlstringoptionalDownload URL.
artifacts[].formatstringrequiredArchive, compression, or installer type, or raw for a bare executable.
artifacts[].bin[]array of string or objectoptionalExecutables inside the artifact: a path from the archive root, or { path, name } when the PATH name differs. A name is the command as typed, without .exe.
artifacts[].requiresobjectoptionalWhat the host must provide. See Host requirements.
artifacts[].provenance[]array of stringoptionalURLs of SLSA build provenance statements for this artifact.

Requirement fields

FieldTypePresenceMeaning
requires.os_minstringoptionalMinimum OS version in the OS’s own terms.
requires.glibc_minstringoptionalMinimum glibc for a gnu Linux build.
requires.libs[]array of stringoptionalShared libraries loaded from the host, by loader name (libssl.so.3, vcruntime140.dll). Read from the executables by packslip create; empty means none needed, absent means unchecked.
requires.bin[]array of objectoptionalCommands the executables need on PATH: { name, min? }, a bare name and the lowest version that works.

Resource fields

FieldTypePresenceMeaning
resources[].kindstringrequiredcompletion, man, cli-spec, skill, sbom, desktop, icon, app, or a kind consumers may not know yet.
resources[].artifactstringoptionalExact artifact file name. Limits this resource to that artifact and outranks platform-only scope.
resources[].os, arch, libcstringoptionalLimit the entry to artifacts of that platform, when layouts differ.
resources[].archivestringone sourcePath inside the selected artifact, from the archive root.
resources[].assetstringone sourceName of a separate release file, listed in subject with its digest.
resources[].urlstringoptionalDownload URL of the asset. Only with asset.
resources[].repostringone sourcePath in the source repository at source.commit, which is then required.
resources[].exec[]array of stringone sourceAn argv whose first element is a bin name and whose stdout is the file. See Running an exec entry.
resources[].envobject of stringoptionalEnvironment variables for the command, with {shell} substituted in values as in the argv. Only with exec.
resources[].shellstringconditionalRequired for a static completion: the shell it completes.
resources[].shells[]array of stringconditionalRequired for an exec completion: every shell it generates, substituted for {shell} in argv and env values.
resources[].formatstringconditionalRequired for cli-spec (usage) and sbom (cyclonedx, spdx).
resources[].binstringconditionalThe executable the entry is for, by its bin name. Required for cli-spec; for completion or man, required when the release has several executables, and identifying the only executable when omitted from a release with one.
resources[].namestringconditionalRequired for skill: its name.

Signer and extension fields

FieldTypePresenceMeaning
predicate.identityobjectrequiredHow the document is signed and by whom. See Signing.
predicate.identity.schemestringrequiredsigstore-oidc or sigstore-key.
predicate.identity.key_idstringrequiredThe certificate identity, or the key id in uppercase hex.
predicate.identity.issuerstringoptionalThe OIDC issuer, for sigstore-oidc.
predicate.identity.pin_workflowbooleanoptionalFor sigstore-oidc: false asks consumers to hold later releases to the signing repository, not to the workflow that signed this one. true when absent. See Workflow pinning.
predicate.attested_bystringoptionalvendor (default) or repackager. See Repackager attestation.
predicate.evidence[]array of objectoptionalWhat a repackager checked: { kind, detail? }.
extensionsobjectoptionalVendor- or consumer-defined data, keyed by who defines it, on the release, each artifact, each resource, the release list, and each list entry. See Extensions.

Resources

The optional resources list describes content associated with the release: completions, man pages, CLI specifications, agent skills, SBOMs, and desktop integration files. Each entry has a kind and exactly one source. Several entries can offer alternative sources for the same resource; scope, specificity, and source priority determine which one a consumer uses.

The entries also describe how consumers can install the software. A command-line package manager installs executables listed in bin; a desktop launcher uses desktop or app resources. A release can provide both without assigning the application a single category.

Source types

A resource comes from one of four sources. After resource selection narrows the entries, consumers prefer these sources in the order shown:

SourceContentVerification
archiveA path inside the selected artifact, from its true archive root. Available only when the artifact contains paths.Covered by the artifact’s digest.
assetA separate release file, named in subject; the entry’s url gives its download location. A skill directory can ship as its own archive this way.Checked against the subject’s digest, just like an artifact.
repoA path in the source repository at source.commit.Pinned by source.commit, which is required.
execThe stdout of an argv whose first element is a bin name; env supplies command-specific environment variables.The executable is verified; its generated output has no separate verification.

An exec source is common for completions: cobra, clap, oclif, and usage generate them from the binary without shipping a static file. If a vendor also provides a static file, it lists that source first. Running an exec entry defines when a consumer may run the command.

Resource scope

An entry’s optional os, arch, and libc fields limit it to artifacts with matching field values. Each supplied field must equal the selected artifact’s field. Omitting all three imposes no platform restriction.

The optional artifact field names an exact file in artifacts. Use it when artifacts for the same platform have different layouts or when a resource belongs to one variant. The named artifact must exist and must match any platform scope on the entry. For example, if a fips build places its man page at a different path from tool-linux-x64.tar.xz, name each artifact in its corresponding resource entry. The TOML manifest that packslip create reads spells this as artifact = "tool-linux-x64.tar.xz" inside [[resource]].

An archive entry never applies to an artifact whose format is bare: raw, gz, xz, zst, or bz2. Such an artifact is the executable itself, so it holds no path for the entry to name. A vendor that publishes tool-linux-x64.tar.xz beside a bare tool-linux-x64 therefore needs no scope to keep an archive entry off the bare one; the source itself limits its applicability. The same man page can still be supplied for a bare artifact through an asset or repo entry.

Resource identity

Entries compete only when they describe the same resource. Identity consists of the kind and the following fields:

KindIdentity within that kind
completionbin and shell. An exec entry offering several shells has one identity per shell.
cli-specbin and format.
skillname.
sbomformat.
Every other kindThe source’s file name, or the kind alone for an exec source.

Entries with different identities never hide one another. A Linux-scoped skill does not replace an unscoped skill with a different name, and a platform-specific zsh completion does not affect a bash completion.

Selecting resources

For each resource identity, a consumer applies these steps. The reference implementation exposes this selection as select_resources.

  1. Keep the entries that apply to the selected artifact.
  2. Of those, keep the most specific. An entry that names artifact outranks every entry scoped only by platform; among entries equally scoped by artifact, the entries naming the most of os, arch, and libc win.
  3. Try the remaining entries by source priority, as Fallback and verification defines.
  4. Try entries with the same source type in their order in resources. This final tie-breaker is an ordered list of alternatives: the first usable entry wins, and a consumer stops fetching or running lower-priority entries once it has that thing.

For example, two equally scoped repo entries for one skill try the first directory, then the second only if the first is unavailable. A verification failure refuses the release; it is never a reason to try the next entry.

Resource kinds

Shell completions and man pages

completion supplies a shell completion script. A static source uses shell to name its shell: bash, zsh, fish, powershell, nushell, or elvish. An exec source uses shells to list every shell the command generates. The consumer substitutes each shell for {shell} in argv elements and env values. Common patterns are:

Generatorexecenv
cobra or clap["tool", "completion", "{shell}"]None needed.
clap dynamic completions["tool"]{ "COMPLETE": "{shell}" }
click["tool"]{ "_TOOL_COMPLETE": "{shell}_source" }

bin names the executable the completion is for. When a release has one executable, the field may be omitted: the entry then identifies that executable and competes with an entry that names it explicitly. When a release has several executables, bin is required.

man supplies a man page whose section is its file suffix, as in mise.1. Its bin identifies the documented executable using the same rules as a completion.

CLI specifications

cli-spec is a machine-readable description of the executable named by its required bin, in the specified format. The documented format is usage, a usage spec. The consumer’s own copy of usage can generate completions for every shell it supports, a man page, and Markdown documentation from that spec, without running vendor code at install time.

Generated completions call usage complete-word when the shell requests completion, so a consumer that generates them installs usage beside the tool. Generated man pages and documentation have no runtime dependency on usage. A vendor can also ship static completions for consumers that want to avoid that dependency.

Agent skills

skill describes an agent skill in the Agent Skills format, identified by name. A skill is a directory containing SKILL.md and the files it references. Its source determines the layout:

A consumer counts a skill as present only after SKILL.md is in place. A partially fetched directory does not satisfy the resource.

Software bills of materials

sbom supplies a software bill of materials with format set to cyclonedx or spdx. Its source must be archive, asset, or repo, so the content is covered by a digest or source commit; exec is never allowed. A release with one SBOM per platform lists an entry per os or per artifact archive.

Desktop integration

Fallback and verification

After scope and specificity have narrowed the alternatives, a consumer takes the first usable source for each resource in this order:

  1. archive.
  2. asset.
  3. repo.
  4. Content generated from a cli-spec.
  5. exec.

Static cli-spec sources follow the same archive, asset, repo priority. Within each source type, entries are tried in document order. A consumer ignores kinds and sources it does not know, so a vendor may publish a font or its own kind before the specification names it.

Resources are optional to installation. If an asset or repository file is unavailable, or a network request fails, the consumer may try the next eligible source. If no source is usable, it reports the missing resource and completes installation of the executables; a later attempt may fetch the resource.

Verification failure has a different consequence. A resource whose digest differs from subject, or whose repository content differs from source.commit, causes the consumer to refuse the release. It does not try another source, just as it would not accept an artifact with the wrong digest.

Running an exec entry

An exec entry runs a release executable. For completions, consumers run it on demand when the shell first requests completion, without additional permission beyond installation. A consumer caches successful output per release, executable, and shell, so repeated requests do not rerun the command.

Other exec resources, such as a generated skill written to disk, run at install time only if the user has allowed vendor code to run then. Without that permission, the consumer treats the entry as absent rather than failing the installation. Consumers may also generate completions at install time under the same permission.

Whenever it runs an exec entry, the consumer:

A non-zero exit, timeout, or empty output makes the entry unavailable for this attempt. The consumer may try it again later.

Host requirements

requires describes what the host must provide before the software can run. Its four optional fields are os_min, glibc_min, libs, and bin. They use OS versions, loader names, and command names, allowing consumers to check requirements without a shared package registry.

Minimum OS and glibc versions

os_min is the lowest supported OS version, expressed in the OS’s own terms. glibc_min is the lowest supported glibc version for a gnu Linux build.

For os: "linux", os_min names the Linux kernel version, excluding local build suffixes such as -18-amd64 or +. It does not name a distribution’s release: the OS selector identifies Linux, with no distribution identifier. For darwin, os_min names the macOS product version. For windows, it names the Windows major, minor, and build numbers.

Shared libraries

libs lists shared libraries the executables load from the host, using the names resolved by the loader:

PlatformNameExamples
Linux and FreeBSDsonamelibssl.so.3, libstdc++.so.6
WindowsDLL namevcruntime140.dll
macOSdylib file namelibssl.3.dylib

The list excludes the C runtime and loader already described by libc and glibc_min: libc, libm, libdl, libpthread, librt, libgcc_s, and the dynamic loader; /usr/lib and /System/Library on macOS; and DLLs shipped by Windows. It also excludes libraries bundled in the artifact and found through its own rpath.

packslip create reads the list from the executables named in the artifact’s bin. A consumer with the artifact can inspect the same bytes; the signed release manifest lets it check before downloading. Presence of the field records whether that inspection happened:

Required commands

requires.bin lists commands the release’s executables invoke and cannot work without. Each entry has a bare name, as invoked by the executable (java, python3, or git), with no directory or .exe suffix. Its optional min gives the lowest supported version.

The vendor declares these requirements; binary inspection does not establish that a program invokes java. Only required commands belong here. Optional integrations go under extensions, and a required command must not be one the release itself provides.

Comparing versions

min, glibc_min, and os_min are numeric lower bounds, so a min of 17 means 17.0.0 and later, including 21. A consumer compares dot-separated nonnegative integer components numerically, padding missing components with zero; 2.10 exceeds 2.9. If either spelling cannot be compared this way, the check is unknown and the consumer warns rather than guessing.

Checking requirements before installation

Select the artifact first, as Selecting an artifact defines, then check its requirements before installing. Requirements do not break selection ties or silently redirect installation to another build.

The consumer’s response depends on which requirement failed:

Check resultRequired response
A library in libs is missing, or the host is below glibc_min or os_min.Refuse installation and explain what is missing in the consumer’s own terms, such as a distribution package providing the soname or an OS upgrade. The user may override the refusal for that tool.
A command in bin is missing or below min.Install with a warning naming the command and required version. If the consumer can install that command, present it as an available installation.
A check is unknown.Warn and continue, including when versions are unreadable or the loader cannot be queried.

Missing libraries or unsupported OS and glibc versions prevent the executables from starting. A missing command affects the code paths that invoke it, and the user may intend to install that command next.

Check libraries using the loader’s own search locations: the dynamic linker’s cache and search path on Linux (ldconfig -p, LD_LIBRARY_PATH), system library and framework directories on macOS, and PATH with the system directory on Windows. Find commands on PATH, adding .exe on Windows, and run them with --version to read their versions. Compare min, glibc_min, and os_min against the command, glibc, and OS versions respectively. A failed version probe or an unrecognized version spelling leaves the check unknown.

Limits of host requirements

requires uses names the OS resolves. It does not name other projects or their versions, or say where to get them. A package manager’s installation hints belong under extensions.

Version 1 does not express alternative commands satisfying one requirement, such as terraform or tofu, or required library symbol versions, such as libstdc++.so.6 at GLIBCXX_3.4.29. glibc_min expresses a version requirement for libc alone.

Extensions

Use extensions for metadata this specification does not define, such as installation hints, an end-of-life date, or a build ID. An extensions object is allowed on the release predicate, each artifact, each resource, the release-list predicate, and each release-list entry:

"extensions": {
  "example.com": { "build_id": "20260901.3" }
}

Each key identifies the party that defines the value. Consumers use their names, such as mise or pacvamp; vendors use domains they control, such as example.com. That party documents the value’s meaning. The signature covers extension data, and packslip show prints it unchanged. A consumer reads the keys it defines and ignores the rest.

packslip does not and will not assign meaning to data under extensions or claim its keys for future standard fields. This keeps namespaced data from colliding with later additions to the format.

Outside extensions, consumers also ignore unknown fields so revisions of version 1 can add fields without breaking older consumers. Vendors must put their own data in extensions rather than inventing fields elsewhere: a future revision could assign an invented field a different meaning. If an extension becomes useful across many vendors, the format may add a standard field for it; the extension key continues working alongside that field.

Repackager attestation

When a vendor publishes no packslip, a package repository (such as apt or AUR), registry, or mirror may describe and sign the vendor’s artifacts with "attested_by": "repackager". Mirrors can also attest to files already covered by a vendor’s packslip, as described below.

The project still identifies the vendor’s project, and the artifacts remain the vendor’s files. identity identifies the repackager that signs this statement. evidence records what it checked before signing:

"attested_by": "repackager",
"evidence": [
  { "kind": "apt-release-gpg", "detail": "3FEF9748469ADBE15DA7CA80AC2D62742012EA22" },
  { "kind": "pkgbuild-checksums" }
]

Each evidence entry has a kind and optional detail. The documented kinds are:

KindEvidence checked
pkgbuild-checksumsArtifact digests matched the packaging maintained by the repackager.
checksum-file-over-tlsThe vendor’s unsigned checksum file, retrieved over TLS.
apt-release-gpgAn apt index signed with the key identified in detail.
vendor-signatureA detached signature published by the vendor.
vendor-packslipThe vendor’s packslip was verified; detail is the SHA-256 of its bundle file.
github-attestationGitHub artifact attestations were verified.
provenance-verifiedSLSA provenance was verified against the builder.
scanThe artifacts were scanned; detail points to the report.
noneNothing was checked; the document rests on the repackager’s signature alone.

A verified repackager document authenticates the repackager’s publication of these digests and its statement that it checked the listed evidence. It does not establish vendor claims the vendor did not sign. Consumers rank it below a vendor document. A consumer that already holds a vendor document for a project refuses to replace it with a repackager document without a person’s approval.

A mirror is a repackager whose document keeps the vendor’s digests, substitutes the mirror’s download URLs, and includes vendor-packslip evidence. A consumer using the mirror obtains the same bytes under the mirror’s pin. It can also fetch and verify the vendor’s document to retain both attestations.

Discovery

Publish each release’s bundle alongside its artifacts, either as a release asset or in the release’s directory on a download site.

Discovery tells a consumer which releases exist and where to fetch their bundles. Projects on their own domains publish a signed release list. GitHub projects use the repository’s releases endpoint and may add a signed list to withdraw releases, mark security fixes, map tags to versions, or recommend a default. Discovery does not replace verification of the selected release.

The signed list

A release list uses the same signed bundle structure as a release packslip, but its statement has the releases/v1 predicate. Its subjects pin release bundles rather than artifacts:

{
  "_type": "https://in-toto.io/Statement/v1",
  "subject": [
    { "name": "https://dl.example.com/2026.9.1/packslip.sigstore.json",
      "digest": { "sha256": "...", "sha512": "..." } },
    { "name": "https://dl.example.com/2026.9.0/packslip.sigstore.json",
      "digest": { "sha256": "..." } }
  ],
  "predicateType": "https://packslip.dev/releases/v1",
  "predicate": {
    "project": "mise.jdx.dev",
    "generated_at": "2026-09-01T12:00:00Z",
    "expires_at": "2026-10-01T12:00:00Z",
    "sequence": 42,
    "latest": "2026.9.1",
    "identity": { "scheme": "sigstore-key", "key_id": "5A0A0B8B9C6D7E1F" },
    "releases": [
      { "version": "2026.9.1", "tag": "v2026.9.1", "published_at": "2026-09-01T12:00:00Z",
        "packslip": "https://dl.example.com/2026.9.1/packslip.sigstore.json",
        "security": true },
      { "version": "2026.9.0", "tag": "v2026.9.0", "published_at": "2026-08-20T12:00:00Z",
        "packslip": "https://dl.example.com/2026.9.0/packslip.sigstore.json",
        "status": "yanked", "status_reason": "CVE-2026-1234" }
    ]
  }
}

Each entry copies version and published_at from the release’s packslip. If the packslip has a source.tag, the entry also copies it as tag, preserving the vendor’s spelling. Entries may additionally carry:

The optional list-level latest recommends the vendor’s default release. It must match an entry’s exact semver version, including any build metadata; it is neither a tag, a range, nor a per-release flag. A version absent from the list makes the list invalid. A listed target that is yanked or otherwise ineligible leaves the list valid but triggers the Latest fallback rules.

List fields

FieldTypePresenceMeaning
_typestringrequiredAlways https://in-toto.io/Statement/v1.
subject[]arrayrequiredOne entry per listed packslip.
subject[].namestringrequiredThe packslip’s URL, as its entry’s packslip gives it.
subject[].digest.sha256stringrequiredSHA-256 of the bundle file, lowercase hex.
subject[].digest.sha512stringoptionalSHA-512 of the bundle file, lowercase hex.
predicateTypestringrequiredAlways https://packslip.dev/releases/v1.
predicate.projectstringrequiredThe project the list is for. See Project names.
predicate.generated_atstringrequiredWhen the list was produced, RFC 3339 UTC.
predicate.expires_atstringrequiredWhen the list goes stale, RFC 3339 UTC. A consumer refuses an expired list.
predicate.sequenceintegerrequiredA nonnegative counter. A consumer refuses a list whose sequence is lower than one it has accepted.
predicate.lateststringoptionalThe vendor’s recommended default: the exact version of an entry. See Latest.
predicate.identityobjectrequiredHow the list is signed, with the fields a release statement’s identity has. See Field reference.
predicate.releases[]arrayrequiredThe listed releases, in any order.
releases[].versionstringrequiredSemver 2.0.0, copied from the packslip.
releases[].tagstringoptionalThe packslip’s source.tag.
releases[].published_atstringrequiredRFC 3339 UTC, copied from the packslip.
releases[].packslipstringrequiredURL of the release’s bundle. The subject of that name carries its digest.
releases[].statusstringoptionalyanked when the release was withdrawn.
releases[].status_reasonstringoptionalWhy it was withdrawn.
releases[].securitybooleanoptionalThe release fixes a vulnerability.
releases[].evidence[]array of objectoptionalOn a list from someone other than the vendor, what the publisher checked: { kind, detail? }, with the kinds Repackager attestation documents.
extensionsobjectoptionalOn the list and on each entry. See Extensions.

Freshness and rollback protection

Each subject names a listed packslip’s URL and carries the digest of its bundle file. The consumer therefore checks the exact document the list selected, even when a URL’s contents change.

The list also carries two freshness controls, borrowed from TUF’s timestamp role:

These controls apply to the list, separately from verification of its release bundles.

Publishing a list

For a project on its own domain, publish the list at:

https://<host>/.well-known/packslip/<path>.json

Here <host> is the project name’s host and <path> is the remainder of its name. A bare host instead uses https://<host>/.well-known/packslip.json.

ProjectList URL
mise.jdx.devhttps://mise.jdx.dev/.well-known/packslip.json
jdx.dev/misehttps://jdx.dev/.well-known/packslip/mise.json
github.com/jdx/misehttps://github.com/.well-known/packslip/jdx/mise.json

GitHub does not serve the last URL; GitHub defines its discovery path instead. A forge that serves its well-known list needs no additional discovery mechanism. A project on its own domain must publish the list: if none exists, the consumer refuses the project rather than guessing download URLs.

The project’s domain anchors its identity, while the list and release bundles point to wherever the bytes are hosted. A vendor with only a git repository can keep the list and bundles at paths served as raw files and point artifact URLs at another host, including an LFS store. A forge’s release API is not required.

The reference library exposes list_url for both a project’s own list and a list kept by another publisher. github_list_path returns the repository-relative path for a GitHub supplementary list. The CLI’s packslip releases command builds a list from local release bundles; --latest sets the list-level recommendation, and omitting that option leaves the list without one. The list’s JSON schema is at https://packslip.dev/schema/releases-v1.json.

GitHub

For github.com/<owner>/<repo>[/<subpath>], the repository’s releases endpoint supplies discovery without requiring a signed list:

Supplementary signed lists

The repository may also carry a signed list on its default branch: .well-known/packslip.json, or .well-known/packslip/<subpath>.json for a monorepo tool (.well-known/packslip/crates/cli.json for github.com/biomejs/biome/crates/cli). GitHub serves the file at https://raw.githubusercontent.com/<owner>/<repo>/HEAD/<path>.

The list is signed by the same identity as the release packslips. It supplements the endpoint rather than replacing it:

Publish a supplementary list only when the project needs to withdraw a release, mark a security fix, recommend a default with latest, or map a tag that names no version. A project needing none of those omits the list. A signed withdrawal also works with immutable GitHub releases, where the release and its bundle cannot be deleted.

Keeping a supplementary list available

After a consumer accepts a supplementary list, it remembers that the project uses one. A later missing list is an error: returning to endpoint-only discovery would silently undo signed withdrawals.

The vendor must re-sign the list before every expires_at, even if no new release has shipped, because consumers refuse expired lists. To retire its entries, publish a fresh, unexpired list with a nondecreasing sequence. A user may explicitly clear the remembered list policy for that project.

Lists from other publishers

A registry, mirror, or scanning service can publish a list under its own host. It publishes one list per vendor project, using the vendor’s full project name in the well-known path:

https://registry.example/.well-known/packslip/github.com/jdx/mise.json

The library’s list_url returns this URL for a publisher other than the project’s host. A consumer trusts a publisher only through configuration and keeps a pin for each trusted host.

Each entry pins a release bundle’s digest. That bundle may be the vendor’s packslip or a repackager document signed by the list’s publisher. Verify a vendor document against the vendor’s pin implied by project. Verify a repackager document against the pin implied by its own project and attested_by. The list’s signature establishes who selected the releases and what their evidence says the publisher checked. It does not establish who built the artifacts.

This selection is a stamp, and the publisher is a stamping host. Stamping lets a registry require a scan or an organization curate versions without maintaining a per-tool installation recipe. A consumer configured to trust stamping hosts applies these admission rules:

A stamping host can use any signing scheme that fits its operation. For example, a continuous scanning service can hold a key, while a registry publishing from a repository can sign keylessly. In either case, the consumer pins the host.

Versions

The signed version must use semver 2.0.0: MAJOR.MINOR.PATCH, with an optional prerelease part after - and optional build metadata after +. Calendar versions such as 2026.9.1 qualify. Both packslip create and consumers refuse other spellings. source.tag preserves the vendor’s tag, which may have a different spelling such as v2026.9.1 or oxlint_v1.0.0.

Ordering, prereleases, and channels

Derive all three properties from the signed version string:

VersionChannel
1.3.0-nightly.20260904nightly
1.2.0-beta.2beta
1.2.0-rc.1rc
1.2.0-1None: the first prerelease identifier is numeric.
1.2.0None: the version has no prerelease part.

Separate prerelease and channel fields could contradict the signed version or mutable forge metadata, so the format does not store them. Withdrawals and default recommendations instead belong in discovery metadata, where they can change without replacing a signed release manifest.

Spelling a version

A vendor using another version spelling must choose a semver spelling once and keep it. The tag retains the original spelling.

Two releases must not share a version. If the vendor’s spelling would collide, as with two releases on the same day, the semver spelling must distinguish them with an ordered component rather than build metadata.

Tags

To derive a forge release’s version from its tag, remove an optional project prefix and an optional v, then apply Spelling a version to the remainder. An allowed prefix is the tool’s full subpath, the last segment of that subpath, or the repository name, followed by /, -, _, or @.

ProjectTagVersion
Any projectv1.2.31.2.3
github.com/oxc-project/oxc/oxlintoxlint_v1.0.01.0.0
github.com/biomejs/biome/crates/clicli/v1.9.41.9.4
github.com/jqlang/jqjq-1.7.11.7.1
Any projectv4.14.1.0

Deriving a version from a tag avoids fetching every bundle during listing. The signed packslip remains authoritative: if its version differs from the derived version, refuse the release. Tags such as nightly and release-1.2.3 do not name versions under these rules and are omitted from tag-based listing. To expose such a release, publish a signed list entry mapping its tag to its version.

Matching a request

A version request matches a prefix of dot-separated components:

RequestMatches
20Every 20.x.y release.
3.12Every 3.12.x release.
1.2.0-betaThe betas of 1.2.0.

A request may also use the vendor’s tag, with or without its leading v. Match that spelling against source.tag or the release-list entry’s tag, so a user can request a release by the name the vendor published.

A matching release is eligible only if it is not yanked, satisfies the prerelease policy, and belongs to the requested channel when one was given. Prereleases require an explicit request. The release list’s sequence supplies rollback protection; a single release manifest does not.

Latest

An unconstrained latest request, including a consumer’s default install request, asks for the vendor’s recommended eligible release. For example, a vendor may publish 3.0.0 while recommending 2.8.4 as the default. Exact versions, version prefixes, ranges, and channel requests still use their own matching rules and semver precedence; the recommendation does not change their ordering.

Finding the recommendation

Use the first applicable source:

  1. Vendor’s signed list. If the accepted list has latest, use it. For GitHub projects, this is the supplementary list. Verify the list’s signature, identity, expiry, and sequence before using the pointer.
  2. GitHub’s latest release endpoint. For a GitHub project without a signed latest, use the release returned by the latest release endpoint. Resolve its tag through normal discovery, including any signed list entry that maps the tag. The release must belong to the requested project: a repository-wide pointer for another tool supplies no recommendation for this one. The endpoint is an unsigned discovery hint; it supplies neither authenticity nor sequence protection.
  3. No recommendation. Outside GitHub, a project without a signed pointer has no recommendation.

Pointers in third-party stamping lists do not replace the vendor’s recommendation. Those lists control which candidates are admitted.

Verify a recommended candidate exactly as any other release: check its signature and identity, then the manifest’s version and digest consistency. A verification failure is an error.

Select a verified recommendation only if it also passes every eligibility check: vendor withdrawals, prerelease policy, minimum release age, configured stamping policy, and artifact and host eligibility. A recommendation does not waive any of those checks.

Falling back to version precedence

If there is no recommendation, or its target is absent from discovery or excluded by eligibility policy, select the highest-precedence eligible release from the normal candidate set. An ineligible signed pointer falls directly back to semver selection; do not try GitHub’s pointer instead. Report any skipped recommendation and the reason. Fail if no eligible release exists.

For example, when the recommended 2.8.4 is yanked or too young, the highest eligible release may be 3.0.0. To exclude 3.0.0, the vendor must withdraw it or use an admission policy. A latest pointer recommends a release; it does not exclude other versions.

Fallback applies to an absent or ineligible recommendation, never to a verification or fetch failure:

Change or remove a signed recommendation by publishing a new list with an increased sequence. No release manifest needs to be replaced.

Selecting an artifact

The first three rules filter candidates; the remaining rules choose one of those candidates. The reference library exposes this process as select_artifact.

  1. Match the host. Each of os, arch, and libc must either be absent or match the host. If the host’s libc is unknown, accept only artifacts without a libc. A consumer may support additional fallbacks, such as a gnu host using a musl build or an aarch64 macOS host using x86_64 under Rosetta. Rank exact matches first and report any fallback used.
  2. Match the variant. If a variant was requested, keep only artifacts carrying it. Without a requested variant, keep only artifacts that have none.
  3. Check format support. Keep only formats the consumer can handle.
  4. Prefer specificity. Choose the artifact naming the most of os, arch, and libc. A host-specific build therefore beats a portable build.
  5. Apply format preference. For equally specific artifacts, use the consumer’s format preference. A typical order is tar.xz, tar.zst, tar.gz, tgz, tar.bz2, tar, zip, 7z, then single compressed executables, then raw.
  6. Refuse unresolved ties. Two remaining artifacts with equal rank are a vendor error. The consumer refuses to guess between them.

Consumer rules

The rules cover discovery, verification, selection, and installation. Consumers must keep state across installs to enforce signer continuity, no-downgrade policy, and release-list rollback protection. The rules and their linked sections use this remembered state:

  1. Pin the identity once.

    • For a forge project, use the project name to establish the first pin: accept only the forge’s issuer and an identity under that repository. Remember the repository ID in the certificate. That ID then pins the project as Repository identity and renames defines: renames and transfers preserve the pin, while a repository recreated under the old name does not inherit it.
    • If the consumer has independently read the project’s published signer fingerprint, include it in the first pin and check releases against it. This avoids trusting a release on first use.
    • For other projects, pin a public key or identity from the consumer’s maintained pins, or from the well-known list on first use.
    • Trust another publisher’s list per host, through configuration.
    • Never take a key from the document itself or trust a bundle’s key hint.
  2. Verify the bundle and downloaded files. Verify the signature, certificate chain, and log entry as Sigstore defines them, then the statement’s structure. For a forge project, check that the statement names the requested project or the same repository renamed, under Project names and Repository identity and renames. Then verify the subject digest of every downloaded artifact or asset and the size of every artifact. Refuse a bundle without a log entry unless unlogged bundles were explicitly allowed under Signing.

  3. Enforce no-downgrade. Compared with the last release accepted for the project, refuse a release:

    • whose identity.scheme is weaker;
    • whose signer changed without a person’s approval;
    • whose attested_by went from vendor to repackager;
    • that dropped per-artifact provenance the last release carried.

    Compare keyless signers by workflow path, without the ref: a new tag from the same workflow keeps the same signer. Across a repository rename or transfer, compare the path inside the repository only after the repository IDs establish that it is the same repository. Use the ID from the release’s certificate, or, when comparing two recorded signers such as lockfile entries, the ID recorded with each. A changed repository ID means a changed signer even if the identity string matches.

    With identity.pin_workflow: false, compare by repository alone. If the remembered value is true, changing it to false weakens the pin and requires a person’s approval under Workflow pinning.

  4. Apply the minimum release age. Measure any configured age from the log’s integration time. Use published_at only for an unlogged bundle the consumer chose to accept.

  5. Use the project’s discovery metadata.

    • For GitHub projects, read the releases endpoint and any supplementary signed list. For other projects, read the signed list at the well-known URL. Refuse a project with neither discovery source, and refuse when a previously accepted signed list is now missing, as GitHub describes.
    • Refuse a signed list that has expired or whose sequence is below the last one accepted.
    • Never select a yanked entry; skip prereleases unless asked for them; rank by semver precedence, and take the vendor’s recommendation for an unconstrained request as Latest says.
    • Refuse a packslip whose digest is not the one the list pins, or whose version is not the one its tag or list entry named.
    • When the consumer trusts stamping hosts, select only versions one of them lists, unless the user chose the vendor alone for that project, as Lists from other publishers says.
  6. Select one artifact. Apply Selecting an artifact and refuse unresolved ties.

  7. Check host requirements before installing. Check requires for the artifact selected by rule 6, using Host requirements. Refuse an install when a library, glibc, or OS version prevents the executables from starting. Warn when a required command is missing or too old, and report either outcome in the consumer’s own terms. A requirement the consumer cannot check produces a warning, never a refusal. Requirements do not resolve an ambiguous artifact selection.

  8. Install resources. Apply Resources:

    • For each resource identity, filter to entries whose scope fits the selected artifact, then keep the most specific entries under Selecting resources. Choose the most verifiable source in the order given by Source types and Fallback and verification.
    • Run an exec completion when a shell first requests it and cache the result. Run other exec entries only if the user chose to run vendor code at install time, under Running an exec entry.
    • Ignore kinds the consumer does not know.
    • If a resource cannot be fetched, report it and finish the install without it. If its digest differs from the signed digest, fail the install.

Stability

Version 1 is final. The predicate types https://packslip.dev/release/v1 and https://packslip.dev/releases/v1 identify this format contract: a document that verifies under them today continues to verify for as long as its signing material holds.

An unchanged verifier may still need updates to support new Sigstore bundle or log formats. The reference implementation’s compatibility matrix records the release baselines, historical verification material, and authenticated root rotations it has tested. Security policy and freshness checks continue to apply; format stability does not require accepting expired metadata or overriding a security rejection.

What version 1 holds fixed

What version 1 may add

Removing a field, making an optional field required, changing a selection rule’s result, or making any change outside these bounds requires version 2. That version gets its own predicate type, published alongside version 1, and consumers may accept both. No revision of this specification invalidates a release already signed under release/v1, and vendors are not required to migrate.

Format and implementation versions

The predicate types version the format. Separately, the reference Rust crate, CLI, and two GitHub Actions share an implementation semver version. The implementation may reach version 2 while the format remains at version 1.

Conformance coverage

The conformance vectors express the covered rules as executable examples: artifact and resource selection, tag versions, statement validity, forge identity, and signer fingerprints. An implementation that disagrees with a vector disagrees with this specification.

The forge identity vectors include a remembered pin in each case. The other vectors do not cover rules that depend on state retained between installs, such as no-downgrade and release-list sequences. Consumers must implement those stateful rules as well.

Tooling

The packslip repository contains the reference Rust library, CLI, and two composite GitHub Actions. The release action publishes a release’s packslip; the release-list action builds and signs discovery metadata.

The CLI creates and verifies release and list bundles, prints signer fingerprints, and installs tools. Its commands serve different parts of the consumer workflow:

Other consumers build their discovery, installation, and trust policies around the library’s verification and selection operations. Use the guides for task-oriented examples and the command reference for flags and arguments:

TaskGuideCommand reference
Install the CLIInstallation methodsCLI overview
Install an upstream toolInstall a tool with packslipinstall
Create a first packslipGetting startedcreate
Generate an Ed25519 keyGetting startedkeygen
Publish from GitHubPublish with GitHub Actionsaction.yml
Describe release filesArtifact configuration, Resources, Host requirements, Release recipescreate
Publish discovery metadataManage release listsreleases
Host releases on your own domainHost releases on your own domainreleases/action.yml
Verify downloadsVerify a releaseverify
Pin a keyless signer by fingerprintVerify a releasepin, verify --pin
Inspect a statement without verificationVerify a releaseshow
Build a consumer or mirrorBuild an installer or mirror—
Export JSON schemasJSON schemasschema
Check another implementationConformance vectors—

The CLI reference is generated from command help, and the specification page is generated from this source. See Contributing for the documentation workflow.