Packaging & Distribution

This content was generated with the assistance of AI and should be verified against the official documentation of each tool and framework before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

This page was written against the documentation of pipx, uv, Homebrew, npm, JBang, GraalVM Native Image, Cargo, the Windows Package Manager, .NET tools, GoReleaser, Sigstore cosign and GitHub artifact attestations as checked on 2026-09-30, and against the docsctl running example of this sub-section (fictional, illustrative). The snippets are fragments, not compiled or run here, and every URL, owner and checksum in them is a placeholder. Names and defaults change between releases, so check the linked page for the release you use.

A CLI that agents can drive is only useful if it is there: on the developer’s laptop, on the CI runner, inside the sandbox in which the agent runs shell commands. Distribution is therefore part of the tool’s interface. The decisions are which channels to publish to, how a user pins an exact version (an agent that re-resolves "latest" on every run is a reproducibility problem), what the target machine must already have, and how anybody can verify that the file they run is the file you built. The security angle of the last point is treated in Security; the language-specific entry points are in the framework pages (Python, Java, Node.js, Go & Rust and Swift & .NET).

Choosing channels

Publish to the channel that is native to your users' ecosystem first, and add a self-contained binary for everyone else. The table lists the channels of this page; each one has a section below.

Table 1. Distribution channels for docsctl
Channel Install (and pin) The target machine needs Notes

pipx / uv tool

pipx install docsctl, uv tool install docsctl@0.1.0

Python (uv can provide one)

Each tool gets its own isolated environment; the console-script entry point becomes the command.

Homebrew

brew install OWNER/tap/docsctl

Homebrew on macOS or Linux

A formula in your own tap; users get upgrades with the rest of their packages.

npm bin

npm install --global docsctl, npx docsctl

Node.js

The bin field links a file that must start with a Node shebang.

JBang

jbang app install Docsctl.java

A JDK (JBang can download one)

A single source file with //DEPS lines; no build step for the user.

GraalVM native binary

Download, verify, put on the PATH

Nothing (one build per OS and architecture)

Fast start-up, the best default for agents; the build needs the native toolchain of each platform.

cargo install

cargo install docsctl --locked

A Rust toolchain

Compiles from source, so it is slow; publish prebuilt binaries as well.

winget

winget install Publisher.Docsctl

Windows with the Windows Package Manager

A manifest submitted to the community repository.

dotnet tool

dotnet tool install --global docsctl

The .NET SDK

A NuGet package; a local tool manifest pins the version per repository.

GoReleaser

Not a channel: builds the archives, checksums and signatures above

n/a

One .goreleaser.yaml produces the release for every platform.

Container image

docker run --rm IMAGE search kalman --json

A container runtime

A reproducible runtime for CI and sandboxes; pin by digest.

For Rust tools, cargo install and the [package] metadata that crates.io needs are covered in the Rust Reference’s Cargo & Workspaces.

The flow from a tag to the channels is the same for every ecosystem: build once per platform, checksum and sign the outputs, publish them, then let each package manager point at the published files.

flowchart LR T["Git tag v0.1.0"] --> B["CI builds per OS and architecture"] B --> A["Archives and native binaries"] A --> C["Checksums file"] C --> S["Signature and build attestation"] S --> R["Release assets published"] R --> H["Homebrew tap formula"] R --> W["winget manifest"] R --> I["Container image"] B --> P["PyPI, npm, NuGet, crates.io"]
Figure 1. From release tag to installable channels

pipx and uv tool for Python

pipx installs and runs end-user Python applications in isolated environments: it creates a separate virtual environment for each application and exposes only its commands on the PATH, so a tool’s dependencies never collide with a project’s. pipx install installs a tool, pipx run executes the latest version temporarily without installing it, pipx upgrade updates it and pipx ensurepath fixes the PATH. uv offers the same model as uv tool: uv tool install installs, uvx is an alias for uv tool run, uv tool upgrade updates, and uv tool update-shell adds the executable directory to the PATH. Both work for any package that declares console-script entry points, which is what [project.scripts] does in the Python framework page.

