PiG Documentation

Guides and references for configuring, using, and extending PiG.

Piglet Binaries

A Piglet Binary is a target-native PiG executable for one fixed Piglet composition. It delivers an agent application directly without creating another harness implementation.

A Piglet Binary is a carrier for a Piglet. It does not introduce another configuration schema.

Build a Binary

Validate the Piglet first:

pig piglet validate ./agents/reviewer.yaml

Build the executable:

pig piglet build ./agents/reviewer.yaml \
  --format binary \
  --out ./pig-reviewer

Run it directly:

./pig-reviewer

The build records the Piglet, its Resource closure, its component plan, the target, and verification information.

A Piglet needs no extension to build (D18). A base that only strips built-ins and sets defaults, such as a model, builds a Binary of PiG's own parts with that Piglet baked in:

name: pig-core
model:
  provider: openai
  name: gpt-5
strip:
  extensions: [mcp, codemode, tool-search]
  commands: [/share]

Whether a Piglet lists extensions is read from its effective form, after extends and extends.remove. A Piglet that lists extensions must still resolve at least one of them: an extension whose origins do not resolve fails the build as unresolved extension, and when none resolves the build also reports piglet resolves to no extensions. A child that extends an empty base and adds extensions builds as any other Piglet.

Build progress

PiG reports build progress on stderr (D18). A terminal shows a spinner, the current phase, elapsed time, and dim step text. CI and pipes receive one plain line per phase. The final summary gives the binary path, byte size, and total time after verification and record publication.

Add --verbose to stream toolchain output with member labels:

pig piglet build ./agents/reviewer.yaml --format binary --out ./pig-reviewer --verbose

Packed members share one compiler invocation and label. Fused Go members compile with the final binary. Module resolution, compilation, and linking can occur inside one compiler invocation, so PiG reports that operation as one phase rather than estimating progress. A failed build names its phase, shows the diagnostic tail, and prints a hint. With --json, progress stays on stderr and stdout remains one JSON result.

The source-checkout command pig build [--verbose] uses the same progress display. It builds Stock PiG, not a Piglet Binary.

Worked example

This is the reviewer Piglet of Piglets, built, checked and run. The output was captured from PiG built from a source checkout (PIG_SOURCE_ROOT named it), with the Go toolchain's cache already warm. A first build compiles PiG and takes longer. A released pig fetches its own source instead (see Piglets).

pig piglet add ./agents/reviewer.yaml
pig piglet build reviewer --format binary --out ./pig-reviewer

PiG prints one line per phase to the standard error, then the result:

Resolving manifest (0s) — reviewer
Resolving extensions (0s) — reviewer
Packing resources (0s) — Inlining prompts and skills
Selecting builder (0s) — auto
Locking build inputs (0s) — Hashing source, extensions, and toolchain identities
Packing build inputs (3s) — Embedding cells, resources, and component records
Compiling and linking Go binary (3s) — reviewer → /home/you/project/pig-reviewer
Checksumming and verifying binary (9s) — /home/you/project/pig-reviewer
Writing binary and records (9s) — Publishing verified artifact to the managed store
Built /home/you/project/pig-reviewer (69042439 bytes) in 10s

The file is a complete PiG with the Piglet inside, 69 MB here. It runs on its own:

./pig-reviewer --version
pig piglet verify ./pig-reviewer
pig piglet list
0.5.0+1.2.0
Piglet Binary: ./pig-reviewer
Signature: unsigned (no signature: anyone who can rewrite this file can rewrite its build record)

verify exits 1 for an unsigned Binary. Sign one with --sign-key (Sign a Binary). pig piglet list now fills the BINARY column for the Piglet:

NAME      SCOPE  SOURCE                                BINARY       IMAGE  DESCRIPTION
reviewer  user   /home/you/.pig/piglets/reviewer.yaml  linux/amd64  -      Reviews a change and lists risks.

Run it like pig, with no --piglet option, because the Piglet is built in (a Binary accepts --piglet as an overlay unless it is built with build.closed: true). Checked with a test model, the provider request carried the Piglet's own system prompt text and only the tools the Piglet allows (read, and codemode because an MCP server was configured on that machine):

./pig-reviewer -p "Review the last commit"

Copy the file to another computer of the same operating system and architecture and it runs there with no PiG installed. It still needs a model and credentials on that computer.

Static linking

A Binary is statically linked when nothing in it uses cgo, so on Linux it needs no libc and runs in an empty container. The builder chooses CGO_ENABLED=0 for the Go build unless one of these holds:

  • you set CGO_ENABLED in the environment or with go env -w CGO_ENABLED=...; your value is used unchanged;
  • a package the Binary builds has cgo sources (a fused Go member or one of its dependencies imports "C"); the Go default stays, which links dynamically where cgo is on;
  • the target is Android outside arm64, where the Go linker requires cgo.

Standard-library cgo files, such as those of net and os/user, do not count: they have pure-Go fallbacks that the static build selects. The choice is not stored in the build or provenance records, which do not carry the build environment.

To confirm, run file ./pig-reviewer (a static Linux Binary reports statically linked) or ldd ./pig-reviewer (not a dynamic executable).