Installing and running docsctl from PyPI (placeholder package name)
pipx install docsctl                     # isolated environment, docsctl on the PATH
uv tool install docsctl@0.1.0            # pinned to an exact version
uvx docsctl@0.1.0 search kalman --json   # run once, without installing

Pin the version in anything an agent or a CI job runs (docsctl@0.1.0), so the tool cannot change under a working prompt. References: pipx and uv — Tools.

Homebrew formulae

A Homebrew formula is a small Ruby file with a homepage, a license, the url and sha256 of a source or binary archive, an install method and a test do block. Formulae for your own tool live in a tap, a Git repository named homebrew-<name> under your account, so users install with brew install OWNER/tap/docsctl after a one-time brew tap. The Formula Cookbook asks for tests that need no user input and exercise real functionality rather than only --version or --help, and recommends brew audit --new --formula before submission.

Formula for a prebuilt docsctl archive (Formula/docsctl.rb, placeholders)
class Docsctl < Formula
  desc "Search and publish this site's documentation"
  homepage "https://example.invalid/docsctl"
  url "https://example.invalid/releases/docsctl-0.1.0-macos-arm64.tar.gz"
  sha256 "<sha256 of the archive, copied from the checksums file>"
  license "Apache-2.0"

  def install
    bin.install "docsctl"
  end

  test do
    # A read-only command with no network access and no prompts
    assert_match "\"schemaVersion\"", shell_output("#{bin}/docsctl schema --json")
  end
end

The test block above uses the contract’s schema --json, which is read-only and never prompts, so it passes in the sandboxed test run. GoReleaser can generate and push the tap update on every release; its documentation lists Homebrew formula publishing as deprecated in favour of homebrew_casks, so check which one your GoReleaser version documents before adopting either. References: Homebrew — Formula Cookbook.

npm bin

An npm package exposes commands through the bin field of package.json, either as a string (one command named after the package) or as an object mapping command names to files. The referenced file must start with #!/usr/bin/env node, otherwise it is not started with Node. A global install links the file into a directory on the PATH (a .cmd shim on Windows); a local install makes it available in node_modules/.bin, from where npx can run it.

package.json (fragment) and the entry file
{
  "name": "docsctl",
  "version": "0.1.0",
  "bin": { "docsctl": "bin/docsctl.js" },
  "files": ["bin", "dist"]
}
bin/docsctl.js
#!/usr/bin/env node
import "../dist/main.js";

Keep the entry file tiny so that start-up cost stays low, as described in Agent Ergonomics, and use npx docsctl@0.1.0 …​ (an exact version) when an agent runs the tool without installing it. Reference: npm — package.json bin.

JBang

JBang runs a single Java source file as a script: //DEPS comment lines declare Maven dependencies, and a #!/usr/bin/env jbang shebang line makes the file executable (see the JBang documentation for how to write it in a Java file). It needs a JDK and downloads one when none is available. jbang app install installs a script as a command, and jbang app list shows what is installed. JBang can also run a file from a URL or a Maven coordinate, and a catalog lets users run a published tool as jbang docsctl@OWNER; because that runs remote code, jbang trust exists to make the user authorise the source first.

Docsctl.java as a JBang script (picocli as in the Java framework page; DocsctlCommand is the command class from that page)
//DEPS info.picocli:picocli:4.7.7

public class Docsctl {
    public static void main(String... args) {
        System.exit(new picocli.CommandLine(new DocsctlCommand()).execute(args));
    }
}
Installing it as a command
jbang app install Docsctl.java     # puts a docsctl launcher on the PATH
jbang app list

JBang is a good way to hand a JVM tool to Java developers without a release pipeline; it does not remove the JVM start-up cost, which is what the next section addresses. Reference: JBang documentation.

GraalVM native binaries