Sign a Binary

Create an Ed25519 author key once. Keep the private file secret and publish the .pub file:

pig piglet keygen ./reviewer-signing.key

Sign during the native build:

pig piglet build ./agents/reviewer.yaml \
  --format binary \
  --out ./pig-reviewer \
  --sign-key ./reviewer-signing.key

The signature covers every executable byte and a DSSE manifest that names the Piglet, target, PiG version, resolution record, component plan, components, and embedded files. The Binary embeds the public half of its author key and checks the signature before command dispatch. An altered signature, manifest, Piglet, record, component, or executable byte makes the Binary refuse to run.

Check a file without running it:

pig verify ./pig-reviewer
pig piglet verify ./pig-reviewer

pig verify reports a valid Piglet signature as ok and an unsigned file as n/a; because it also verifies arbitrary archives and data files, it applies the required-signature policy only after a Piglet signature block identifies the target. pig piglet verify is Piglet-specific and succeeds only when the file carries a valid signature. Both commands reject a signed file whose key is revoked, and Piglet Binary startup enforces the required-signature policy for unsigned Binaries.

Check that a Binary still matches its source:

pig piglet verify ./pig-reviewer --source reviewer
Piglet Binary: ./pig-reviewer
Signature: signed by ed25519:<key-id> (not in your Piglet trust store: this proves it is unchanged since that key signed it, not who holds the key)
Provenance: matches /home/you/.pig/piglets/reviewer.yaml

--source bakes the named Piglet as the build does and compares two digests with the ones the Binary signed: the source digest of the Piglet file and the Piglet digest of the definition baked into the Binary. A changed Piglet file fails the source digest. A change that alters the baked definition, such as an edited prompt file, skill, or base Piglet, also fails the Piglet digest. A Piglet built with --workspace <dir> is checked with the same --workspace. pig piglet lineage ./pig-reviewer --source reviewer reports the same check beside the extends chain.

Trust and policy are explicit local actions:

pig piglet trust add ./reviewer-signing.key.pub
pig piglet trust list
pig piglet trust revoke ed25519:<key-id>
pig piglet trust require on

A valid signature by the embedded author key proves that the file has not changed since that key signed it. It does not prove who controls the key. Add a reviewed public key to the trust store to attach local identity, and enable require on when every Piglet Binary must be signed by a trusted, non-revoked key.

Publish to GitHub Releases

Set release.version in the Piglet. Then publish signed Binaries as one GitHub Release named v<release.version>:

pig piglet publish ./agents/reviewer.yaml \
  --to github \
  --repo acme/reviewer \
  --sign-key ./reviewer-signing.key

Without --yes, publish is a dry run. It validates the Piglet, the signing key, and every target, checks that the release does not exist yet, and prints the assets it would upload. It builds and uploads nothing. Add --yes to build, sign, and upload these assets:

  • one signed Binary per target, named pig-<name>-<os>-<arch> (with .exe for Windows);
  • SHA256SUMS, which lists the SHA-256 of each Binary;
  • piglet-release.json, the release index, signed with the same key.

Targets come from --targets os/arch,..., else build.targets, else the host target. The native builder builds only the host target, and a container builder does not sign, so one machine can usually publish only its own target. When a target has no ready signing builder, publish lists each missing target with every builder's reason and uploads nothing. Each build also writes the managed build record that pig piglet build writes.

To publish several targets, build each target on a matching host, collect the Binaries in one directory, and publish them without building:

pig piglet publish ./agents/reviewer.yaml --to github --repo acme/reviewer \
  --sign-key ./reviewer-signing.key --artifacts ./dist --yes

Publish identifies each file in --artifacts by its signed manifest. Every file must be a Binary of this Piglet source and release.version, signed by --sign-key. The directory must hold exactly one Binary for each target, and every Binary must come from the same PiG version. Before the upload, PiG checks each staged asset against the signed index with the checks that pig piglet pull applies.

PiG runs the GitHub CLI (gh release view and gh release create) with your existing gh authentication and never handles a GitHub token. It refuses a release that already exists, because a published release is immutable. A SemVer prerelease version, such as 2.0.0-rc.1, publishes as a GitHub prerelease. --commit <sha> creates a missing release tag at that commit. The index records the Piglet source as git:github.com/<owner>/<repo>@<commit or tag> unless --source-ref names another npm or Git source. Publication contacts GitHub only through gh, and it never contacts pi.dev or a PiG service.

Publish from a monorepo

Use an explicit Piglet-name namespace when several Piglets share a repository (D18). For pig-with-batteries in MichaelKinsy/pigpen, use the following shape. Replace <version> with a published release version:

pig piglet publish ./piglets/pig-with-batteries/piglet.yaml \
  --to github --repo MichaelKinsy/pigpen --tag-prefix pig-with-batteries/ \
  --sign-key ./pig-with-batteries-signing.key --artifacts ./dist/pig-with-batteries --yes
pig piglet pull 'github:MichaelKinsy/pigpen/pig-with-batteries@<version>'

--tag-prefix must be <manifest-name>/. It produces pig-with-batteries/v1.2.3 while release.version stays 1.2.3. Omitting the option keeps v1.2.3. PiG does not infer a namespace by scanning a checkout, so adding another Piglet cannot change existing release names. Dry runs, existing-release checks, uploads, default Git source refs, and download URLs use the same namespace. URLs escape the slash as %2F.

The signed index records the GitHub repository and tag prefix independently of the source ref. A named pull rejects a different Piglet name, version, repository, or prefix. Its receipt retains that signed identity. For independently addable monorepo source, provide --source-ref 'git:https://github.com/MichaelKinsy/pigpen.git@<full-commit-sha>#subdirectory=piglets%2Fpig-with-batteries'. Without --source-ref, the source ref identifies the repository's commit or namespaced tag, not a selected subdirectory.

The reusable workflow docs/examples/piglet-release.yml builds each target on a native GitHub runner, attests the build provenance of each Binary, and publishes the release with --artifacts. Copy it to .github/workflows/piglet-release.yml in the Piglet's repository. Its header shows the workflow that calls it.

Pull a published Binary

A publisher can place signed per-target Binaries and a signed piglet-release.json index on GitHub Releases or another HTTPS host. Pull the Binary for the current target:

pig piglet pull https://github.com/acme/reviewer/releases/download/v1.2.3/piglet-release.json

Select another target or use a GitHub release reference explicitly:

pig piglet pull github:acme/reviewer@1.2.3 --target linux/amd64

PiG verifies the release-index DSSE signature, the complete asset size and SHA-256, the Binary signature trailer, and the Piglet, release, target, and PiG identities before it installs any file. The first successful pull pins the signer key for that Piglet. A later signer change is refused unless you name the new key exactly with --accept-signer ed25519:<key-id>. Local revocation and trust require on apply to pulled releases.

Pulled Binaries live under ~/.pig/artifacts/piglets/, and their signed receipts live under ~/.pig/receipts/piglets/. pig piglet pull prints the Binary's path on its binary: line. Run that file directly; pig --piglet <name> does not run a pulled Binary. pig piglet list and show report them as Binary facets. Pull requests only the supplied index and the asset URL signed into that index; it performs no automatic catalog or PiG service request.

Update an installed GitHub Binary

pig piglet update pig-with-batteries
pig piglet update pig-with-batteries --version 2.0.0-rc.1

Update reads the repository and tag namespace from the current signed receipt. It enumerates GitHub's public release API and selects the highest stable SemVer in that namespace. It never uses repository-wide latest, even for unprefixed releases. Drafts, prereleases, unrelated prefixes, and invalid versions do not enter automatic selection. Use --version to request a prerelease explicitly. Discovery fails if the bounded release inventory is incomplete.

Update preserves the installed target unless --target os/arch selects another. It refuses rollback, repository or namespace changes, revoked keys, and unapproved signer changes. --accept-signer ed25519:<key-id> explicitly authorizes a new signer. The same checksum, size, signature, and manifest checks as pull apply before the current pointer changes. An already-current release is a no-op. An index without signed GitHub identity cannot supply automatic updates. Update does not refresh registered source Piglets or query a catalog. PIG_OFFLINE and PI_OFFLINE prevent update requests.

Sigstore keyless provenance

An Ed25519 Piglet signature is offline artifact integrity and local trust policy. Sigstore keyless provenance is separate evidence that a named CI workflow built an artifact. A release workflow can publish GitHub build provenance, and the recipient can verify it explicitly:

pig verify --provenance \
  --repo acme/reviewer \
  --signer-workflow acme/reviewer/.github/workflows/piglet-release.yml \
  ./pig-reviewer

This command delegates to gh attestation verify, which checks the Sigstore bundle, certificate identity, and transparency-log inclusion. It requires the GitHub CLI and may contact GitHub's attestation service. PiG never performs this network check at startup. Startup signature verification remains offline.

What the Binary contains

A Piglet Binary always contains:

  • the Stock PiG execution engine;
  • one fixed Piglet;
  • its resolution and component plan;
  • every extension or native component marked as Binary materialized;
  • verification data required at startup.

A Binary can still require external items when its plan declares them:

  • an external MCP service;
  • a secret value;
  • a configuration file;
  • an interpreted runtime;
  • a component supplied by a release or required agent environment.

Do not describe a Binary as self-contained unless its record proves that none of these requirements remain.

Realization and materialization

Each executable component records two independent facts.

Axis
Values
Question answered
Realizationfused, subprocess, externalHow does the component execute?
Materializationbinary, release, agentEnv, externalWhere do its bytes and runtime come from?

A subprocess component does not create a different Binary type. It remains part of the same Piglet application and component plan.

Fused Go extensions

A compatible Go factory can be fused into the Binary. PiG compiles the extension into the executable and starts it through the same registration contract used by subprocess extensions.

Advantages:

  • one target-native executable;
  • no target-side Go compiler;
  • no extension process startup;
  • no serialization cost for process placement;
  • exact extension source included in the build closure.

Costs:

  • the Binary must be rebuilt when the extension changes;
  • the artifact is target specific;
  • the extension shares the PiG process failure boundary;
  • fused code requires stricter review;
  • only compatible Go factories can fuse.