GraalVM Native Image compiles a Java application ahead of time into a standalone executable. The documentation describes executables that use fewer resources than the JVM, start quickly and reach peak performance without warm-up, which is why native binaries are the usual default for a tool an agent calls many times; measure with the hyperfine commands on the Java page before choosing. Two constraints shape packaging. First, the closed-world assumption: the build analyses which code is reachable, so reflection, resources and dynamic proxies need metadata (picocli-codegen produces it for picocli commands, see the Java page). Second, the build needs the platform’s native toolchain, so you build on each target operating system and architecture (a CI matrix) rather than cross-compiling one artifact for all of them.

Building the native binary (Maven and Gradle plugins automate the same step)
native-image -jar target/docsctl-0.1.0.jar docsctl
./docsctl --version

Run the contract tests of Testing CLIs against the native binary, not only against the JAR, because a missing metadata entry appears only in the native build. Publish one archive per platform and list its checksum. Reference: GraalVM Native Image.

cargo install

cargo install compiles a crate that has a binary target and puts the executable in <root>/bin (by default $HOME/.cargo/bin, chosen with --root, CARGO_INSTALL_ROOT, install.root or CARGO_HOME). --locked uses the dependency versions from Cargo.lock, so the user builds what you tested; --path installs from a checkout, --git from a repository, and --force reinstalls over an existing binary.

Installing docsctl from source
cargo install docsctl --locked         # from the registry (placeholder crate name)
cargo install --path . --locked        # from a checkout
cargo install --list                   # what is installed
cargo uninstall docsctl

Because it compiles on the user’s machine it needs a Rust toolchain and takes a while, which suits Rust developers and not CI runners or agent sandboxes. Publish prebuilt archives too, and let cargo install be the fallback. Reference: cargo install.

winget

The Windows Package Manager installs from manifests: YAML files that follow the manifest schema and live in the community repository (microsoft/winget-pkgs). A manifest names the PackageIdentifier (the Publisher.Package form), PackageVersion, publisher, licence and description, and one or more installers with an architecture, an installer type, the InstallerUrl and its InstallerSha256. The wingetcreate utility (wingetcreate new) prompts for the values, and can submit the pull request for you.

Minimal single-installer manifest shape (placeholders)
PackageIdentifier: Publisher.Docsctl
PackageVersion: 0.1.0
PackageLocale: en-US
Publisher: Publisher
PackageName: docsctl
License: Apache-2.0
ShortDescription: Search and publish this site's documentation.
Installers:
  - Architecture: x64
    InstallerType: <a supported installer type>
    InstallerUrl: https://example.invalid/releases/docsctl-0.1.0-windows-amd64.zip
    InstallerSha256: <sha256 of the file at InstallerUrl>
ManifestType: singleton
ManifestVersion: 1.12.0

The single-file form is valid only for one installer and one locale; use the multi-file form otherwise. winget accepts installers only in its list of supported formats and requires silent installation, so check the current list on the winget documentation and choose the installer type for a command-line tool from it; a plain executable is rarely the right shape for one. GoReleaser has a winget section that generates the manifest and opens the pull request from your release. References: Windows Package Manager — submit packages and Create your package manifest.

dotnet tool

A .NET tool is a NuGet package containing a console application. Two project properties, PackAsTool and ToolCommandName, turn a project into one, dotnet pack builds the package and dotnet nuget push publishes it. Users install it globally (dotnet tool install --global), in a custom location (--tool-path) or as a local tool recorded in .config/dotnet-tools.json, which dotnet tool restore reinstalls, pinning the version per repository. The project fragment and the three install forms are on the Swift & .NET page; the Microsoft documentation warns that tools run in full trust and that the manifest decides what dotnet tool run executes, so keep it under review.

Publishing the package (the key comes from the CI secret store, never from the repository)
dotnet pack -c Release
dotnet nuget push ./nupkg/docsctl.0.1.0.nupkg --api-key "$NUGET_API_KEY" --source https://api.nuget.org/v3/index.json

References: .NET tools and Create a .NET tool.

GoReleaser