Fused behavior must match source subprocess behavior. Fusion is a delivery optimization, not a second extension API.

Fused members with external Go modules

A fused extension or frontend member may require Go modules beyond the PiG SDK. The build links them into the Binary through PiG's own module, without writing into the PiG source tree:

  • The member's require directives join PiG's requirements by Go's minimum version selection. Each module gets the highest version PiG or any member requires, so a member never downgrades PiG. When a member raises one of PiG's own requirements, the build prints a note: line naming the module and both versions.
  • PiG and its extension SDK are pinned by the PiG source being built. A member that requires a newer SDK version than that source fails the build with an error naming both versions; build it with a PiG source at that version or later.
  • The member's replace directives for other modules apply to the build, with relative paths resolved against the member. Two members, or a member and PiG, that replace one module differently fail the build.
  • Checksums come from the member's go.sum together with PiG's go.sum and, for a checkout with go.work, its go.work.sum. Keep the member tidy (go mod tidy) so its go.sum covers its module graph. The build verifies these checksums under -mod=readonly, works offline once the modules are in the Go module cache, and succeeds from a read-only PiG checkout. A missing checksum stops the build instead of being written into the checkout.

The member's go.mod and go.sum, and the source of each local replacement, are part of the Binary's recorded build inputs, so a change to the member's module graph changes the Binary's identity.

What the fused vet checks

Before it links fused members, the build vets them for process-global hazards: calls such as os.Exit, os.Chdir, log.Fatal, and writes to standard output, which a fused member would run inside the PiG process instead of an isolated subprocess (see Fused Go extensions). The vet reads Go source and follows imports, so its reach is the source it can read in the build:

  • Vetted: each member package and every package it imports, directly or indirectly, from the member's own module and from the workspace modules it names.
  • Not vetted: every other module the members require, direct or indirect, including the modules a local replace points to. The vet does not read their source, so a hazard in that code reaches the Binary unchecked. Reading it would mean fetching and parsing the whole third-party graph on every build, and a hazard in a library is not always a defect: a command-line library may call os.Exit on a path your member never takes. PiG therefore reports the gap instead of guessing.

When a build fuses members that require such modules, pig piglet build prints one note: after the vet passes. It lists each unvetted module once as path@version, sorted by module path, at the version the build selects: the highest version PiG or any member requires. When a replace directive supplies the module's source, the entry ends with => and the replacement module or directory. A build whose members have no external modules prints no note. PiG and its extension SDK are not listed, because the PiG being built is the one that is linked. The list comes from the require directives of the members' and workspace modules' go.mod files, so it can also name a module that only the members' tests use. The note follows from those go.mod files and the PiG source, which are already part of the Binary's recorded build inputs, so the Binary's record needs no separate entry for it.

Review the listed modules yourself, or keep a module with hazards out of a fused member by running that member as a subprocess extension.

Subprocess extensions

A normal Piglet Binary can start exact subprocess components when its plan permits them.

Advantages:

  • process failure isolation;
  • support for Go, Rust, Python, Node, and native programs;
  • independent heartbeat and cancellation handling;
  • easier source development and reload.

Costs:

  • process and IPC overhead;
  • explicit runtime requirements;
  • more lifecycle and transport handling;
  • the Binary might not contain every required runtime.

A prebuilt native subprocess component can avoid a target-side compiler. An interpreted component still needs its recorded runtime.

TypeScript and JavaScript extensions

A Piglet Binary can carry TypeScript and JavaScript extensions. The build packs each Node extension's sources into one archive per Node cell and embeds the archive in the Binary. It archives a directory extension whole, including its node_modules, but not its .git directory, and it archives a single-file extension as that file alone. A symbolic link that points outside the extension directory fails the build (a link to the directory itself is kept), because the Binary would run without the code the link points to. Keep an extension's local imports and dependencies inside its directory.

The Binary does not contain Node. It runs the extension with the Node on the user's PATH, which must be version 22.13 or newer, the same requirement Stock PiG has. At startup the Binary checks the archive against the digest in its embedded cell manifest and extracts it once per digest under piglet-binary-cells/node/ in the PiG configuration directory (~/.pig by default). A later start reuses that directory and does not check its files again, so a file changed or deleted there stays changed until you delete the <digest> directory; the next start then extracts the archive again. It then plans, starts, and reloads the extension through the same Node cell path that Stock PiG uses for the same extension. The extension registers the same tools, commands, and handlers, in the same order.

The component plan records a Node extension as a subprocess component with binary materialization and a node runtime requirement. The signed manifest lists the archive among its embedded files, and the signature covers its bytes.

The Binary costs what the extension costs in Stock PiG. On an Apple M4 Pro, a Binary with one fused Go extension and one small TypeScript extension is 16,528 bytes larger than the same Binary without the TypeScript extension. Its -p run with the faux provider took 178 ms (median of 10), against 179 ms for Stock PiG with -e and the same extension, and 34 ms for the Binary without it. Almost all of that time is Node starting the extension.

When Node is missing or too old, the Binary behaves like Stock PiG. It reports the failed extension, names the Node version it needs, and prints the -ne hint. It then exits with status 1 before the session starts:

Error: Failed to load extension "/Users/me/.pig/piglet-binary-cells/node/<digest>/sources/greeter": Failed to load extension: reload packed Node cell packed-node:<key>: spawn:runtime_unavailable: TypeScript extensions need Node.js 22.13 or newer; node was not found on PATH
Hint: Start without extensions using "pig -ne".

A Piglet that strips node-extensions cannot carry a Node extension. The build stops with the conflict described in Stripped built-ins.

External components

An external realization connects to a separately managed service. The Binary records the requirement but does not embed or operate that service.

Use an external realization for network services that have their own lifecycle, scaling, credentials, or deployment owner.

Require all extensions to fuse

A Piglet can reject subprocess fallback:

build:
  extensionRealization: fused

The build then requires every selected extension to be a fuse-compatible Go factory. If one extension resolves to a subprocess, the build fails and names that extension and the reason.

PiG Standard uses this requirement. Every extension added to PiG Standard must:

  • use the public Go extension factory contract;
  • work in source mode through the normal subprocess host;
  • work as a fused component;
  • pass the same behavior and lifecycle tests in both modes.

Frontend members

A Piglet Binary can fuse one frontend member that draws the interactive mode in place of PiG's terminal renderer (D91). Pi has no equivalent: Stock PiG and a Piglet without a frontend member paint exactly as Pi does.

slots:
  frontend:
    member: ./frontend

slots.frontend.member names a directory inside the directory of the Piglet file that names it. It holds a Go module whose root package exports:

func Frontend() frontend.Frontend

The frontend package is github.com/MichaelKinsy/PiG/extensions/sdk/frontend. A frontend member depends on PiG only through the Go SDK module; it may require other Go modules, as described in Fused members with external Go modules. It is not an extension: it has no wire protocol and runs only fused, and only the native builder fuses it. A Piglet may have a frontend member and no extensions, as a Piglet may have neither (see Build a Binary).

A Piglet that extends another inherits its frontend member, which still resolves against the base Piglet's directory. The child replaces it by naming its own slots.frontend, or removes it so PiG's terminal renderer applies again:

extends:
  source: local:./pig-tern/piglet.yaml
  remove:
    slots: [frontend]

pig piglet show lists the frontend member in its Slots section as frontend replaced(<member directory>), beside any stripped built-ins (see Piglets). The Binary's own embedded Piglet carries no slots, because the member is compiled into the Binary.

On every interactive start, PiG calls the member's Open after the terminal enters raw mode and before the first paint. A member that returns no session leaves the terminal renderer in place. A session receives the interactive screen as retained-tree frames: transcript entries in the main region and the input dock in the dock region. Tool calls arrive as tool nodes with their arguments, status and output, and an edit's result with its unified diff; a call the user aborted has the status cancelled. User and assistant messages arrive as Markdown nodes with their role and Markdown source, and thinking as thinking nodes that say whether the user hid it; an assistant message is one node per part, and the part still being generated is marked as streaming, so a frontend can append what a frame adds. Every other component arrives as the terminal lines it rendered, at the width the session reports for its region. Terminal input that the session claims never reaches the editor.

The input editor arrives as an editor node with its text, its cursor in UTF-16 code units, a placeholder and whether it is sendable. PiG draws no ANSI border around it, since the frontend frames it. Above it comes a working node while PiG works, compacts, summarizes a branch or waits to retry: its kind, its message, and an extension's spinner frames and interval, without the current frame, so the frontend animates the spinner. Below it come the lines of its completion list and the components under it, then a footer node while PiG's footer shows: the working directory, git branch and session name, the session's token and cost totals, the cache-hit rate, the context usage, the model with its provider and thinking level, and the extension statuses. A footer that an extension sets replaces the footer node and arrives as lines. While a component PiG does not model replaces the editor, or an overlay that takes the keys shows, the dock arrives as lines. The session acts on the editor through Env: Edit replaces a range of its text as one undo step, Undo steps back through the editor's own history, and Send submits a prompt through the path Enter takes, so commands, multi-line prompts and steering behave as typed. PiG runs these on its own loop in the order they arrive, ignores an edit computed against text that has changed since, and reports the editor sendable only once its input loop runs and while the editor has the keys.

A selector or dialog that takes the editor's place, such as /model, /resume, /tree, /fork, /login, the thinking level picker or an extension's select or confirm dialog, arrives in its place as a selector node: its title and description, the search text, the items shown in the order the arrow keys step through them with a label, a detail, a check for the current or enabled entry and a tree depth, the selected item, its tabs (such as the scope of the model and session lists), a status message, a loading flag and a confirm question. /settings arrives as a settings node: each setting with its value, the values it cycles through, and whether it opens a submenu, which then arrives as a selector under the same id. Keys still reach the selector as typed. A session acts on one with Env.Act: highlight, choose, dismiss, filter, switch tab or set a value. PiG performs each action as the keys it stands for, so the selector behaves exactly as with the keyboard. Overlays, such as an extension's custom component shown as an overlay, arrive in an overlay region as overlay nodes with their lines, width, anchor and whether they take the keys. Components PiG does not model, such as an extension's input or editor dialog, the login dialog, and the session rename and tree label fields, arrive as lines.