GoReleaser automates the release: it builds the binaries, archives them, writes checksums, signs, and can publish to package managers (Homebrew and winget among them), Docker images and SBOM generation. Its configuration is .goreleaser.yaml; goreleaser init creates one, goreleaser check validates it, goreleaser release --snapshot --clean produces a local trial release without publishing, and goreleaser release performs the real one. It documents Go, Rust, TypeScript, Zig and Python projects, so it also fits a Rust CLI such as the docsctl Rust variant.

A release configuration for the Go variant of docsctl (.goreleaser.yaml, abridged)
version: 2

builds:
  - main: ./cmd/docsctl
    env: [CGO_ENABLED=0]
    goos: [linux, darwin, windows]
    goarch: [amd64, arm64]

archives:
  - formats: ["tar.gz"]
    name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
    format_overrides:
      - goos: windows
        formats: ["zip"]

checksum:
  name_template: checksums.txt

signs:
  - cmd: cosign
    signature: "${artifact}.sigstore.json"
    args: ["sign-blob", "--bundle=${signature}", "${artifact}", "--yes"]
    artifacts: checksum

Always run the snapshot form in CI on pull requests, so a broken release configuration fails before the tag rather than on it. References: GoReleaser, archives and signing.

Container images

A container image gives CI jobs and sandboxes an identical runtime without installing anything on the host. The general practices (minimal, pinned base image, multi-stage builds, a non-root user, no secrets baked in) are documented on the Docker reference, in particular Image Best Practices and Supply-Chain Security; the image for a native docsctl follows them.

Dockerfile for a prebuilt native docsctl (placeholders)
FROM debian:stable-slim
COPY docsctl /usr/local/bin/docsctl
USER 65532:65532
ENTRYPOINT ["docsctl"]

The ENTRYPOINT in exec form makes the image behave like the CLI: docker run --rm IMAGE search kalman --json prints JSON on stdout and returns docsctl’s exit code as the container’s. Three habits matter for agents. Do not allocate a terminal (-t) when the output is parsed, because a TTY changes the tool’s behaviour as described in TTY, Colour & Interactivity. Pass credentials by forwarding the variable (-e DOCSCTL_TOKEN with no value copies it from the host environment) or by mounting a token file read-only, and never COPY a credential into an image. And pin the image by digest, not by a tag that can move.

Signing and checksums

A release should carry evidence of what it is.

  • Checksums. Publish a file of SHA-256 checksums with every release (GoReleaser’s checksum section writes it). Package manager manifests (the Homebrew sha256, the winget InstallerSha256) repeat those values, so the file is also the source of truth for them. A checksum proves the download is intact; it proves who built it only if the checksum file is itself signed or served over a trusted path.

  • Signatures. Sigstore’s cosign sign-blob signs a file; the documentation recommends --bundle so that the signature, certificate and transparency-log proof travel together, and describes keyless signing, which ties the signature to an OpenID Connect identity, and --yes for unattended CI runs.

  • Build attestations. GitHub’s actions/attest action records build provenance for a workflow’s output (the job needs the id-token: write, contents: read and attestations: write permissions), and gh attestation verify PATH -R OWNER/REPO verifies it.

Verifying a downloaded release binary (placeholders)
sha256sum --check checksums.txt --ignore-missing
gh attestation verify docsctl_0.1.0_linux_amd64.tar.gz -R OWNER/docsctl

The full verification recipe with cosign verify-blob, the identity and issuer to accept, and the caveat that an attestation links an artifact to its source and build rather than certifying it as safe, is in the "Signed releases" section of Security. References: Sigstore — Signing blobs and GitHub — Use artifact attestations.

Checklist

  • One tag produces every channel, from one CI workflow, and a snapshot build runs on pull requests.

  • Every channel can install an exact version, and the documentation (and any skill) shows the pinned form.

  • The binary needs nothing at run time that the channel does not provide (Python, Node, a JDK) or is native.

  • docsctl --version prints the same version in every channel, and docsctl schema --json reports it too.

  • Checksums are published and signed, and manifests reference them.

  • The contract tests of Testing CLIs pass against the packaged artifact.

  • Man pages and completions are in the package (see Documenting CLIs).

  • The whole list is condensed in the Agent-Readiness Checklist.