The first frame also names the session file, the file the session persists to, or none for a session that is not persisted (--no-session). A later frame names it again only when it changes, such as after /new, /resume, /fork or /clone. A frontend can pass it on, for example to let another program read the conversation.

A session is also the terminal that PiG's interactive mode reports its program status to: working while a run or compaction goes on, blocked while a dialog or a login waits for the user, then done, error or idle (the OSC 7501 Program Status Protocol of Pi 1.5.0). A session that implements frontend.ProgramStatusSession receives each change through ProgramStatus as the report Pi writes to a supporting terminal, including the report that clears the status. PiG calls it on its owner loop in the order the status changed, so it must return without blocking. It clears the status before Suspend and Close and reports the latest one again after Resume. A session without the method receives nothing. Either way, while a session may draw, PiG writes neither the OSC 7501 support query nor a report to the terminal itself.

When a session asks to fall back, or fails to draw a frame, PiG closes it, repaints with its configured terminal renderer and shows a warning. While a session draws, PiG refuses to switch the TUI mode. --print, JSON and RPC modes never consult the member.

The member's source is part of the Binary's identity: its tree digest is a frontend input in the resolution record.

Stripped built-ins

A Piglet's strip list (D92, see Piglets) carries into its Binary. The native builder passes one Go build tag per stripped built-in extension, model API and compile-out feature (pig_strip_<id>, for example pig_strip_mcp or pig_strip_node_extensions), so the Binary does not link that code; the build prints the tags it uses. The baked Piglet keeps the list, so the Binary also disables the runtime-only entries (tools, commands, themes, skills, prompt-templates) at startup. A Piglet cannot compile out an extension runtime its own extensions run on: a Node or Python extension, or a Go or Rust extension the Binary builds at runtime, fails the build with the strip entry named. A Piglet whose strip list removes every built-in tool and /quit together does not build either (see Piglets).

Stripping cuts size and startup work. On darwin/arm64 a Piglet Binary with one fused Go extension is 62.2 MB; stripping MCP, codemode, llama.cpp, tool-search and three provider APIs makes it 54.2 MB, and stripping every binary ID makes it 31.7 MB.

The Binary record lists each stripped ID with its kind and disposition in its strip field, so the strip list is part of the Binary's identity and record digest. A Binary without a strip list has no strip field. pig piglet show prints the list under each Binary in a Slots section (extensions.mcp stripped(binary), tools.bash stripped(runtime)), and pig piglet show --record <path> prints it for one record.

A keep-mode list (Piglets) reaches the Binary expanded: the build compiles out every built-in of that list the Piglet does not keep, and the strip field lists each one. The record states the intent too: stripKeep lists each keep-mode list with the IDs it keeps ([{"kind": "extension", "ids": []}]), and pig piglet show prints it above the expanded rows (extensions keep([])). A deny-mode Piglet has no stripKeep field. The baked Piglet keeps the keep lists, not their expansion, and the Binary expands them again at startup against its own table, which is the table it was built from.

Every Binary record also keeps stripTable, the strip table of the PiG that built it, one list of IDs per kind. A later PiG compares its own table with it and prints the strip delta report on pig piglet build and pig piglet show: the new built-ins a keep-mode list leaves out and the ones a deny-mode list now includes.

Accept overlay Piglets

A Piglet Binary behaves as stock PiG started with its built-in Piglet (D202). It accepts --piglet, PIG_PIGLET_PATH and PIG_PIGLET_NAME, and it takes the selected Piglet as an overlay on its own Piglet. It also accepts --system-prompt, and it discovers ambient extensions and skills as its effective Piglet's discovery says.

An organization can close a Binary. Set build.closed: true in the Piglet you build:

name: pig-base
build:
  closed: true
strip:
  extensions: [mcp]

The default is false, so a Binary is open. A closed Binary runs only its built-in Piglet. It refuses the overlay selectors and --system-prompt, and it discovers no ambient extensions or skills. A Binary built with 0.5.3 behaved this way, so build.closed: true keeps the 0.5.3 behavior. Like all of build, the setting belongs to the Piglet you build: a Piglet that extends a base with build.closed: true does not inherit it. The builder keeps the setting in the Piglet it bakes, so it is part of the digest the Binary's record and signed manifest name. A signed Binary whose setting was changed refuses to start, and pig piglet verify <binary> --source <piglet.yaml> fails when the source flips it. pig piglet show in a Binary session says whether the Binary is open or closed.

A closed Binary also runs only the extensions and MCP servers its signed closure lists. It refuses -e/--extension (a path, npm: or git: source), pig install, pig remove and pig update --extensions (also --all, --extension <source> and a package source), and pig mcp add; each prints one message that names the refused option, the Binary and build.closed, and exits 1. It ignores settings.extensions entries and the servers of the user and project mcp.json and says so in a notice. pig update for pig itself and --models still work, and servers that an extension in the closure registers still start. An open Binary accepts all of them, as stock PiG does.

An open Binary takes the same selectors stock PiG does and resolves them the same way, with the same project trust, user and system Piglet directories, and Package install flow (lifecycle scripts stay off unless you allow them):

./pig-base --piglet ./extra.yaml
PIG_PIGLET_PATH=./extra.yaml ./pig-base

The overlay is an ordinary Piglet file with its own name; its own extends chain works as usual. The Binary keeps its name for /piglet and for strip messages, and /piglet also prints Overlay of: <Binary>. A Binary starts exactly as before when you select no Piglet: it installs nothing and resolves nothing.

An overlay merges onto the Binary's Piglet exactly as an extends child merges onto its base, so an open Binary behaves like stock pig started with a different Piglet.

  • systemPrompt replaces the Binary's prompt. --system-prompt is accepted with stock precedence (the flag wins over the Piglet's prompt); a closed Binary refuses it and names build.closed.
  • extends.remove removes Binary members: extensions, skills, packages, slots: [frontend] (the Binary then starts without its compiled-in frontend) and strip entries.
  • extends.allowWiden: true allows what widens the Binary's ceilings: tools, discovery, agentEnv, an extension's tools, and removing a strip entry. Without it resolution fails with capability widening [...] requires extends.allowWiden: true, as for any extends child.
  • An extends block with no source extends the Binary itself. It carries only remove, allowWiden and nothing else; a Piglet file with such a block resolves only as an overlay on an open Piglet Binary, and stock pig refuses it by name.
name: tern-lite
systemPrompt:
  text: You answer in one paragraph.
extends:
  allowWiden: true
  remove:
    slots: [frontend]
    extensions: [codegraph]
    strip:
      tools: [bash]

A name in extends.remove must exist in the Binary. Naming a member the Binary does not have fails with remove.extensions: "codegraph" is absent. The tern-lite overlay above needs a Binary that has an extension named codegraph.

To test an overlay, start the Binary with the overlay as its Piglet, for example ./pig-base --piglet ./tern-lite.yaml. Stock pig piglet validate rejects an overlay file by design, because an extends block without a source has no base to extend.

Three things stay fixed, because they are compiled into the Binary. Each refusal names the field, the Binary and the reason:

The overlay
Why it is refused
sets slots.frontend (a new one, or another under the Binary's)A frontend is Go code linked into the Binary. Removing the Binary's frontend with extends.remove.slots: [frontend] is allowed.
removes a strip entry whose built-in a build tag compiled out: strip.extensions (mcp, codemode and the other built-in extensions), strip.apis, and the compile-out strip.features (docs is one)The code is not in the executable. pig piglet show marks these stripped(binary).
names a fused or embedded extension again with originsThe Binary selects its own members by name and cannot build another at run time under that name. Remove it with extends.remove.extensions and add the new one under another name.

An overlay can undo runtime strips: strip.tools, strip.commands and the runtime-only features (themes, skills, prompt-templates), which pig piglet show marks stripped(runtime). It cannot undo a stripped(binary) entry.

An open Binary otherwise behaves as stock pig running the same Piglet: every CLI flag, environment variable, setting, Package and pig install, -e source, /reload and discovery of extensions, skills, prompts and themes was compared against pig --piglet. Ambient discovery follows the Piglet's discovery list, as it does for stock pig; a closed Binary discovers no ambient extensions or skills. These differences remain, each with its reason:

Difference from stock pig
Reason
The compiled-in frontend, fused extensions and prebuilt cells start from the executable, not from source: no build, no cells/go or toolchain cache files.They are linked into the Binary.
strip entries marked stripped(binary) stay off, and pig mcp or pig docs say they are stripped.A build tag removed the code.
pig update refuses with the immutable-Binary message when the Piglet sets release.version.The Binary's release owns its updates.
pig version and pig diagnose report the Binary's build identity.The artifact is built by pig piglet build, not stamped from the PiG checkout.
A baked skill or fused extension has no file, so its location in /skill: blocks, get_commands and sourceInfo is builtin:piglet or the extension name.Nothing exists on disk to point at.
/piglet prints the Binary's name, and Overlay of: <Binary> when an overlay is selected.Stock pig has no Binary.
A standalone overlay file inherits the Binary's members; stock --piglet starts from nothing. PIG_PIGLET_NAME and PIG_PIGLET_PATH select the overlay without --piglet, as they select the Piglet for stock pig.The Binary's Piglet is the base the overlay extends.
An overlay load error names the overlay and the Binary: overlay <path> on Piglet Binary "<name>": ....The Binary is the base of the derivation.
pig login, pig mcp and the other subcommands run before any overlay is resolved and see the Binary's Piglet.Subcommands never read --piglet, on stock pig either.

A closed Binary refuses the selector and names itself and build.closed:

pig: this Piglet Binary (pig-base) was built with build.closed: true, so it runs only its built-in Piglet and does not accept --piglet overlays (nor PIG_PIGLET_PATH or PIG_PIGLET_NAME); rebuild its Piglet without build.closed to accept them, or run the Piglet with stock pig (pig --piglet <name|path>)

Accepting overlays changes neither the Binary's verification nor its update ownership: it still verifies its signature, Piglet and component closure before anything else, and pig update still refuses to modify an immutable Binary. The overlay is input you supplied, outside that closure. An open Binary that you ship to others runs whatever Piglet its user selects, within the rules above. Build it with build.closed: true when it must not.

Startup verification

A Piglet Binary verifies its embedded Piglet, component closure, and optional Ed25519 signature before command dispatch. A signed Binary refuses to run when any executable or signature byte changes, within the limit that the verification cache below describes. An unsigned Binary reports unsigned and runs unless the local trust policy requires every Piglet Binary to carry a trusted signature.

Signature verification reads and hashes the whole executable. For a 63 MB darwin/arm64 Binary on an Apple M4 Pro the hash takes about 25 ms. An unsigned Binary has no signature block to check and skips the hash.

Signature verification reads and hashes the whole executable at every start. For a 62 MB darwin/arm64 Binary on an Apple M4 Pro this adds about 26 ms. An unsigned Binary has no signature block to check and skips the hash.

Startup does not silently:

  • install a Package;
  • download a missing component;
  • substitute a same-named executable from PATH;
  • log in to a provider;
  • weaken an environment requirement;
  • replace a failed fused extension with a subprocess.

A missing or tampered requirement causes a clear failure.

Verification cache

A signed Binary hashes its executable only when its file changed since its last passing start (D18). After every startup check passes, the Binary records the check in state/piglet-verify/verified.json under the PiG config root ($PIG_HOME, else $XDG_CONFIG_HOME/pig, else ~/.pig). The entry holds:

  • the Binary's path with symbolic links resolved;
  • its file identity: device, inode, size, modification time, and status-change time; on Windows the volume serial number, file index, size, last write time, and change time;
  • the SHA-256 of its signature block;
  • the trust policy that applied: require on, the trusted, revoked, and embedded keys.

At the next start the Binary still reads its signature block and checks the signature, the signed size, the signer under the current trust policy, and the signed manifest. When every recorded field matches, it skips the hash of its executable bytes. Any difference causes a full check: a new path, a touched or rewritten file, a file replaced by another one, a changed trust store or require on. A missing, corrupt, unreadable, or not owner-only cache also causes a full check. An entry never vouches for another path.

  • A failed check is never recorded. A check is recorded only after the signed manifest matches the Binary.
  • The cache file is readable and writable by its owner only and is replaced atomically. It keeps one entry per Binary path, at most 8, and drops the oldest entry first.
  • pig verify and pig piglet verify never read the cache. They always hash the whole file.
  • When PiG cannot write the cache, the Binary still starts and prints pig: warning: write Piglet verification cache: ... on stderr. The next start checks the Binary in full again. On a config root this user cannot write (a read-only file system or a permission denial) the warning is not printed, because that is a deployment choice; pig diagnose reports the read-only config root and that every start checks the Binary in full (D157).

On an Apple M4 Pro (median of 10, -p with the faux provider), Stock PiG took 31.1 ms. The 63 MB signed Binary took 56.1 ms with a full check at every start, 59.3 ms for a start that also writes the cache, and 33.3 ms for a start with a matching cache entry.

What the cache keeps and what it gives up:

  • It keeps detection of every change made through a file system that records a status-change time (on Windows, NTFS's change time). Writing to the Binary changes that time even when the writer restores the modification time, and replacing the Binary changes its inode, so the next start hashes the new bytes and refuses a change.
  • It gives up detection, at start, of corruption that keeps the size and every recorded time, such as a storage fault below the file system, and of an in-place write that restores the modification time on a file system without a status-change time, such as FAT. On Windows a writer can also set the change time back through the file information API; such a writer is not detected at start. pig piglet verify still detects all of these.
  • The startup check is a self-check. Anyone who can rewrite the Binary can also rewrite the code that checks it, with or without a cache. The check does not stop an attacker who can write the Binary. Protect the Binary with file permissions, and run pig piglet verify on a Binary you did not build before you run it.

Failure and recovery

Fusion and subprocess execution have different physical failure boundaries, but they use the same extension behavior contract.

For subprocess components, PiG provides bounded heartbeat, cancellation, crash detection, quarantine, reload, and replacement behavior. A failed replacement does not remove the last working extension set.

PiG does not automatically replay an interrupted tool or command. A replay could duplicate file changes, messages, or network actions. The caller decides whether an operation is safe to retry.

A fused extension cannot lose a process socket because it runs inside PiG, but it can still panic, block, or misuse resources. Fused extensions therefore require lifecycle, shutdown, and leak verification before release.

See Extensions for the complete tradeoff and recovery model.

Portability

Build one Binary per target:

build:
  targets:
    - linux/amd64
    - darwin/arm64
    - windows/amd64

Do not claim target support until the artifact passes native verification on that target.

A Binary built on one target does not make an external service, secret, or interpreter portable. The record keeps these requirements visible.

Source mode versus Binary mode

Need
Use
Edit and reload extension sourceHost Piglet source mode
Inspect a composition before releaseHost Piglet source mode
Hand off one executablePiglet Binary
Require one native all-fused applicationPiglet with extensionRealization: fused
Carry a complete operating environmentPiglet Image when that producer is available
PiG project documentation · MIT License