Treeship
About

Changelog

Recent Treeship releases, newest first. The complete history remains in the repository changelog.

The latest Treeship releases are documented here.

The format follows Keep a Changelog; versioning follows Semantic Versioning. The canonical source and complete release history live in CHANGELOG.md at the repo root. Edit that file, not this page.

Unreleased

0.31.14 (2026-10-05)

  • An approved shell command gets through the hook. The approval is checked in the store that recorded it, so a project stub no longer deletes a real approval and blocks the command again.
  • grant revoke records the withdrawal in the local journal, and a later use of that grant is refused there.
  • verify --strict fails when a handoff names artifacts this store does not hold. The missing ids are in the JSON either way.
  • session start --config writes the session beside that config. A host id for an explicit config is written in that config's directory.
  • Hub attach accepts the code the CLI prints. 0.31.13 required a second code the activate page never sends, so every attach was rejected. Submitting the printed device code approves the attach.
  • TS-2026-006 is published (GHSA-w5fp-wc8j-3h4p). Session discovery, approvals, signing, and effect grading through 0.31.12 could accept actions the product's own rules refused. Upgrade the CLI and any self-hosted hub to 0.31.14.

0.31.13 (2026-10-01)

  • Signing and approvals fail closed. A halted actor cannot attest or wrap. wrap honors require_approval. An idempotency key is reused only for the same actor, action, and subject. A journal use vouches only for the action it names. An expired key, a revoked grant, an out-of-scope action, and a revoked capability card cannot sign. A self-supplied readback is not independent evidence. A later approval session continues the journal chain from a signed window start.
  • A receipt inside a session chains onto the session head whoever signs it. The actor or --system is in the signed statement. --no-parent still keeps a receipt off the chain. --chain remains and is already the default.
  • A linked worktree keeps the main checkout's ship. treeship init --worktree writes a local stub that extends the main config. Sessions stay in the worktree. The Claude Code session hook does this itself, and says so when a tool call is not recorded.
  • The hub does not publish to public Rekor unless TREESHIP_REKOR_URL is set. Device-code approval required a user code. The CLI does not show that code.
  • A discovered project config is used only when you own it and only you can write it, and the walk stops at the git root.
  • Docs match the product. The site index is 149 pages and 55 posts. Linux ARM64 is a shipped CLI platform. The trust-fabric page no longer presents a hosted agent URL or a receipt page as full verification.
  • Docs match 0.31.12's agent pins. Every --kind agent_cert example carries --agent (README, onboard, bundle, verify, what-to-expect and the five-minute post), resolve documents key_bound_reason, the 0.31.12 changelog names TS-2026-005, the advisory tells self-hosted hub operators to upgrade, SECURITY.md names 0.31.12 as current, and the trust-fabric acceptance test isolates HOME.
  • npm lockfiles point at the published 0.31.12 packages. Refreshed by hand after publish; the release workflow's new lockfile job failed on its first run and is fixed separately.
  • TS-2026-005 is published (GHSA-w356-2hw3-x2v2), with the regression tests and cross-SDK vectors for the 0.31.12 agent-card fix that were held back until the advisory went out.

0.31.12 (2026-09-30)

Security release: TS-2026-005 / GHSA-w356-2hw3-x2v2 (agent-card verification and the preview/certificate pages). Upgrade the CLI, the SDKs and @treeship/verify, and run any self-hosted hub on 0.31.12.

  • Breaking: agent_cert pins carry an agent scope, and a pin without one no longer binds a card. treeship trust add … --kind agent_cert --agent agent://<name> records which agent a key is pinned as; agent register --own-key writes it, and the pin lines from keys export --agent <uri> and onboard carry it. A --kind agent_cert line from an earlier release (no --agent) still lets that key's artifacts verify and import, but resolve, verify-capability and verify-presentation now grade such a card asserted and print the re-pin command, treeship trust add <key_id> <pubkey> --kind agent_cert --agent <uri> --replace --yes; a pin whose label is the bare agent name agent register wrote keeps binding. Changing a pin's agent needs --replace; pinning one public key as two agents warns.
  • Capability cards are judged on the keys that verified them. A card is key-bound only when its own key is among the signatures that verified and that key's pin names the card's agent; resolve picks the current card among the agent's key-bound cards, newest first, locally and with --hub, and the hub serves every revocation that names a card rather than one. The verdicts from resolve, verify-capability, verify-presentation and the SDKs carry key_bound_reason (pin_unscoped, pin_scoped_to_other, not_pinned, key_not_verified, agent_uri_invalid) for scripts. A revocation with reason compromised applies to every card of that key; trust remove remains the definitive answer to a compromised key.
  • Receipts no longer carry the machine's hostname. host_id was host_<hostname> and appeared on every event and in the hosts table, eleven times in a typical receipt, untouched by path redaction. Unset TREESHIP_HOST_ID, it is now a random id minted once per install and kept at ~/.treeship/host_id: stable on one machine, unrelated to the hostname. (A hostname digest was considered and rejected: with no secret in it, a short list of common machine names reverses it.) TREESHIP_HOST_ID still names the host outright, and the propagation context the bridges read gives the same answer. The id is published whole (written beside the name, then linked into place), so sessions that start at the same moment on a fresh install agree on one id. (0.31.11 re-test, N-41.)
  • One JSON document, the last eight leaves. --format json onboard printed three documents (one per sub-command); it prints one, with the agent, key, card, rules written, the trust-bundle commands and the verify command. --format json quickstart mixed prompt text into its output; in JSON mode it never prompts, runs the default command, uploads nothing, and prints one document with the ship id, the wrapped command's own result and what to run next. hub open --no-open, otel enable, otel disable, zk-setup, zk-tls-setup and an idle daemon stop wrote 0 bytes on success; each prints one document. The JSON contract test now runs all eight. (0.31.11 re-test, N-2.)
  • Release lockfile job and hub deploy, follow-ups. The refresh-lockfiles job updates an already-open lockfile PR on a re-run instead of failing on the existing branch, and has a 45-minute timeout. The hub Dockerfile prefers RAILWAY_GIT_COMMIT_SHA over a HUB_COMMIT left on the service, so a connected-repository deploy never reports a stale commit; release.sh prepare no longer calls the lockfile refresh a manual step.
  • Lockfiles are refreshed by the release workflow. The npm lockfiles at a tag name the previous release, because release.sh prepare cannot hash tarballs that do not exist yet; the refresh after publish was a manual step, and v0.31.10 and v0.31.11 both left main unbuildable for JS for hours. The release workflow now waits for the published packages, runs release.sh refresh-lockfiles and opens the lockfile PR itself. RELEASING.md documents the order of operations, why npm ci at a tag commit cannot work for the in-repo JS packages, and the hub deploy. (0.31.11 re-test, N-3.)
  • Hub deploys carry their commit. railway up uploads have no .git, so /v1/version reported commit: unknown. scripts/hub-deploy.sh sets HUB_VERSION, HUB_COMMIT and HUB_BUILT_AT on the Railway service (the Dockerfile takes them as build args) and then uploads. (N-34.)
  • Revoking an asserted capability card takes effect. revoke-capability on a card that is not key-bound printed "revoked" and exited 0, but verify-capability and resolve kept honoring the card: the revocation's signature was checked only against pinned trust roots, and an asserted card's key is the ship's own, unpinned key. A revocation is now verified against the keys this machine trusts, its own keys and the pinned roots, the same set the card itself is verified against, so a self-revocation by the card's key counts whether that key is pinned or local. The revoke output names the authority as "the card's key, the ship default key" for such a card instead of "agent key". (0.31.11 re-test, N-38.)
  • A linked .treeship/ fails closed for session commands. The rule that routes session commands to the workspace holding the active session refuses a .treeship/ directory or a config.json beside the session that is a symlink, including a dotfiles-style link to your own checkout; such a workspace falls back to the normal resolution with a clear error rather than being opened through the link. Use --config with the real path if you keep .treeship behind a link.
  • Session commands record into the session's own workspace. With TREESHIP_CONFIG exported to another store, the Claude Code plugin's hooks appended events to the project's session but sent the signed approvals and attestations (attest approval, attest receipt --chain, halt list, judge) to the other store, so the receipt showed nothing captured. Every session command and every command that signs into a session now uses the workspace whose config.json sits beside the active session.json, opened with the same discovery checks as the shell hook, only when that workspace is inside the home directory and reached through no link; --config still wins, and TREESHIP_CONFIG applies when no active session is found. The session walk stops at the home directory, like the shell hook's. (0.31.11 re-test, N-12 / N-36.)
  • session close --summary is path-redacted. The operator's summary (and --headline, --review) landed in the signed receipt as written, so absolute home paths reached narrative.summary, the timeline's closing entry and preview.html while every other field was already ~/.... The text is now rewritten the same way before signing, and close prints a warning naming the flags it rewrote. (0.31.11 re-test, N-28.)
  • An unjoined invitation passes package verify --strict. An honest host that minted an invitation nobody redeemed failed chain_completeness under --strict: the invitation is sealed but unchained, and only a redeemed invitation counted as bound. An invitation an authenticated signer issued for this very session (its signed session_ref names the session) is now bound by that signature, joined or not; the row says "invitation issued for this session". (0.31.11 re-test, N-39.)
  • hub unpublish asks before it takes a receipt down. The takedown is permanent and the command asked nothing, even off a terminal. On a terminal it now asks you to type the session id back; off a terminal, or with --format json, --yes is required, and without it nothing reaches the hub and the exit is 4. (0.31.11 re-test, N-42.)
  • The cross-SDK contract job builds again on Node 22. Its TS SDK step removes the lockfile to install the local core-wasm build, and with vitest 4's optional peer dependencies npm 10 then fails without a message; the step installs with --legacy-peer-deps.
  • The security-claims check reads the CLI's help text too. packages/cli/src/main.rs, whose doc comments are what --help prints, is in the claims scan, and wording that calls the key bound to a machine joins the banned phrases: the key is protected by the seed file beside the keystore and travels with .treeship/. The CLI help text is held to the same strict check as the docs.
  • Final 0.31.11 re-test P2 docs backlog closed (N-29, N-30, N-31). N-31: check-claims.py's finer rule let a marked line use a word its own claim's not: list disclaims -- README.md's "The Hub stores immutable bytes" passed under hub-storage-write-once even though that claim's own says field negates immutable. The check now derives, per claim, which disclaimed words its says text actually negates (an affirmed word with its own qualifier, like rekor-artifact-anchoring's "witnessed only if ...", needs no negation from the doc line), and requires the doc line to negate it too; fixed two bugs found writing this (a claim id cited by name inside another claim's says, like checkpoint-not-witnessed, isn't itself a negation of witnessed for the citing claim; the marker syntax and link URLs on a scanned line aren't prose and shouldn't count as hits), added scripts/tests/test_check_claims.py, and reworded README.md's actual overclaim ("write-once bytes -- not immutable"). N-29: receipt-put.mdx's "Idempotency" section said a second PUT from the owning dock "overwrites the first" -- the hub returns 409 unless the replay is byte-identical, and 410 for a tombstoned id; neither was in the error table. Fixed the section and table, and the OpenAPI spec's PUT/GET/DELETE response codes (missing 403/409/410/413 on PUT, 400/403 on GET -- the GET block's lone 404 case was actually describing the 403 "session still open" state under the wrong code). N-30: "The Hub cannot forge a passing result" was unscoped in 5 files (api/overview.mdx, api/verify.mdx, api/receipt-get.mdx, concepts/trust-model.mdx, AGENTS.md) -- true for a signed artifact verified against the caller's own trust roots, not for a bare Session Receipt fetch, whose determinism/merkle_root checks are self-consistency only and cover only the artifact ids (packages/core/src/session/package.rs's own comment: "An attacker who edits [the narrative] fields and re-serializes will pass determinism and pass the Merkle check"). Registered hub-cannot-forge-signed-verification with both halves and marked or reworded each occurrence.
  • N-35 and N-40 (P3). SECURITY.md's supported-versions table said "current: 0.31.10" at the 0.31.11 tag; fixed, along with a stale "ahead of the 0.31.10 release tag" note in hub-storage-write-once's own claims.yml entry. trust add --help's second example (ship_acme ... --kind cert_issuer) is refused by the real key-id rule (cert_issuer/revoker/agent_cert validate the id shape; a human label goes in --label, not the positional KEY_ID) -- verified the failure, then the fix, against the built binary; replaced with a key_<16 hex>-shaped placeholder matching cli/trust.mdx's own existing example.
  • npm lockfiles point at the published 0.31.11 packages. scripts/release.sh refresh-lockfiles after publish, so npm ci resolves @treeship/* 0.31.11 from the registry in every package and runtime-acceptance fixture.
  • npm audit findings fixed across the JS packages (N-37). vitest/@vitest/mocker (moderate, path traversal via redirect mock, GHSA-82fw-gwwq-j7x9) affected packages/sdk-ts, packages/verify-js, bridges/mcp and bridges/a2a -- dev-only, but bumped vitest to ^4.1.11 (the first patched release, and still the latest 4.x, so this is the smallest version jump that's actually fixed rather than a jump straight to 5.x). hono (moderate, three separate issues: incomplete toSSG() path-traversal fix, unbounded parseBody() nesting, and a query-parser/fragment cache-key differential) reached bridges/mcp transitively through @modelcontextprotocol/sdk; pinned via a package.json overrides entry (^4.13.5, the first patched release) since it isn't a direct dependency. fast-uri (high, host-confusion and SSRF via IDN/IPv6/percent-decoding normalization bugs) reached schemas transitively through ajv; same fix, an overrides entry (^3.1.6) to stay on the 3.x line ajv declares rather than jumping to fast-uri 4.x. All five packages now audit clean (npm audit: 0 vulnerabilities), and every package's own test suite passes on the bumped versions: sdk-ts 41/41, verify-js 111/111, bridges/mcp 31/31, bridges/a2a 79/79, schemas' validate.mjs fixtures. npm ci (what CI actually runs) works cleanly from every regenerated lockfile; npm install alone hits a known npm/arborist bug (Cannot read properties of null (reading 'edgesOut')) resolving vitest 4's new optional peer dependencies and needs --legacy-peer-deps to regenerate a lockfile from scratch, worth knowing for the next bump in these packages.
  • The preview and certificate pages escape every rendered field. preview_template.html and certificate_template.html's shared esc() helper now covers " and ' as well as &/</>, so a field placed inside a quoted HTML attribute (a capability's title, for example) can't end that attribute early. Fields meant to be counts (authority.checked/violations/unverified/bearer, an approval grant's delegation_hops and scope.max_uses) are coerced through the existing num() helper before they reach the page, rather than assumed to already be numbers. timeOf() (the timestamp formatter used across both pages) now escapes its own output once, at the source, instead of depending on every call site to do it. The render-failure path's error text is escaped too. receipts/ssn_aab97aefa1f8a48c.treeship/preview.html, the example package checked into the repo, is regenerated from its own receipt.json against the updated template so it matches what ships. Two new tests build a page with an unusual payload in every rendered field and confirm the output never contains an unescaped tag or attribute-breaking quote.

0.31.11 (2026-09-27)

  • The shell hook records into the project whose session is open. treeship hook post (installed by treeship install into the shell) used the store an exported TREESHIP_CONFIG named, not the project's .treeship/config.json beside the config.yaml it had found, so a session started with --config in the project and the hook's receipts could end up in different stores. For the shell hook only, the precedence is now: an explicit --config, then the project's own config when a session is active in that project, then TREESHIP_CONFIG, then discovery. The project config is opened as a discovered config, so a cloned repository's config.json gets every discovery check (stores inside its own .treeship, no extends outside the home, no linked .treeship) before any key is used.

  • The lockfile check also compares the package's own version. A package-lock.json whose root version lags package.json (the OpenClaw plugin's read 0.10.3 at 0.31.10) fails scripts/check-lockfile-sync.py; the plugin's lockfile is refreshed, scripts/lockfile-pin.py stamps those fields at release time, and the plugin is in release.sh's lockfile lists.

  • Clippy runs on wasm32 in CI (PKG-3). std::fs::File has no Drop on that target, so an explicit drop(lock_file) in the keystore migration failed the lint there; the handle is bound to the function's scope instead, and the feature-matrix job lints core-wasm on wasm32 with warnings denied.

  • wrap exits with the wrapped command's code in every format. --format json wrap -- <cmd> returned 0 when the command failed, with the code only inside the document, so treeship --format json wrap -- npm test && deploy deployed after failing tests; text mode was right. Every format, and --quiet, now propagates the code (128 + signal for a signal death).

  • --format json leaves exactly one document on stdout. Warnings ({"status":"warning",…}) go to stderr in JSON mode, like errors; 0.31.10 printed a warning document before the result on attest action --v2 outside the grant's scope and on trust add --replace, and parsers failed with "Extra data".

  • merkle publish and checkpoint --publish report what reached the hub. --format json merkle publish wrote nothing on success; it now prints the hub checkpoint id, proofs published, local-only artifacts skipped, the consistency proof's outcome and the share URL. checkpoint --publish --format json carries the same under publish, so a caller can tell the checkpoint was published; text mode is unchanged. The checkpoint hint names the whole root, not a truncated one merkle verify would reject.

  • A registered predicate that fails its schema exits 4. attest receipt --kind memory.write.v1 (or any registered kind) with a payload that fails validation is the call refused as written, exit 4 as the exit-code table says; 0.31.10 exited 1. Same for the session and judge paths that validate a predicate.

  • Countersign hints are complete. session answer-challenge and session join (text and countersign_hint in JSON) name every flag the host needs: --challenge, --challenge-issued-at and --challenge-response, and --pending <file> in place of the participant id when the join was written with --out for a host on another machine. The host used to learn the missing flags from exit 4.

  • match --class and room create --delegate validate their arguments. An unknown class exits 4 before the hub is asked (it used to answer with an empty match set); a delegate that is not an Ed25519 public key exits 4 instead of being sealed into the room, and a bare base64url key is stored in the canonical ed25519: form.

  • agents remove of a card that is not there is an error, not "removed" with exit 0.

  • A second init says what is true. In a directory with no workspace of its own while a global one exists, the message names the global workspace and how to create a project one (it no longer suggests regenerating the global identity); when the global workspace lives in the current directory it says so.

  • hub open on a self-hosted hub mints no token and points at the hub's JSON workspace. Such a hub has no workspace page; the command used to mint a share token and paste it into a treeship.dev URL that does not exist for that hub. It now prints <endpoint>/v1/workspace/<dock> and mints nothing. The hosted hub (a treeship.dev endpoint, judged by a real URL parser) is unchanged. hub attach --endpoint drops a trailing slash, which used to produce //v1/dock/challenge and a 404.

  • init --help no longer says the key is tied to the machine. The key is encrypted with a seed file kept beside the keystore and travels with .treeship/, as SECURITY.md says.

  • One JSON document on stdout, everywhere. --format json add printed one success document per detected harness; it now prints one document listing configured, skipped, failed, detected and treeship_md, and answers as one document when nothing is detected or none of the requested agents are. attest action|action --v2|endorse --out - wrote the DSSE envelope with a bare print before the result; in JSON mode the envelope is the envelope field of the result. session report with no hub attached put a report carrying error on stdout and exited nonzero; it is an error on stderr now, with --no-upload named for the local verdict. The JSON contract test no longer skips add.

  • checkpoint --publish reports a failed publish. In JSON mode the sealed checkpoint is printed with publish: {status: failed, ok: false, error} and the command exits nonzero, so a retry does not seal a second checkpoint for nothing. The consistency-proof skip reasons are split: an empty previous tree, a previous checkpoint larger than this one, and a checkpoint covering more leaves than the store holds each say so, instead of every case reading "not a prefix".

  • wrap exit codes match the shell. A child ended by a signal exits 128 + signal (it exited 255 from a -1) and the JSON output carries signal; a command that cannot be started exits 127 with nothing signed. The wrap page documents that the propagated code can coincide with wrap's own.

  • The shell hook's upward walk stops at the home directory. A .treeship/config.yaml above $HOME is not used.

  • Test hygiene. Every CLI end-to-end test clears TREESHIP_CONFIG, so the suite passes with the variable exported in the developer's shell.

  • A release no longer tags a red main. The lockfile check (scripts/check-lockfile-sync.py) failed on main between the version bump merging and the packages reaching npm, because the installed lockfile entries could not yet name a version that had no tarball, so every release was tagged on a failing commit. An installed entry that lags the release in flight (the version in packages/core/Cargo.toml) is now a warning while that exact version is not on the registry, and fails again the moment it is published, which is what forces scripts/release.sh refresh-lockfiles. The declared-range half of the check, the release-branch window, and the behaviour when the registry cannot be reached are unchanged. The gate has unit tests with the registry lookup stubbed.

  • TS-2026-004 published (GHSA-p367-33qm-pwv7). docs/security/TS-2026-004.md documents the two 0.31.10 security fixes: package verify accepting a close record signed by any key the package carried rather than the session's own signer, and workspace writes following symlinks. SECURITY.md's supported-versions table and key-properties list, and README.md's security history, now name it. docs/claims.yml gains package-verify-binds-receipt, describing what receipt_binding and signer_trust each prove. cli/package.mdx's receipt_binding row, cli/trust.mdx (--replace, and which trust add pin hints still print --yes), cli/session.mdx, concepts/multi-agent-sessions.mdx, and guides/handoffs.mdx are updated to match 0.31.10's actual behavior, verified against source rather than carried over from the pre-fix wording.

  • sync-changelog.mjs --check no longer compares the Unreleased section. It's exactly whatever changelog.d/ fragments happen to be on main right now, a moving target every fragment-adding PR shares -- comparing it meant landing any one PR's fragment made every other open PR's already-correct branch fail the check, the same per-PR churn changelog.d/ was built to remove. --check now strips ## Unreleased (heading through the next ## heading) from both sides before comparing; released sections, the actual content responsible for the drift this check exists to catch, are still compared in full. Verified both properties: adding an unrelated fragment no longer fails the check, and a genuine edit to an already-released section still does.

  • The docs gates cover the whole tree and cannot pass by accident. The security-claims check scans every docs page, the root *.md files and skills/, and a claims marker now backs only its own line (or the line it sits above), not a whole paragraph. The SDK check resolves every documented client call, TypeScript module methods and Python client methods alike, against the real sources, and scans skills/ and integrations/ too. The command and flag checks take extra roots (--warn-root) and scan skills/, integrations/ and the root *.md. The https one-liner check is a Python script with an explicit path boundary and a unit test, replacing a git grep \b that BSD grep read differently. New findings from the wider scope are reported as warnings until the drift they surface is fixed, then the gates switch to failing (--strict, and the roots move out of --warn-root).

  • TS-2026-001..003 now carry their GHSA ids. The three earlier advisories were published as GitHub Security Advisories (GHSA-pmp9-x698-fh4v, GHSA-29hj-wmwv-7hx8, GHSA-qhv9-h5vq-hw6r); their header tables now name them, plus CVE (pending) and CWE, matching TS-2026-004's format.

  • docs.treeship.dev/api/hub-openapi.yaml now serves the spec check-hub-openapi.py actually validates. Two copies of the Hub OpenAPI spec had diverged: docs/public/api/hub-openapi.yaml, the file fumadocs actually serves at that URL, was a stale older draft (no schemas: section, treeship.dev/dock/activate instead of the corrected treeship.dev/hub/activate, thinner response bodies); docs/content/docs/api/hub-openapi.yaml, the richer and route-checked copy, sat under docs/content/ where fumadocs never serves raw files, so nothing a visitor could reach was ever checked against packages/hub/main.go. Merged into one file at docs/public/api/hub-openapi.yaml (the served location) and repointed scripts/check-hub-openapi.py at it; verified the check still passes (30 routes) and that a built server serves the correct content at /api/hub-openapi.yaml.

  • Docs and the claims registry catch up to treeship hub unpublish (#533). treeship hub unpublish had no CLI reference page section -- docs/api/receipt-delete.mdx already linked to /docs/cli/hub for it, but the page never documented the command. Added a full ## treeship hub unpublish section (usage, options, the permanence callout) and its row in the commands table. docs/claims.yml's hub-storage-write-once claim said the Hub's storage was write-once with no exceptions; it now also describes the one real exception, the publisher's own one-shot tombstone (DELETE /v1/receipt/{session_id}, verified against db.TombstoneSession and InsertSessionWriteOnce's explicit "tombstoned" guard, which refuses to ever refill a taken-down session id). Six other pages that explain publishing (concepts/session-receipts.mdx, cli/session.mdx, cli/package.mdx, api/receipt-put.mdx, guides/quickstart.mdx, sdk/python.mdx) called the receipt URL "permanent" or said it "never expires" -- true before this release, no longer since a publisher can take it down -- reworded to say the URL stays up until unpublished, with a link to the command. treeship session report's own success hint (packages/cli/src/commands/session.rs) had the identical overclaim baked into the CLI itself ("they never expire and need no auth"); fixed at the source, not just in docs, to "they need no auth and stay up until you unpublish them."

  • Two live-crawl link bugs fixed, and the checker learned to catch both. /integrations became reachable once #520 removed its redirect, exposing 21 links (./slug) that had always been broken: the page's own rendered URL has no trailing slash, so a browser resolves ./slug against /, not /integrations/. Fixed at the source -- scripts/gen-integrations-index.py generates docs/content/docs/integrations/index.mdx, and now emits absolute /integrations/<slug> links. Separately, CHANGELOG.md's original entries for the evaluator kit, receipts-in-pull-requests, verification reporting, TS-2026-002 and verdict-invariant.md still carried the exact repo-relative links a later summary bullet claimed were already fixed -- they weren't; fixed the five original entries directly (three to their real /guides/... routes, two to GitHub blob URLs for files outside docs/content/). docs/scripts/check-internal-links.py only matched root-relative links (/foo); it never had a chance to catch either bug. It now resolves page-relative links (./foo, foo, ../foo) against the linking page's own rendered URL using the same rule a browser applies -- verified by temporarily reintroducing the original ./claude-code link and confirming the checker now flags it, then confirming it passes clean again after reverting. docs/scripts/sync-changelog.mjs gained a --check mode (mirroring sync-feature-matrix.mjs's), wired into docs-drift in ci.yml, so the committed about/changelog.mdx can no longer drift from CHANGELOG.md silently.

  • CLI docs re-test fixes, area A (v0.31.10 retest). Fixed fictional or stale commands verified against the released binary: agent-cards.mdx's agent add --kind (no such flag; manual provenance has no producer at all), coverage-levels.mdx's v0.9.9 agent invite/join --invite (now the real session invite/session join), ONBOARDING.md's trust-template (real commands are init --template/template apply, plus the missing 8th template), zk-risc0/README.md's prove --engine --chain (real path is prove-chain <session_id>), AGENTS.md's nonexistent web/ directory and hub.treeship.dev hosting claim, threat-model.md's package import (real: bundle import) and command-artifacts.mdx's self-contradicting "slated for v0.10.0/v1.0" framing for a CLI surface that still doesn't exist 21+ releases later. Also: cli/overview.mdx's exit-code table had two exit-4 examples that actually exit 2 (clap's own parser, not the command's usage check) -- replaced with verified real exit-4 invocations (judge --resolve with no --by, session countersign with neither a participant id nor --pending). zk.mdx and zero-knowledge.mdx had stale callouts about bugs since fixed (zk-setup's hint text now prints the real command; verify-proof is a real hidden subcommand that exits 5, not an absent one). 7 of 13 hidden-command reference pages (bundle, merkle, harness, otel, ui, dashboard, install/uninstall) now carry the same "Hidden from --help" callout the other 6 already had.

  • SDK/MCP docs re-test fixes, area B (v0.31.10 retest). Every fix verified against source: typescript.mdx's s.hub.status() result shape ({connected, endpoint?, hubId?}, not {attached}); sdk/overview.mdx now says four modules (attest/verify/hub/session, matching the real Ship client) and lists session_event for Python; python.mdx gained session_event()/SessionEventResult and Treeship.ensure_cli()/BootstrapResult sections that didn't exist. mcp.mdx: the unqualified "Treeship never breaks your MCP workflow" is now qualified by the real halt/TREESHIP_STRICT behavior, added a Server section documenting the real 9 treeship-mcp tools (was undocumented entirely), removed the fictional TREESHIP_DEBUG (real for bridges/a2a, not bridges/mcp) and added the real TREESHIP_STRICT. bridges/mcp/README.md: "5 tools" -> 9 (named), "pure WASM" verify claim fixed (the CLI's native verifier isn't WASM; @treeship/verify is), "never fail the underlying tool call" design rule corrected to match the file's own halt/strict section two paragraphs above it. openclaw/treeship.skill/SKILL.md's hardcoded v0.9.4 (skill metadata + prose) replaced with an independent skill version, matching the convention other skill files already use. cli/overview.mdx and guides/workflows.mdx: the fictional TREESHIP_LAST env var replaced with the real .last file / last keyword mechanism, and wrap's real parent-resolution priority (--parent > TREESHIP_PARENT > .last); workflows.mdx's approval-chaining example also had --parent doing duty for approval-nonce consumption, which isn't what it does -- rewritten to use attest action --approval-nonce, matching wrap's documented lack of that flag elsewhere in the docs. AGENTS.md: stripped the specific version/test-count/endpoint-count claims its own intro says were "deliberately gone" but the body still had (shipped at v0.24.0, 12 API endpoints, 6 tests x2, 167KB gzipped, 25+ commands, 4 vectors); fixed dock.rs/ui.rs (real files: hub.rs, tui/) and three wrong Hub API routes (/v1/workspace/{dockId}, /v1/merkle/{artifactId}, /v1/merkle/checkpoint/latest); fixed the TREESHIP_ACTOR/TREESHIP_PARENT/TREESHIP_TOKEN env-var-fallback bullet (only TREESHIP_PARENT is real for the CLI; TREESHIP_ACTOR is MCP-bridge-only; TREESHIP_TOKEN reads nothing, the real vars are _IN/_OUT). ONBOARDING.md's /v1/workspace/:dockId "Public" row is now accurately DPoP/session-token authenticated. Five files (ONBOARDING.md, packages/cli/README.md, PLATFORM.md x3) said the treeship-cli crate was "orphaned at v0.4.0" -- crates.io shows all 7 versions yanked now, a stronger and different failure mode (cargo install fails outright, not just installs something stale). hermes.mdx/integrations/hermes/README.md's smoke-test download pinned v0.15.0, a dead tag; switched to GitHub's releases/latest/download/ alias so it can't go stale again. packages/rig/README.md's "treeship-core 0.23.0" corrected to match its own Cargo.toml pin (0.31.10). packages/sdk-ts/src/ship.ts's CLI-not-found error message pointed at a URL that 404s through two redirects; fixed to the real working guide URL. README.md softened two "captured from v0.24" / "run as written against v0.24" claims that were 20+ patch releases stale.

  • Security/schema docs re-test fixes, area C (v0.31.10 retest). Every fix verified against source. capabilities.mdx: "hardware identifier" keystore claim fixed to match the seed-file mechanism; the checkpoint transparency-log section's "the hub... never generates or verifies" corrected (it does verify checkpoint signatures, merkle/handlers.go) and its Certificate Transparency analogy qualified (no independent monitors or gossip protocol today). README.md: added per-artifact Rekor anchoring (shipped v0.31.9) to the Shipped list, removed it from the Open/roadmap line; "Ed25519 for all signatures" qualified (Verifiable Intent uses ES256/P-256); two 20+-release-stale "captured from v0.24" claims softened. packages/core/src/verify/anchoring.rs's doc comment no longer lists a bare Hub checkpoint or OpenTimestamps as real anchors alongside Rekor -- only per-artifact Rekor is real today. reference/receipt-format.mdx: fixed the false "type field carries the same version string as payloadType" claim (it's a differently-shaped string), added the 4 missing real media types (decision, invitation, session-participant, session-liveness) and removed the fictional session.v1 media type, explaining that the close record actually uses the receipt.v1 payloadType with an internal kind: "session.v1" field. packages/verify-js/README.md: verifyCertificate/crossVerify examples and prose omitted the required trustRoots parameter and wrongly said the certificate's own embedded key is trusted (removed at v0.10.3). packages/hub/README.md: corrected the endpoint count (~23, not 13), Go version (1.23+), the fictional .env.example/./cmd/server paths, and the database (SQLite, modernc.org/sqlite, not PostgreSQL) -- a complete rewrite of the Requirements/Running-locally sections; also softened the "public transparency log" claim to match the same CT-analogy correction made in capabilities.mdx. Six specs (docs/specs/*.md) marked "draft, not implemented" turned out to have substantially shipped -- agent-resolver (treeship resolve), agent-capability-cards (treeship verify-capability, minted via agent register not the spec's attest card), transparency-log (treeship audit, including --watch), and work-history (treeship history/treeship profile) all have real commands whose --help text matches the spec prose almost verbatim; updated each status line. agent-invitations-rooms's Phases 1-2 (invitation, join, room create/status/participants) are shipped, including the cross-machine extension from #499 the spec predates; capability-provenance genuinely remains unimplemented, left as-is. AGENTS.md: "ZK TLS... feature-flagged" fixed (there's no feature flag; zk-tls-setup just prints setup instructions, unconditionally, for a notary you'd run yourself). Three files (artifacts.mdx, merkle-proofs.mdx, logs-vs-receipts.mdx) had "there is no way to modify an artifact while preserving its ID" without the tamper-evident, not tamper-proof qualifier (<!-- claims:receipt-tamper-evidence -->) the claims registry already establishes elsewhere -- fixed to distinguish the signed payload (ID-changing) from unsigned fields (can still change). security.mdx/README.md: "Ed25519 for all signatures" -- Verifiable Intent uses ES256. threat-model.md: removed the unverifiable "NCC Group audited" attribution in the 3 places it survived the earlier SECURITY.md removal. merkle-proofs.mdx/api/overview.mdx: Rekor submission described as unconditional; added the real skip condition (agent-key-only artifacts are skipped with a reason, not anchored). docs/feature-inventory.yml: the trust-roots entry's kind list was missing transparency_log; the zk-circom-native entry claimed a native.rs scaffold that doesn't exist (only commented-out Cargo.toml lines do).

  • capability-map.json now names hidden commands (row 110). gen-capability-map.py parses the top-level Command enum in packages/cli/src/main.rs for #[command(hide = true)] attributes, converts each variant to its clap kebab-case name (honoring an explicit name = "..." override), and probes every one against the built binary (<cmd> --help, probed: true/false) to add a new cli_commands_hidden array (22 entries, all probing clean) alongside the existing cli_commands list. A new HIDDEN_CLI_COMMANDS list in the script is the hand-maintained, human-reviewed source for what gets probed; a new check (check_hidden_list_current(), runs unconditionally, not just under --check) fails the moment the source and that list disagree in either direction, so a newly-hidden command needs a person to add it (and, ideally, ship a docs page) rather than the map silently drifting again.

  • HIGH-priority docs re-test fixes (v0.31.10 retest). PLATFORM.md (historical snapshot): fixed the still-wrong bodies under the banner -- the real Python SDK method is hub_push, not ts.hub.push; the real TS hub-status shape is {connected, endpoint?, hubId?}; the MCP bridge's export is Client, not TreeshipMCPClient, with the real constructor signature and TREESHIP_ACTOR env var (not a constructor option); merkle publish only publishes to Hub, Rekor anchoring is separate and per-artifact; the stale 0.9.x-current version table now points at SECURITY.md. init.mdx, how-it-works.mdx, trust-model.mdx: unqualified "encrypted at rest" now says AES-256-GCM under a seed file, not a passphrase, not machine-bound (<!-- claims:keystore-at-rest -->). Two blog posts (from-subprocess-to-wasm...mdx, why-we-chose-rust...mdx) describing a nonexistent core-wasm sign/init API now carry editor's notes; multi-agent-skills.mdx's stale 2026-08-18 correction note (which itself had gone false) is corrected and its SDK-shape bullets fixed (hub_push not dock_push, ship()'s four modules not Ship.init()/createCheckpoint()/createBundle()); treeship-0-9-10-approval-authority.mdx's journal list/inspect gets a note pointing at the real approval surface. docs/next.config.mjs: removed the /integrations -> /integrations/claude-code redirect now that /integrations has its own index.mdx -- the redirect was making the real index page unreachable even though meta.json lists it first (row 118).

  • Lobster Cash skill and docs no longer sell nonexistent security features (row 27, N9). The skill README, LISTING, skill-registry.json and the docs integration page sold "ZK proof: policy compliance" and "ZK proof: spend within limits" (the Circom/Groth16 path, quarantined -- its proving keys were never generated under a real ceremony, so prove/verify-proof fail closed on any build) and a "Full chain proof (RISC Zero)" at session close (requires --features zk, not in release binaries, and not automatic). Replaced with what actually runs: signed receipts, scoped single-use approvals, and Merkle inclusion proofs. treeship init --template lobster-cash-commerce (four places: the skill README, the docs page, and skill-registry.json's install field) named a template that does not exist -- fixed to plain treeship init. SKILL.md's treeship session start --skill lobster-cash used a flag session start doesn't have -- fixed to --name.

  • OpenClaw's install command uses a real flag (row 26, docs side). openclaw plugins install --local ./ isn't real on OpenClaw 2026.2.23 or 2026.9.6 (error: unknown option '--local'); passing the path directly (openclaw plugins install ./) is the route that works (verified: Installed plugin: treeship). Fixed at the source (integrations/agents.json) and regenerated docs/content/docs/integrations/install.mdx and docs/public/.well-known/treeship-agents.json from it, plus integrations/openclaw-plugin/README.md directly.

  • treeship onboard examples that omitted a capability source. onboard refuses without one of --from-harness, --tools-json, --from-a2a or --tools ("no capability source"). Added --tools <list> to the three copy-pasteable examples that were missing one (a blog post, cli/overview.mdx's command list, and specs/per-actor-signing.md); the latter's agent register --actor also doesn't exist (the flag is --name) -- fixed both in the same file.

  • docs.treeship.dev serves security.txt (RFC 9116). /.well-known/security.txt 404'd. Added as a route handler (app/.well-known/security.txt/route.ts, same pattern as app/llms.txt) rather than a public/ static file -- a static file at that path is shadowed by the [[...slug]] catch-all's own generated response for the same URL. Same content as www's, with Canonical pointing at the docs host.

  • TS-2026-003.md said its fix "ships in the next release." It shipped in 0.31.9, already tagged. Fixed the public-disclosure-window field.

  • Two docs-drift gates moved from warn-only to failing; a false-positive bug in a third is fixed. check-sdk-docs.py --strict: the 17 full-resolver findings were a resolver gap, not phantom docs -- bridges/a2a's TreeshipA2AMiddleware (onTaskReceived, onTaskCompleted, onHandoff, decorateArtifact, mintTaskChallenge, admitTask) is a real, shipped class the resolver didn't know about (same treeship.<method>( variable convention as the Ship SDK, a different package); the resolver now recognizes it. check-cli-commands.py/check-cli-flags.py: the 7 --warn-root findings were one pattern -- ONBOARDING.md and PLATFORM.md each carry a "full command surface" cheat-sheet in a bash-tagged fence (real commands, a|b shorthand for two subcommands on one line, attest approval shown without a scope flag since it's a reference table, not a script); retagged both fences to a non-runnable language tag and added a one-line note; both scripts gained a --hard-root flag (they previously supported only one hard root plus repeatable --warn-root) and skills/, integrations/ and the root *.md files moved from --warn-root to --hard-root in ci.yml.

    check-claims.py's finer per-line rule matched banned phrases inside inline code spans and fenced blocks, so a real flag (--max-unwitnessed), a JSON field literally named anchored, and similar identifiers all false-positived as unbacked prose claims; both scans are now prose-only (fences and `code spans` stripped before matching), cutting 124 wider-scope findings to 76 with no loss of real coverage. CHANGELOG.md, README.md, SECURITY.md, AGENTS.md and DESIGN.md are now fully clean under the finer rule (down to 0 findings across all five), backed mostly by existing claims.yml entries plus two new ones for real, previously-unregistered properties: hub-storage-write-once (the Hub's own database is write-once per artifact_id/session_id -- ON CONFLICT DO NOTHING and a 409 on a second receipt upload -- distinct from receipt-tamper-evidence, which holds even with no Hub involved) and transparency-log-client-reverified (audit/history/resolve --hub re-verify Merkle inclusion offline against the reader's own trust roots and audit witnesses checkpoints across runs to catch a forking or regressing hub -- distinct from rekor-artifact-anchoring, an external third-party log, and from checkpoint-not-witnessed, about whether a checkpoint is externally corroborated at all). check-claims.py itself stays warn-only for now: 46 findings remain, all in docs/content/docs/** and docs/content/blog/**, in progress as a follow-up.

  • check-claims.py --strict is now the failing gate. The 46 findings left after the wave-1 false-positive fix (see the earlier changelog entry) were spread across docs/content/docs/** and docs/content/blog/**. Most (37 of 46) were real, backed claims that just needed a {/* claims:&lt;id&gt; */} marker on the exact line -- mostly transparency-log-client-reverified (the Hub transparency-log surface: audit, history, resolve --hub, the /v1/agents/* and /v1/merkle/* APIs) and rekor-artifact-anchoring (per-artifact Sigstore pushes, --max-unwitnessed, trust add --kind transparency_log). Nine needed real wording fixes, not just a marker: concepts/secrets-and-redaction.mdx and concepts/session-receipts.mdx both called receipts or packages immutable, the exact overclaim receipt-tamper-evidence exists to correct -- both now say "tamper-evident." concepts/trust-fabric.mdx used witnessed generically for file-change attribution (nothing to do with Rekor or checkpoints) -- reworded to "captured." concepts/agent-handshake.mdx's air-gapped verification was generic offline-capability prose -- reworded to "fully offline verification." Three blog posts had the same generic air-gapped usage, reworded to "no network connection"/"offline or network-isolated." cli/audit.mdx had a banned word in MDX frontmatter, where a {/* */} marker doesn't parse -- reworded instead. And one real correctness bug: a 2026-09-11 retrospective blog post (treeship-0-1-signed-content-addressed-offline.mdx) said the keystore is machine-bound, so a copied key file does not decrypt elsewhere -- directly contradicting the registered keystore-at-rest claim (no passphrase; copying .treeship/ is sufficient to decrypt elsewhere). Fixed the sentence, not just linted around it.

  • A published receipt can be taken down. treeship hub unpublish <session_id> sends a DPoP-signed DELETE /v1/receipt/{session_id}; only the dock that published the receipt may call it. The hub removes the receipt body for good, along with the session's name, timing and counts, and keeps only the id, the dock and the takedown time; the receipt URL answers 410 Gone with a short reason from then on, never the old bytes, and the session id cannot be re-uploaded. Local copies are untouched, and a copy a browser or proxy cached while the receipt was public can persist for up to 24 hours. Taken-down receipts are left out of /v1/stats session counts.

  • Session receipts carry no absolute home paths. Files under $HOME are recorded as ~/... when the receipt is composed, so the close record signs the redacted form and file digests are unchanged. The same substitution runs over command lines (treeship wrap, shell-tool events), file:// URLs, network destinations, agent notes, and the process names and timeline summaries built from them, wherever the home directory appears at a path boundary; the resolved form of $HOME is redacted along with the literal one. session close --summary is the operator's own text and is published as written. A published receipt had carried over a thousand local /Users/<name>/... paths.

  • Hub responses carry Vary: Origin, so a shared cache never serves one origin's CORS header to another on receipts cached with Cache-Control: immutable; HEAD is answered on every GET route instead of 405; DELETE and HEAD are in the allowed methods.

  • /v1/version reports a real version on Railway. The Dockerfile's build args default from RAILWAY_GIT_COMMIT_SHA, and an unset version falls back to the hub's own Release constant, which the release script stamps, instead of dev.

  • The shell hook ignores stale state. The installed zsh, bash and fish hooks now pass the finished command to hook post; state left by a hook pre for a different command is dropped and nothing is recorded, instead of attributing the old command to the new run. In bash the whole typed line is taken from history once per prompt when history just recorded it, so git commit -m x && echo ok is recorded once and an empty Enter records nothing; a line history did not record is taken from BASH_COMMAND, so nothing is ever signed for a command that did not run. The snippets are quiet under setopt nounset, the hooks run with --quiet so a matched command prints nothing into the prompt, and hook diagnostics go to stderr. treeship uninstall gives the file back byte for byte. A hook from an earlier release passes no command; there, state older than 24 hours is dropped. treeship install replaces an older hook block with the current one and changes nothing outside its marker lines.

  • npm lockfiles point at the published 0.31.10 packages. This is the post-publish lockfile refresh the release process requires.

0.31.10 (2026-09-26)

  • treeship add claude-code installs the Claude Code plugin. It used to write ~/.claude/mcp.json and report Claude Code as configured, but Claude Code never reads that file (its MCP servers live in ~/.claude.json and a project's .mcp.json, its hooks in ~/.claude/settings.json). The plugin is the real harness, hooks plus the MCP server, so add now runs claude plugin marketplace add zerkerlabs/treeship and claude plugin install treeship@treeship through the claude CLI when it is on PATH and you say yes (or pass --all), prints the two commands otherwise, says plainly what it did and did not change, and removes the stale mcpServers.treeship entry an earlier version wrote, only when it is exactly that entry. (0.31.9 full test, CLI-16.)

  • Package verification is stricter about who closed a session and what a verdict requires. Security fix; see the accompanying advisory. The close record must be the session's session.v1 record, name the sealed session.close, and be signed by its signer or by the record key it names; session close now names an agent's own record key in session.close, so pinning the ship key is enough. Behaviour change: a package closed before this release by an agent with its own key fails receipt_binding by default; read it with --structural. verified and signatures-pass now require checks that passed (an artifact signature, receipt_binding, signer_trust), and structural-pass requires a sealed set. When any signing key is trusted, every other signer must be trusted or vouched for by the session's own signed records. Approval signers are judged in a new approval_signer row; approval use is checked against the approval's signed scope (approval-use-limit), and approval-consuming actions need their use records (approval_evidence). A key id that names a different key than the root pinned under it fails key_id_collision. treeship verify <package>, treeship dashboard and session report report the same verdict as package verify, and verify <package> honours --config.

  • trust add protects pinned key ids. Pinning a different public key under an already-pinned key id needs --replace and shows both fingerprints; this ship's own key ids cannot be re-pointed at all. Pin hints show the key's fingerprint and leave out --yes, so a key copied from a package is confirmed before it is trusted.

  • Approvals carried with a package are checked like sealed ones. An approval minted before the session, which the package carries in approvals/grants rather than in the sealed set, is verified against its signature and its signed use limit; two approvals that share one nonce fail. package verify --structural judges trust with this ship's own keys too, and trust add checks this ship's own key ids against the workspace --config names.

  • --format json always emits JSON. doctor, package inspect, pending, declare --show, otel status, templates and version wrote nothing at all in JSON mode and exited 0, because every line they print goes through a printer call that is a no-op there. Each now emits one document (doctor: every check with status, label, detail and suggestion, plus the tallies; package inspect: the receipt and its side-effect summary; pending: the queue; declare --show: the declaration or found: false; otel status: configuration without the credential; templates: the catalogue; version: version and implementation). A new test runs every command that answers locally in JSON mode and requires a parseable document, and checks the binary's own command tree (a hidden __dump-cli) so a new command must be run or named with a reason. merkle status, checkpoint and merkle proof are the follow-up (their file is changing in another fix). (0.31.9 full test, CLI-10.)

  • doctor no longer reports auto-push as enabled. The auto_push setting is a stub (the daemon logs "not implemented"); doctor now warns that it is set but does nothing, and the bundled templates no longer turn it on. (0.31.9 full test, DOC-1.)

  • merkle status, checkpoint and merkle proof emit JSON in --format json. merkle status wrote 0 bytes; checkpoint and merkle proof wrote the root shortened to 16 hex characters and numbers as strings, so a script could neither compare a root nor read a size. Each now emits one document with the whole sha256: root and real numbers (tree_size, index, height, leaf_index), and the JSON contract test runs all three instead of skipping them. This closes W1-10. (0.31.9 full test, CLI-10.)

  • daemon status no longer kills the daemon, and daemon start backgrounds. The pid file holds <pid> <start epoch>, but the reader parsed the whole line as one number, so every read failed: status reported "stopped", deleted the file, and the daemon (which exits when its pid file disappears) died of a status check; stop then said "not running". The reader takes the pid, a pid file is removed only when it names a process that is gone, and a file that does not parse is reported and left alone. daemon start without --foreground now starts a detached child and returns with its pid; it used to run in the foreground and print "tip: run with &". (0.31.9 full test, CLI-8.) Two starts racing used to both truncate the pid file, so the loser wiped the winner's pid and left a daemon nobody could stop; the file is now locked before it is written, liveness is decided by the lock rather than by a pid that may have been reused, and a start succeeds only when its own child holds the lock. The daemon starts in its own session, so closing the terminal does not take it down.

  • keys rotate moves the default everywhere. The keystore promoted the successor but hub attach, prove and the dashboard read the default from config.json, which still named the predecessor; a hub attached after a rotation was bound to a key that stopped being valid when the grace window closed. The keystore manifest is now the one source of truth and every reader uses it; nothing writes config.json on rotation (a first fix did, and flattened a project stub onto itself, breaking the ship). Rotations are serialised, so two at once form one chain instead of two successors for one key. config::save follows a project stub to the config it extends and writes atomically. hub attach records created_at as an RFC 3339 timestamp instead of "<epoch>Z". (0.31.9 full test, CLI-9.)

  • An honest approval or room package verifies under --strict on a second ship. --strict failed every approval-bound and every room package on any machine but the producer's. The approval, the invitation, the participant and its liveness record are sealed unchained, and chain_completeness treated them as loose; and the producer's Approval Use Journal, which a second ship can never hold, was required. An unchained artifact that a signed reference inside the sealed set accounts for is now bound: an approval whose nonce a chained action consumes in its signed approvalNonce, a participant whose countersign verifies against its sealed invitation, that invitation, and the host's liveness record for that participant. The referencing signer must be authenticated here, so an attacker's own action cannot vouch for an approval it smuggled in; package order stays unproven, and the row says so. The local-journal check applies only where the package was produced (a sealed artifact verifies under one of this ship's keys); elsewhere a local_journal row says it is not applicable and what was checked instead, and --strict does not promote it. Audit 2026-09-25, CLI-18.

  • A countersigned room participant verifies inside its package. The joining agent and the host sign a participant's canonical bytes, not the DSSE envelope, so package verify checked it like any other artifact and failed every honest room (treeship verify passed the same artifact). The package verifier now checks a participant against the invitation it redeems, which must be sealed in the same package and signed by a key the package carries that is the invitation's issuer: both signatures over the canonical bytes, the same session, and the id re-derived from the pending envelope. A missing or failing invitation fails the row; it never falls back to the generic check. The host's key, not the joiner's, is what signer_trust judges. An invitation redeemed by more participants than its max_uses fails the extra rows, and an artifact sealed twice in one package fails its second row. Audit 2026-09-25, CLI-3 (package half; cross-ship countersign follows).

  • A room works across two machines, and the host is the single-use gate. A joiner on another machine could not be countersigned: session countersign required the invitation's nonce to have been consumed in the host's own journal by session join, which only happens when both run on one machine. The same check let a host countersign any further pending envelope for an invitation once one join had consumed it. Now session join --out <file> writes the pending envelope for the host, and session countersign --pending <file> countersigns it: the file must hold exactly one pending envelope whose joining-agent signature verifies, its id is re-derived from its bytes, and the invitation it redeems must be one this host issued. Across machines --challenge and --challenge-response are required; on one machine they stay optional for now, with a notice that they become mandatory in 0.32. Countersign itself enforces single use: it reserves the use in the host's journal before signing, up to the invitation's max_uses, and never releases it, so a second countersign for a single-use invitation is refused wherever the join ran. Audit 2026-09-25, CLI-3 (W1-3b).

  • A countersign checks that the join challenge is fresh and the session is still open. The challenge must be no older than ten minutes (TREESHIP_JOIN_CHALLENGE_WINDOW_SECS), and the joiner's answer must be signed between the challenge and now; --pending also requires --challenge-issued-at. Countersign refuses an invitation whose session is not the one open on this ship. Every check runs before the use is reserved, so a refused countersign never uses up the invitation, and a busy journal says to retry instead of claiming the invitation was already used.

  • A package without its close record fails package verify. Nothing in a package says, under a signature, which release built it, so a package from before 0.31.4 and a later package with record.json deleted look the same. The verifier treated both as legacy and downgraded the receipt_binding check to a warning, so an edited package could still verify by default (see the advisory published with 0.31.10). Now a package that carries artifact envelopes (0.31.2 and later) but no close record fails receipt_binding, and the message says the sealed set is not under a signature. For a package you know 0.31.2 or 0.31.3 built (the only releases that wrote that shape), --structural reports what can still be checked, with verdict structural-pass, never verified. Packages from before 0.31.2 are unchanged. Audit 2026-09-25, CLI-4; the cases are T2 vectors.

  • merkle verify says "not pinned", not "signature invalid", for a checkpoint signer you have not pinned. Following the CLI's own steps on a fresh ship (checkpoint, merkle proof, merkle verify) failed with "checkpoint signature invalid", exit 1, although the signature and the inclusion proof both held: the signer just was not pinned as hub_checkpoint. The core verifier already told the two apart (Checkpoint::verify_detailed); the CLI collapsed them. Now an unpinned signer exits 6, a new code in the exit-code table, and names the key to confirm out of band before treeship trust add <key_id> ed25519:<key> --kind hub_checkpoint (no --yes: the key comes from the checkpoint itself). --format json gives {"outcome": "not_pinned", "key_id", "public_key"}. A signature or proof that does not hold still exits 1. Audit 2026-09-25, CLI-5.

  • merkle publish points at the attached hub's proof, not treeship.dev. After publishing to a self-hosted hub, the hint still said treeship.dev/merkle?id=…, a page that knows nothing about that hub. It now prints <hub>/v1/merkle/<artifact> (the hub's own proof endpoint), and the treeship.dev page only for api.treeship.dev. Audit 2026-09-25, CLI-12 (the merkle half of W1-9).

  • Every failure exits nonzero. prove, prove-chain and verify-proof on a build without --features zk printed a warning and exited 0, so treeship verify-proof /nope.zkproof passed a script's exit-code check. They now exit 5, a new code for "not compiled into this build", and otel on a build without the feature does the same. otel export to an unreachable collector exited 0 after printing "✗ export failed"; approval status and approval uses on a grant nobody has heard of reported "unbounded, within max_uses" and exited 0; resolve on an agent with no card exited 0. All four are errors now (exit 1). A new contract test (packages/cli/tests/contract.rs) walks every command's --help and pins each of these failure paths to its exit code. (0.31.9 full test, CLI-6.)

  • The zk build compiles again, and CI builds every feature the binary tells you to build. cargo check -p treeship-cli --features zk had failed since a refactor dropped an import, while the non-zk stubs told users to rebuild with exactly that flag; CI built only the default features, so nothing noticed. The import is back, two functions that were dead under --features zk are gone (zk_status duplicated zk-setup; enqueue_proof_job was an unused wrapper), and a new feature-matrix CI job checks --no-default-features, --features zk, --features zk-bonsai and the wasm32 target of core-wasm with warnings denied. Four warnings the wasm32 build had carried (unix-only items not gated to unix) are gated. (0.31.9 full test, CLI-7 and PKG-3.)

  • A receipt's API URL verifies as written. treeship verify https://api.treeship.dev/v1/receipt/<id> and verifyReceipt() in @treeship/verify both did a blind replace of /receipt/ with /v1/receipt/, so the documented API form became /v1/v1/receipt/<id> and a 404. Both now apply one rule: the human page /receipt/<id> maps to /v1/receipt/<id>, the API path and the site's /api/receipt/<id> mirror are kept, the host is never rewritten, a trailing slash is dropped, the query string kept, and a URL that names no receipt is refused instead of fetched. The rule lives in tests/vectors/receipt-urls.json, which both test suites run. (0.31.9 full test, CLI-11.)

  • Share URLs come from the hub you pushed to. After hub push or session report to a self-hosted hub, the CLI printed https://treeship.dev/verify/… and /receipt/… addresses that held nothing, because the hub hard-coded them in its replies. The hub now builds them from configuration only: TREESHIP_PUBLIC_URL names the site that renders pages for its data; TREESHIP_HUB_PUBLIC_URL names the hub's own public origin, for a hub with no page site, which then answers with its own API URLs (<hub>/v1/receipt/<id>, <hub>/v1/artifacts/<id>), and treeship verify <url> accepts those; with neither set it keeps today's treeship.dev pages, so the public hub needs nothing. No request header is consulted, since these URLs are stored with the artifact and served to every later reader. The CLI prints what the hub returns, except that a hub which is not treeship's and still answers with a treeship.dev address is corrected to its own API URL, as is a hub that returns none; session report prints no preview or package page links for a hub that has no pages; hub push no longer suggests treeship open, a command that does not exist. The hub half takes effect on the public hub when it is next deployed. (0.31.9 full test, CLI-12.)

  • Release binaries carry signed build provenance and an SBOM (W2-2, SUP-3). The release job now attests SLSA build provenance, signed through Sigstore with GitHub's OIDC token, for the four binaries, SHA256SUMS and a new treeship-sbom.spdx.json (an SPDX SBOM of the Rust workspace's Cargo.lock, a superset of what the CLI links), and attaches the SBOM to the release. Attestation runs before the release is created, so a signing failure publishes nothing. Check a download with gh attestation verify treeship-<platform> --repo zerkerlabs/treeship.

  • @treeship/verify verifies whole packages (W2-6, PKG-2). verifyPackage(files, { pinnedKeys }) checks every artifact envelope's Ed25519 signatures in core-wasm, binds each envelope to the receipt's artifact_id and full digest, and checks the signed close record against the exact bytes of receipt.json. It returns the CLI's verdicts: verified only under keys you pin, signatures-pass when a signer is known only from the package's keys.json, structural-pass for receipt.json alone (scope: 'receipt-only') or for packages holding approvals (sealed, or carried as use records and grants under approvals/), endorsements or rooms, whose rules it leaves to treeship package verify (scope: 'partial'), and failed otherwise. The T2 vectors gain verify_js and verify_js_pinned columns: every tampered package is failed or, for the two room vectors, capped at structural-pass; none reads signatures-pass or verified. verifyReceipt is unchanged and still structural only.

  • Security headers added to docs.treeship.dev (W2-7). docs/next.config.mjs now sends Content-Security-Policy, X-Frame-Options: DENY, and Strict-Transport-Security: max-age=63072000; includeSubDomains, matching the header set treeship.dev PR #121 shipped for www. The CSP is narrowed to what the docs site actually loads: self-hosted fonts (no Google Fonts exception), same-origin fumadocs search via /api/search (covered by default-src 'self'), and Google Analytics (googletagmanager.com / google-analytics.com) in script-src/img-src/connect-src. No WASM allowance -- that's only needed on the main site's /verify page. Verified with a production build, a local next start, and a headless crawl of the main page types plus the search flow: zero CSP violations, GA and search both load over 'self'/the allow-listed GA hosts.

  • The hub sends HSTS and nosniff. Every response from the hub carries X-Content-Type-Options: nosniff, and every response that arrived over TLS (directly, or through a proxy that says so with X-Forwarded-Proto) carries Strict-Transport-Security: max-age=63072000; includeSubDomains; plain-HTTP local development is untouched. Takes effect on api.treeship.dev when it is next deployed. (0.31.9 full test, WEB-3, hub half.)

  • Security claims registry; keystore/checkpoint/ZK wording rewritten (W3-1, DSEC-1/DSEC-2/DSEC-3/DOC-8). Docs described the keystore as "machine-bound" and protected by a "passphrase" -- neither is true (<!-- claims:keystore-at-rest -->); it's an AES-256-GCM keystore under a key derived from a secret seed file stored beside it, no passphrase, and copying .treeship/ is enough to sign elsewhere (SECURITY.md, threat-model.md, security.mdx). Checkpoints were described as externally anchored/witnessed; a hub checkpoint is single-signed by the publisher and the hub never countersigns (<!-- claims:checkpoint-not-witnessed -->) -- only per-artifact Rekor anchoring (TS-2026-003) is a real external witness today (<!-- claims:rekor-artifact-anchoring -->), and room-sessions.mdx's checkpoint-root/OpenTimestamps design is now marked as a design sketch, not built (<!-- claims:checkpoint-root-anchoring -->). zero-knowledge.mdx contradicted its own quarantine callout by saying the browser verifies Groth16 proofs "today"; core-wasm/README.md and zk-circom/README.md made the same overclaim and the latter referenced a nonexistent setup/ ceremony directory. "Tamper-proof"/"immutable" language (AGENTS.md, compliance.mdx, a blog title) is now "tamper-evident" (<!-- claims:receipt-tamper-evidence -->), with the actual boundary stated (unsigned fields can change; the key holder can re-sign; an approver URI isn't proof of a specific human unless they sign with their own pinned key). Also fixed a wrong key-id derivation formula and a base32-vs-hex artifact-id typo in threat-model.md, and its stale trust add --kind merlke-checkpoint example / "no default trust root" claim (the built-in Sigstore transparency_log key is a default root).

    New: docs/claims.yml registers every claim above with a status (shipped/next/designed/retired) and a proof path, and scripts/check-claims.py (wired into ci.yml's docs-drift job) fails if a banned phrase (passphrase, machine-bound, tamper-proof, immutable, anchored, witnessed, air-gapped) appears in the files claims.yml scans without a <!-- claims:<id> --> / {/* claims:<id> */} marker naming a real entry, or if a claim's proof path doesn't exist. Scoped to the specific files these findings named, not every doc in the repo -- the same English words are used correctly elsewhere for properties that have nothing to do with the keystore or the verifier (HTTP cache headers, a workflow spec's genuinely immutable declared graph).

    PLATFORM.md is marked a historical snapshot at the top of the file rather than rewritten line by line; it is a frozen 0.4.0-era document (current release is far past it) and pointing readers at current docs is more honest than patching individual stale claims in a doc nobody should be reading as current.

  • Regenerated the fictional command/SDK surface in agent skill files (W3-2). skills/treeship/SKILL.md, skills/treeship-perplexity/SKILL.md, integrations/hermes/treeship.skill/SKILL.md and the whole Kimi Code bundle (integrations/kimi-code-plugin/) taught commands, SDK calls and Hub routes that don't exist: treeship approve --approver, key show, treeship inspect, bundle verify, treeship attach <agent>, a Treeship/Ship.init() class API, dock_push, expires_in, a POST /v1/attest endpoint, a websocket stream, and TREESHIP_API_KEY/TREESHIP_AGENT/TREESHIP_HUB_ID, none of which are real. Rewrote every command block and SDK example against the actual CLI (packages/cli/src/main.rs), the TS SDK's ship() factory (packages/sdk-ts), the Python SDK's scoped attest_approval/hub_push (packages/sdk-python), the real 9-tool MCP bridge (bridges/mcp/src/server.ts), and the real Hub routes (packages/hub/main.go). Fixed the three Kimi helper scripts' dock_push calls and its config.toml.template's reference to a nonexistent treeship integrations install command. The oversized Kimi references/{hub_api,mcp_bridge,typescript_sdk}.md files, which had drifted the worst, are now short and point at the generated docs/source instead of re-deriving a route or command table that goes stale again.

  • @treeship/openclaw-plugin's status now matches npm reality (W3-3, PKG-1). The install matrix (integrations/agents.json, which generates install.mdx and the .well-known/treeship-agents.json mirror) marked the plugin 🟢 shipped with openclaw plugins install @treeship/openclaw-plugin as the install command; that package was never published (npm view returns E404) and the command fails. Downgraded to 🟡 candidate and swapped in the local build+install command that actually works today (cd integrations/openclaw-plugin && npm install && npm run build && openclaw plugins install --local ./); integrations/openclaw-plugin/README.md leads with that path now. Also fixed the neighboring Kimi Code plugin row, which advertised a treeship integrations install kimi-code command that doesn't exist. Regenerated both derived files from the JSON source rather than hand-editing them.

  • Install and SDK docs re-verified against 0.31.9 (W3-4, DOC-6). guides/install.mdx said Linux arm64 had no binary until v0.25 -- it's shipped in every release since v0.25.1, and both curl | sh and npm install -g treeship install it directly today, no cargo fallback. core-wasm/README.md named a nonexistent treeship-core-wasm npm package and showed a signature and an init() call that don't exist (correct: @treeship/core-wasm, verify_envelope(envelopeJson, trustedKeysJson) returning a JSON string, no init, two wasm-pack builds not one). sdk/verify.mdx and sdk/typescript.mdx carried a "core-wasm doesn't load under Node 22" warning that was fixed in v0.25.0 -- verified @treeship/verify@0.31.9 loads and runs on Node 22.23.1 before rewriting it. sdk/python.mdx had two contradicting warnings saying attest_approval() and wrap() are "currently broken against CLI 0.24"; both work today (scoped approvals, --format json wrap) and the page already described the working behavior further down the same page. cli/setup.mdx said setup --format json returns {}; it returns a real structured payload (SetupResult) today. Also re-verified and dropped stale v0.9.x/v0.24 version anchors in sdk-ts/README.md, sdk-python/README.md, and fixed a fictional Ship.attestAction() mention in integrations/agent-skills.mdx.

  • /v1/verify documented as retired; artifact-id formula corrected (W3-5, DOC-3/DOC-7). AGENTS.md, PLATFORM.md and ONBOARDING.md described GET /v1/verify/:id as a live server-side verifier, including a worked example claiming it "should return a passing ChainResult"; the route has been intentionally retired since before this audit and returns 410 with no verdict -- the hub is transport/index only. Rewrote each mention to say so and point at local verification. Separately, docs/content/docs/reference/receipt-format.mdx had the artifact-id truncation backwards: hex(sha256(PAE_bytes))[..16] truncates the hex string to 16 characters (8 bytes of entropy, the wrong id); the real formula (packages/core/src/attestation/id.rs) truncates the 32-byte digest to 16 bytes first, then hex-encodes, giving the correct 32-character id. A verifier built from the old formula would compute wrong ids for every artifact. Verified the fix against the Rust implementation's own doctest (id length 36 = art_ + 32 hex chars) before writing it.

  • CLI reference pages for hidden-but-shipped commands (W3-6 follow-up, DOC-9). hook, templates/template, prove/prove-chain/verify-proof/zk-setup/zk-tls-setup, approval, and declare are implemented, tested, and shipped (several stable in docs/feature-inventory.yml) but had no reference page and are hidden from --help -- previously undocumented anywhere except a one-line mention on a concept page. New pages: cli/hook.mdx, cli/template.mdx, cli/zk.mdx, cli/approval.mdx, cli/declare.mdx, each opening with a Callout explaining what "hidden from --help" means here (implemented and stable, just not advertised) and, for the ZK commands, that release binaries ship without the zk feature -- prove/prove-chain/verify-proof exit 5 ("not compiled into this build") without it, while zk-setup/zk-tls-setup aren't feature-gated and run in every build. Flag tables were generated by hand-invoking treeship <cmd> --help on a local build and checked against the real CLI with docs/scripts/check-cli-option-tables.py (179 rows across 35 commands, 0 mismatches) rather than typed from memory. Linked the new pages from the relevant feature-inventory.yml entries' docs: lists and added all five to cli/meta.json's sidebar.

  • Unreleased judge flags labeled; hidden-command status made explicit (W3-6, DOC-5/DOC-9). cli/judge.mdx documented --contract, --state-out, --resolve/--by/--decision/--route/--reason as shipped; they're implemented on main but not in the latest tagged CLI (0.31.9 rejects them). Added "(next release)" markers to each option-table row and a Callout at each section introducing them, pointing readers at treeship --version vs. the CHANGELOG rather than assuming the docs match their installed binary. The generated command-matrix.mdx (built from whatever this repo compiles, which can be ahead of the last tag) now says so in its own header, from scripts/gen-cli-reference.py's template. Separately, docs/feature-inventory.yml marked checkpoint, merkle *, harness *, approval *, bundle *, agent register and declare stable with no indication they're hide = true in main.rs (not in treeship --help) -- added a hidden: true field (documented in the schema header) plus a clarifying note to each of the 6 affected entries, so "stable and hidden" reads as a deliberate, tracked design choice instead of an undocumented gap. Also added the missing rekor-artifact-anchoring inventory entry (shipped 0.31.9) and roadmap entries for checkpoint-root-anchoring, rfc3161-timestamps and opentimestamps. Per-command reference pages for the hidden surfaces that have none (templates, prove, prove-chain, verify-proof, zk-setup, zk-tls-setup, hook) are a separate, larger doc-writing task, not addressed here.

  • Broken doc links and anchors fixed (W3-7, P3-LINKS). about/changelog.mdx had 5 repo-relative links that 404 as rendered docs pages -- three (evaluator-kit, receipts-in-pull-requests, verification-reporting) now point at their real /guides/... routes, and the two that live outside docs/content/ (verdict-invariant.md, TS-2026-002.md) now link to their GitHub blob URLs. The #the-gate anchor was broken on 3 pages (cli/judge.mdx, integrations/typesafe-jev.mdx, blog/any-judge-one-receipt.mdx) -- the real heading is "The gate (PreToolUse)", slugified to #the-gate-pretooluse, which cli/halt.mdx and reference/predicates.mdx already used correctly. blog/multi-agent-skills.mdx linked to a stale SDK anchor computed from an old attest_approval signature; recomputed against the current one. integrations/meta.json was missing buzz, grok-bot and rig from its sidebar pages list even though all three .mdx files exist -- they were reachable only by typing the URL directly, now added to the sidebar.

  • Nonsense input is refused before anything is signed. attest decision --confidence 7.5 was signed and printed as 750%; attest endorsement --kind bogus, attest action --input-digest abc and --actor "", session event --meta '{bad' (silently dropped, where attest refused it), declare --valid-until notatime, history --class bogus and --since yesterday (silently matched nothing), a handoff of artifacts that do not exist, and --format xml (ran as text) were all accepted. Each is now a usage error (exit 4) with the accepted form in the message: a confidence is 0.0 to 1.0; an endorsement kind is validation, compliance, countersignature or review; a digest is sha256: plus 64 hex characters; an actor is a scheme://name URI; --meta is a JSON object; timestamps are RFC 3339 and --since also takes a duration back from now (30d, 12h); --artifacts must name artifacts in the store; --format is text or json. The contract test pins each. (0.31.9 full test, CLI-14.)

  • Output no longer contradicts itself. verify-capability and resolve opened with a green "✓" headline for a revoked card and said REVOKED two lines down; the headline is now the verdict. vi check printed PASS for every rule and then FAIL for the same mandate; rules read as passed only when the mandate is satisfied. attest action --v2 signed an action outside its grant's scope without a word; it now warns at signing time (the receipt still records the violation, and verify still reports it). grant revoke on a revoked grant and judge --resolve on a resolved judgement minted a second receipt as if nothing had happened; both now refuse and name the standing one. treeship ui off a terminal said Device not configured (os error 6); it now says it needs a terminal. dashboard --host 0.0.0.0 said "local-only"; the mode line follows the bind address. approve said "the command will now proceed" when a shell hook cannot hold a command back; it now says to run it again if it did not run. Hints named commands that do not exist (treeship zk-tls notary setup, treeship share last); a test now checks every hint and template line against the binary's command tree. trust add and keys list showed different fingerprints for one key (one hashed the key's text, the other its bytes); both now hash the bytes. verify-capability on an artifact that is not a card says so instead of a JSON parse error. (0.31.9 full test, CLI-15 and P3-UX.)

  • The hub says which build it is, and checkpoints can publish on a schedule. GET /v1/version (alias /version) on the hub returns the version, commit, build time and Go runtime from the build, "dev"/"unknown" when unset; /health joins /healthz. The public hub had no version endpoint, so nobody could tell from outside whether a fix was deployed. GET /v1/merkle/checkpoint/latest adds age_seconds, computed at read time, for a monitor to alert on. On the CLI, treeship checkpoint --publish seals and pushes in one command and exits nonzero if the push fails; an opt-in checkpoint.every in config.yaml makes the daemon seal and publish on that interval, logging each publish, and doctor reports the cadence only as it actually runs. A checkpoint is signed by the publishing ship; the hub stores it and signs nothing. The hub half takes effect when it is next deployed. (0.31.9 full test, WEB-4.)

  • treeship add follows your own dotfiles links. A harness config path under your home that is a symlink (~/.cursor -> dotfiles/cursor, ~/.codex/config.toml -> dotfiles/codex.toml) is now configured where the link points, and the link is kept. Earlier releases skipped such paths with "config path contains a symlink". The project TREESHIP.md in the working directory is still not written through a link.

  • Changelog entries are one file per change. A PR now adds changelog.d/<task-or-topic>.md instead of editing the ## Unreleased section of CHANGELOG.md, so parallel PRs no longer conflict on every merge. scripts/release.sh prepare folds the fragments into a new ## <version> (<date>) section (scripts/changelog.py assemble). The docs changelog page shows pending fragments as its Unreleased section. The docs-drift CI job validates fragments and fails a PR that adds bullets to ## Unreleased directly.

  • Which workspace a command uses is never a guess. A project's .treeship/config.json stub records the account's global config by absolute path, so under a different HOME (a test harness, a container) with no --config or TREESHIP_CONFIG, discovery still routed to the original account's keystore and signed into its store. A discovered stub that extends a config outside the current HOME is now refused with the two ways to say which workspace you mean; an explicit --config or TREESHIP_CONFIG always wins over discovery (a test pins it); and discovery never climbs above HOME. status and doctor already print the resolved source. (Found by the 0.31.9 full test's website lane.)

  • Claude Code MCP-only docs no longer point at a file Claude Code never reads. docs/content/docs/integrations/claude-code.mdx's Method 3 and integrations/claude-code/README.md's Method 2 both instructed a manual MCP config at ~/.claude/mcp.json -- the same stale path CLI-16 (#494) fixed treeship add claude-code from writing to. Claude Code actually reads MCP servers from a project's .mcp.json or ~/.claude.json's top-level mcpServers key; both docs now say so and point the copy-paste template at .mcp.json in the project root.

  • Two-machine room-join docs (W1-3b follow-up). session join --out, session mint-challenge, session answer-challenge, and session countersign --pending (#499) let a host and joiner finalize a room join across separate machines, gated on a mandatory liveness challenge -- none of this was documented. docs/cli/session.mdx gained full reference sections for mint-challenge and answer-challenge, and updated join/countersign entries covering --out, --pending, the challenge flags, the 10-minute TREESHIP_JOIN_CHALLENGE_WINDOW_SECS window, single-use enforcement, and the closed-session refusal. concepts/multi-agent-sessions.mdx gained a "Joining from another machine" walkthrough with the full flow and real CLI output, verified end to end against a locally built binary (two isolated HOMEs standing in for two machines). concepts/room-sessions.mdx's "What ships today" section now points at it instead of implying same-machine-only.

  • session join no longer suggests trusting the host's key blind. When the invitation's issuer is not pinned under session_host, the hint now says to confirm the key with the host out of band first and offers treeship trust add … --kind session_host without --yes, the same rule merkle verify follows for an unpinned checkpoint signer. A copy-paste that silently trusts a key taken from the thing being checked is never offered.

  • The judge docs no longer mark --contract, --state-out and --resolve as "next release". They ship in 0.31.10.

  • Workspace writes no longer follow links. Every file Treeship writes under a workspace, a keystore, a store or a session, and every file it writes where the user asked (--out, a package, a proof in the working directory), is created fresh and renamed into place, never through a symbolic link at the path or on the way to it, never into a file that has another name (a hard link), and temporary files are created exclusively under random names. A workspace, keystore or store directory that is itself a link, or sits below one, is refused with a message saying how to point --config at the real path. Files in the user's own home (a shell rc file, trust roots, saved templates) follow the user's own link and are written at its target, keeping its mode. A project config found by walking up from the current directory may keep its keystore and artifact store only inside its own .treeship; a config that points elsewhere is honoured when named with --config or TREESHIP_CONFIG.

  • A flaky core test no longer fails CI at second boundaries. bundle::tests::anchor_proofs_travel_and_merge_on_import built the same action statement twice and asserted both signings gave one id; the statement carries a timestamp, so the ids differed whenever the two constructions straddled a second. It now signs one statement in both stores. Test-only; no behaviour change.

  • GitHub Releases now lead with the actual CHANGELOG section (W4-3). release.yml's release job used only generate_release_notes: true, GitHub's compare-commits diff -- no editorial content, and not very useful against this repo's squash-merge history. It now extracts the tagged version's own ## X.Y.Z (date) section from CHANGELOG.md (guaranteed present by tag time, since scripts/release.sh prepare runs changelog.py assemble before tag ever runs) and passes it as the release body; generate_release_notes stays on, so GitHub still appends its auto-generated section below it.

  • Removed stale tracked sdk-python build artifacts (W4-3). packages/sdk-python/dist/treeship_sdk-0.2.0-py3-none-any.whl and .tar.gz were committed to the repo even though .gitignore already excludes packages/sdk-python/dist/ -- ignore rules don't untrack a file already committed. Removed both; nothing else under a dist/ path is tracked.

  • Every advertised install one-liner is https:// (W0-3, SUP-2). curl -fsSL treeship.dev/setup | sh had no scheme, so curl's first request went out over plain HTTP before the redirect to https ever happened -- a tamperable first hop, since curl doesn't use HSTS. Fixed across 37 files (root docs, the docs site and its sidebar copy widget, every integration README and skill, the TS/Python SDKs' own error messages) to curl -fsSL https://www.treeship.dev/setup | sh (or /install). Also updated every description of what the one-liner does: it now asks before running treeship add --all, and skips that step (not just the prompt) with no interactive terminal unless TREESHIP_SETUP_YES=1 is set. Added a CI grep (docs-drift job) so a bare treeship.dev/setup//install curl target can't come back unnoticed.

  • Security records refreshed (W0-4). SECURITY.md's supported-versions table said 0.24.x was current; it now says 0.31.x. TS-2026-003 said its fix shipped "the release after 0.31.8"; it shipped in 0.31.9, and the advisory now says so. README's security history was missing TS-2026-003, named the wrong version for the TS-2026-001 keystore fix (v0.10.4 instead of v0.10.3), and said packages are smoke-tested "before a tag goes live" — the release workflow smoke-tests the built binary before publishing, then separately re-verifies the published npm/PyPI packages after they go out; the README now describes both steps.

  • Endorsing anything but the latest artifact no longer reads as tampering. An endorsement's subject is what it endorses; storage chained it onto the most recent artifact, but the statement signed no parent, so verify fell back to the subject and reported SIGNED LINKAGE BROKEN — possible tampering for the endorsement and everything chained after it, sessions included. Endorsements now sign parentId, and the artifact and package verifiers read the signed edge through one core function (treeship_core::verify::signed_parent). Endorsements made by 0.31.9 or earlier sign no parent: they still verify, with a chain linkage partly unsigned warning naming them (chain_linkage_unsigned in verify --format json, a chain_linkage WARN in package verify), and package verify --strict fails them. A present parentId is always checked, never read as the legacy shape. Audit 2026-09-25, CLI-1.

  • Honest flows and tampered packages are CI gates. tests/flows/ runs each documented workflow end to end and requires package verify to pass (T1); tests/vectors/packages/ is a frozen set of honest and tampered packages with the verdict every verifier must give each, checked for the CLI in three modes and for the receipt-only verifier that @treeship/verify runs (T2). A tampered package is never verified or signatures-pass, in any mode. Flows and columns still wrong on main (endorsing a non-latest artifact, room countersigns in packages, merkle verify on an unpinned checkpoint signer, a package with its close record deleted) are marked with the fix that repairs them and fail the run once they pass. Audit 2026-09-25, T1 and T2.

  • Docs no longer claim session close --auto-push or auto_push: true push automatically (W0-5). The flag doesn't exist and the config setting is an unimplemented stub. Fixed the claim in TREESHIP.md, templates.mdx's schema reference, the Claude Code plugin's README and two of its skill files, integrations/claude-code/CLAUDE.md, and npm/treeship/README.md. The Claude Code plugin's SessionEnd hook does have a real opt-in auto-publish path (TREESHIP_AUTO_PUBLISH=1), now described accurately alongside it.

  • Every policy gate reaches the JSON top line. verify --max-unwitnessed and --require-authority exited 1 when they refused, but --format json still read outcome: "pass", failed: 0; a script reading only outcome got a green. Now a failed policy makes outcome fail and is named in the new failed_gates list. The same split finding 30 closed for mandates, found again by the 0.31.9 QA run.

  • package verify no longer fails a package for carrying non-action artifacts. The approval-use-action-binding row checked every envelope under artifacts/ as if it were a consuming action, and since 0.31.2 a package carries every sealed envelope, so the merchant demo failed on its grant and its coverage receipt ("not an ActionStatement"). The row now checks v1 action envelopes only and reports how many non-action envelopes it skipped. Found by the 0.31.9 QA run; the shopping package, which carries no such artifacts, was unaffected.

  • The human's decision on a judgement is its own signed artifact. An escalated judgement used to go quiet: nothing recorded who decided, what, or when. treeship judge --resolve <judgement> --by human://alice --decision allow|deny|route signs a judgement.resolution.v1 receipt under the decider's URI, with the judgement as its subject, chained onto the session, recording what it overrides when it contradicts the judge. package verify's judgements row pairs resolutions with escalations and reports an escalation with no resolution in the package as open (a warning), and names any judge decision a resolution overrode. A machine field saying "a human overrode this" would be the self-report a receipt exists to replace; a receipt under the decider's name is a claim the decider made.

  • The receipt commits to the judge's exact answer. A judgement is signed by the caller, so on its own it is the caller's claim about what the judge said. judgement.v1 now carries response_digest, the hash of the raw response bytes as received, and judge.request_id, the judge's own id for the answer when it gives one (the Jev adapter passes x-typesafe-request-id through); a fabricated answer has to come with a body that hashes to the digest, and a judge that keeps its responses can be asked about the id. Judge co-signing is the next step and the docs say so. --state-out writes the canonical state the receipt's state_digest commits to, for a verifier who re-runs the rules judge or checks what an outside judge was shown.

  • TREESHIP_JUDGE_STRICT=1 makes the Claude Code gate escalate a call the judge could not answer to the operator instead of letting it proceed with a note; the judge page says why the default fails open (the card already allowed the call) and why treeship judge itself never does.

  • judgement.v1 carries the decision contract. treeship judge --contract ticket-router@3 records contract: {id, version} in every receipt: the versioned definition of state, question, options and threshold, separate from the application's version, so a change to the question is reviewable like a code change.

  • Jev in the judge slot. examples/judge-adapters/jev puts TypeSafe's Jev behind the judge contract: a dependency-free adapter that speaks the contract on one side and POST /v1/systemone on the other, pins the model version so the receipt names one, and marks every answer replayable: false. Tested in CI against a mock of TypeSafe's documented request and response shapes (all three primitives mapped, an unavailable judge as an error, the judgements row naming the model). An independent implementation against the published API, not run against the live service. New integration page, systems registry row, and the post "Any judge, one receipt".

0.31.9 (2026-09-25)

  • A lifted halt stays lifted, whatever local file is rolled back. The 0.31.8 fix read the store through index.json, an unsigned cache in the same directory as the halt marker; restoring both re-armed a halt while its signed lift sat on disk. The lookup now scans the record files and honours a halt, or a lift, only when its envelope verifies under the workspace key; the record's unsigned key_id is not consulted. halt list says which reason applies (lifted by art_…, or not signed here) instead of "no signed artifact" for both. (Retest 0.31.8, T20.)

  • verify --full fails what verify fails. Full mode never received the invalid-mandate fix: it opened with a green chain line, closed with three green summary rows, and exited 0 on an artifact the default mode exits 1 on. The summary now carries an authority row, the AUTHORITY INVALID banner follows it, --require-authority is honoured, and every failure exits 1 in every mode. (Retest 0.31.8.)

  • attest receipt --kind list stands alone, without --system.

  • Security: Rekor anchoring works, and only a verified anchor counts (TS-2026-003). <!-- claims:rekor-artifact-anchoring --> The hub's Rekor submissions had never succeeded. Treeship signatures are unpadded base64url, which Rekor rejects, and the hashedrekord type cannot verify plain Ed25519 anyway. The failure was swallowed and the CLI printed rekor: pending. The hub now submits a dsse entry, which Sigstore's staging log accepts, and returns rekor.status (anchored, failed, skipped) with a reason and the full log entry. Re-pushing an unanchored artifact retries it. Separately, verify --max-unwitnessed took witness times from Record.anchors[].observed_at and claimed times from Record.signed_at. Both are unsigned values in files the operator controls, so an edited record passed any policy. It also ran only in text output: --format json and --full ignored it and exited 0. Now an anchor counts only if its Rekor entry verifies offline: signed entry timestamp, RFC 6962 inclusion proof, signed checkpoint, and binding to this artifact's payload and signature. Its time is Rekor's integratedTime. Claimed times come from each statement's signed timestamp. The gate runs in every output mode. JSON gains anchoring (coverage, a tally of verified, claimed-unverified, failed and rejected anchors, and gate). Existing anchors carry no proof and now report as claimed but unverified.

  • Packages and bundles carry their Rekor proofs. session close writes each sealed artifact's proofs to anchors/<id>.json, and package verify adds an anchoring row: it passes with the Rekor time span, fails when a stapled proof does not hold or is for another artifact, and warns on a log you have not pinned. A package without proofs has no row, so existing verdicts are unchanged. bundle export includes proofs under anchors, and bundle import merges them with the local store's instead of replacing its anchors.

  • transparency_log trust roots. The Sigstore public-good Rekor key is built in. trust add <label> @rekor.pub --kind transparency_log pins another log, such as a private Rekor, and replaces the built-in key rather than adding to it. This is a new trust kind: a trust_roots.json containing one will not load in 0.31.8 or earlier.

  • The hub no longer stores a checkpoint's caller-supplied rekor_index. It was saved and served back verbatim, so any dock could attach an arbitrary index to its checkpoint. It is null until the hub anchors checkpoints itself.

0.31.8 (2026-09-24)

  • An invalid mandate fails verify by default. An action whose own mandate did not authorize it (out of scope, expired, wrong holder, revoked) verified as outcome: pass, failed: 0, exit 0, with authority_ok: false two fields down; only --require-authority changed the exit code, and even then the JSON said pass. Now the top line is AUTHORITY INVALID, JSON reports outcome: fail with the verdicts counted in failed and named in authority_failed, and the exit code is 1 with no flag. --require-authority remains the stricter mode (no mandate, or an uncheckable layer, also fails). (Retest 0.31.7, finding 30.)
  • A signed lift outlives a restored marker. Restoring a saved halt marker file after halt --lift re-armed the halt: the gate read the marker and never looked for the lift. halt list, the gate's source, now treats a marker as an order only when no signed lift names its halt; a marker outliving its lift is reported lifted_by: <the lift>, not honoured, and removed. (T20.)
  • The payload's own schema must be the kind. attest receipt --kind confirmation signed an evaluation.v1 payload with a verdict outside its vocabulary, because validation dispatched on the flag alone. A payload naming a registered predicate as its schema is now refused under any other kind. --kind list prints the registry, and --help names every registered predicate (a test keeps the two in step). (31, 32.)
  • grant issue --expiry takes a duration (30d, 12h, 45m), and the help examples use one; the fixed example date had fallen into the past and refused when pasted. (33.)
  • verify --full wraps a long authority reason across box lines instead of cutting it mid-word at the frame. (34.)
  • A workspace moved to another path can still sign. The keystore's primary key binds the store's path, so a workspace restored elsewhere (a fresh container, another CI checkout, a backup) could verify but never sign, and an interrupted session there could never be closed. The store now records the canonical path it decrypts under in keys/keystore.origin; opened at a new path, it decrypts under the recorded one and rewraps. For a store with no origin file, TREESHIP_KEYSTORE_ORIGIN=<previous path> once does the same, and the MAC error says so. (N1.)
  • bundle import names both pins for an unpinned signer and says which applies to an agent's own key and which to the ship key, instead of guessing one from the id. (N2.) trust add prints that its store is machine-wide, whatever --config says, and the trust page says how to isolate ships when testing.
  • treeship judge --enforce exits 2 on deny and 3 on ask, for shell gates; without it the exit code says only whether the judge answered, and the judge page says why that is the default.

0.31.7 (2026-09-23)

  • The judgements row reads a yes/no answer both ways. It flagged every judgement "acted at noul 0.000 below its threshold", which is the rules judge answering no to "unsafe?" and the caller proceeding: exactly what the bar asked for. The row now flags an effect that contradicts the side of the threshold the answer fell on (a yes allowed, a no refused) or a judgement with no bar; a confidence-based judgement is checked as before.

  • treeship judge --format json prints reason_facts: the same answers as Zerker Reason premises under the model-judged authority class, one per question, with the signed receipt as the fact id. Reason's side (the class convention, the example program and its tests) landed in zerkerlabs/reason#21.

  • The judge slot, with a deterministic first judge. treeship judge asks a judge typed questions about a tool call (noul, choice, score), holds the answers to a threshold, and with --attest signs each as a judgement.v1 chained onto the session. The built-in judge is treeship-rules: pattern rules over the call (a path outside the workspace, a shell command that destroys or exfiltrates, a host outside the declared network scope, an amount above a bound) answered with probability 1 or 0 and replayable by a verifier, so no model is in the decision path unless the operator points at one. --judge-url sends the same request to any HTTP judge that speaks the contract (a decision model, an LLM judge, a classifier) and holds its answer to the same checks: every question answered in its type, every probability in range, an unavailable judge an error and never an allow. The Claude Code gate opts in with TREESHIP_JUDGE=1 or TREESHIP_JUDGE=<url>, after the card and never instead of it: an answer over the bar denies with a blocked.v1 (policy_threshold_exceeded) naming the judge and the questions, ask asks the operator, and a judge that cannot answer fails open with a note in the timeline. The gate's halt refusals now chain onto the session too.

  • The kill switch reaches the bridges. treeship halt stopped the Claude Code plugin and nothing else; an agent on the MCP bridge or behind the A2A middleware kept working while the halt stood, and the halt page claimed otherwise for strict mode. The MCP client now asks treeship halt list before every callTool and, under a halt this ship signed against its actor (or *), refuses before the server sees the call and throws TreeshipHaltedError. The A2A middleware refuses in admitTask (halted, before the presentation is read) and in onTaskReceived (local tasks too). Every refusal is signed as the same blocked.v1 the plugin mints, chained onto the session with --chain. A check that cannot run fails open and is said once on stderr, as the plugin does; under TREESHIP_STRICT=1 the bridges refuse instead. TREESHIP_A2A_UNVERIFIED=1 never skips a halt.

  • The gate reads the card onboard writes. treeship onboard --tools … minted a signed card carrying the tools and left the local card record, the file the Claude Code gate actually reads, at empty capabilities, so an agent onboarded the documented way had every tool call refused as off-card. onboard now passes the tools to registration and copies the signed card's full tool set (declared, captured from a harness, discovered from an A2A card) onto the record. The gate end-to-end test covers the onboard path.

  • A recipient reaches proven (key-bound). Actor proof consulted only the local agent registry, so on any other machine an agent's action graded asserted however many pins the verifier held. verify now also walks the signed agent_cert.v1 in the store: a certificate that binds the actor to the signing key, signed by a key pinned under cert_issuer, inside its validity window, proves the binding. Without the certificate the grade stays asserted, and the docs say which pins a counterparty needs today: cert_issuer for resolve, plus the agent's agent_cert for import.

  • Rotation is local, and says so. keys rotate mints a successor in this keystore and nothing else: no succession artifact travels, and a pin has no expiry, so counterparties keep trusting the predecessor and cannot verify the successor until they re-pin. The command now prints that, with the export lines to send and the trust remove to ask for; the trust-model and keys pages no longer claim verifiers can walk a rotation chain.

  • A handoff reports the work it names. verify on a handoff prints how many named artifacts are present in this store and lists the ones that are not, with a warning that the handoff envelope verifies on its own and vouches for nothing it does not carry. The handoffs guide shows the real output and the real trust add (a key id, not a label).

  • trust add refuses a label where a key id is matched. Pins for cert_issuer, revoker and agent_cert are matched against the signature's key id in the cert-chain walk verify uses for actor proof; a pin under a label never matched anything and looked pinned. Those kinds now require key_<16 hex> or key_agent_<16 hex>; --label carries the name. session_host keeps a free-form id like the hub kinds -- it is matched by the pinned key's own bytes, not by id, everywhere it is checked (session join, session countersign, and package verify's signer_trust row, which matches cert_issuer and agent_cert pins by key bytes too -- a separate check from the cert-chain walk above, and not id-based).

  • bundle import names the key and the fix. An unverified envelope now says which key signed it, the exact trust add line to run (from the producer's keys export), and which kind an agent-signed versus ship-signed envelope needs. The bundle page describes the real export (signed envelopes, no public key in the file).

  • Every power keys export prints is explained. The ship key exports four pin lines; each now carries a comment saying what it grants, and revoker is called out as the power to revoke cards on the counterparty's machine, handed over deliberately. onboard explains why both the ship pin and the agent pin are needed today, and which survives a rotation.

  • A revocation names its signer. verify-capability said issuer (ship) revoked for any pinned revoker; it now says self-revoked (the card's own key) or pinned revoker key_… (a key this machine pinned under --kind revoker), and the capability-cards page uses the real kind.

  • An ambiguous artifact prefix is reported at any length. art_c naming twelve artifacts now says so and lists candidates (first ten) instead of not found.

  • Git reconcile skips .treeship/ whole. Every file under the project's Treeship directory is written by the CLI or a hook, never by the agent's work; the receipt named declaration.json and card records as the agent's writes. The runtime-artifact list is gone.

  • session mint-challenge prints the outside-verifier pair (present --challenge and verify-presentation --challenge) alongside the room-join commands, and the handshake page mints its nonce with the CLI.

  • treeship agents is visible in --help and the generated command matrix.

  • The MCP bridge page says when its artifacts chain. Intent and receipt artifacts chain onto the session only when the session's actor is the bridge's; otherwise a chain_completeness warning is expected and means what it says.

  • A closed stdout no longer aborts a command. treeship onboard | head -1 died with a panic after registering the agent and before minting its card. Output writes now ignore a closed pipe; every step runs and the exit code is the command's own.

0.31.6 (2026-09-23)

  • Fix: package verify printed the coverage and network_scope rows twice. A stack merge duplicated the block that pushes them; every package verified on 0.31.6 main showed each row two times. One copy now.
  • Retries as a signed chain. A trace that cannot explain a retry is not observability: attempt two of a flaky call used to be a sibling of attempt one, and a verifier could not tell "recovering from a timeout" from "did the same thing twice". attest action --retry-of <id> --attempt N --retry-cause <timeout|error|rate_limited|operator|unknown> --backoff-ms <ms> signs a retry block into the statement (v1 and v2), with the idempotency key sent, so the reason is under the signature and older receipts keep their bytes. package verify gains a retries row that walks every chain: same action and actor, attempts counting up, the same key on both sides, the retried attempt present in the package, and no two attempts that each report a distinct effect; a chain that fails any of those is named. It proves the recorded sequence and reasons; it does not replay the run.
  • judgement.v1: any judge's answer, as a receipt. Fast decision models (TypeSafe's Jev), LLM judges, classifiers and rules engines all sit between an agent and an action now, and none of them leaves a record a stranger can check. judgement.v1 is that record: the model and version, digests of the state and the questions, the one question by key and type (noul, choice, score), the typed answer with its probabilities and confidence, the threshold the caller held it to and who set it, and the outcome (acted, escalated, refused, ignored), chained onto the gated action with --subject. package verify gains a judgements row that counts sealed judgements, names the judges, and warns on any acted on below its own threshold or with no threshold declared. The receipt is the caller's attestation, not a replay: a sampled judge cannot be re-run, and the docs say so.
  • The predicate validator is recursive. required, type, enum, const, numeric minimum/maximum and digest patterns are now enforced at every depth and across array items, not only at the top level. Nested contracts that were documented but never checked before signing (agent_card.v1 provenance grades, boundary.v1 digests, judgement.v1's inner vocabularies) now fail closed. Error paths are dotted (judge.model, answer.noul).
  • No telemetry, stated, and an adoption report that needs none. A proposal to add an anonymous opt-out install ping was closed without merging: Treeship's claim is that nothing phones home, and a ping contradicts the thing it sells. The README now says so in one line. scripts/adoption-report.py answers "how many people use it" from signals the project already owns: unique repository visitors, stars, release asset downloads per tag, npm, PyPI and crates.io downloads, and the hub's public stats (docks attached and active, sessions reported, artifacts), each printed with its basis, and --snapshot appends a dated record so the trend can be read.
  • Fourteen findings from filming the integrations, and the gate that never fired. Real runs of the documented flows on 0.31.5 from npm, written up by the film crew on 2026-09-22.
    • Approval single-use held per config, not per ship (high). The Approval Use Journal followed the resolved config file, so a project stub (extends the global config) got its own journal while sharing the global key, grants and store: one --max-uses 1 grant could be spent once from every such directory on the same machine, each use verifying 1/1. The journal now lives beside the keystore, one per ship; a journal at the old location is moved once. At verify time the use record must name the action being verified, so a journal holding the same grant and nonce for a different action reports that this action's use is not recorded instead of passing it. The replay-levels and journal pages say the real scope.
    • The Claude Code gate never fired in a real install. Two causes. agent register without rule flags rewrote the card with empty capabilities, and the MCP bridge runs exactly that on every start; it now keeps the rules the card has. The SessionStart hook started sessions as ship://…, so no card ever matched; it now starts them as agent://claude-code (TREESHIP_ACTOR overrides), the actor the bridge signs as. The gate's blocked.v1 refusals chain onto the session (attest receipt --chain) so a package with a refusal in it verifies under --strict. A new end-to-end test runs the real CLI through the real hook scripts in CI; the mock-based parity suite passed while none of this held.
    • A receipt about an action, by the session's actor, failed chain_linkage because its parent lived only in the store record, not in the signature. Fixed by the attest receipt chain rule (above); the exact repro is a test.
    • treeship verify accepts a unique id prefix and its hints print full ids. The hint used to print a 16-character id the command then rejected.
    • receipt_body_binding no longer contradicts receipt_binding. It warned "not signed" on every package next to a PASS saying the close record binds receipt.json. It now passes when the close record is present and binds the body, and warns only on packages without one. signer_trust says "this ship's own key" on the producer's machine instead of implying a third-party pin, and the pin command it prints carries --yes so the pasted line runs in a script.
    • The first receipt no longer lists Treeship's own files as the agent's writes. Git reconcile skips .treeship/config.json, config.yaml, the close lock and the keystore, journal, harness, merkle and halt directories; declaration.json and agent cards still surface.
    • A leftover project stub whose extends target is gone used to make init say "already initialized" and session start say "not initialized", each pointing at the other. Config loading now names the stub and the missing target and says what to remove; init and doctor print that.
    • Bash summaries keep the command. The PostToolUse hook drops Claude Code's cd "<cwd>" && prefix before the 120-character cut, which used to leave only the cd on long paths.
    • The MCP bridge's failure line has a reason in it (spawn treeship ENOENT instead of nothing) when the CLI is missing.
    • The Go hub client installs from outside the repo. packages/hub is now module github.com/zerkerlabs/treeship/packages/hub; go get …/pkg/hubclient resolves.
    • Docs: the approvals guide no longer describes the local journal as a v0.10 roadmap item (it ships and is enforced at consume time), nonce and id examples have the real shapes, the trust add --kind cert_issuer workaround callout from 0.20 is gone, and the trust page states which state is machine-wide (trust roots) and which follows the ship (the journal). @treeship/verify says that verifyReceipt does not tie an artifact's digest to its id.
  • The evaluator kit. A guide for an outside evaluator, written as the sequence it runs: what the lab seals and hands over, which key to pin under which power (session_host, nothing more), what package verify --strict must say before a grade is written, an evaluation.v1 grade that names the package by the digest that verified and points at its coverage receipt, and what to publish so a third party reaches the same verdicts with nothing but bytes and two public keys. examples/evaluator-kit/run.sh plays lab, evaluator and public on one machine with separate keystores, checks every promise the guide makes (including that a tampered copy fails and that the grade is not signed by the subject's key), and runs in CI. Guide: Evaluator kit.
  • Fix: attest receipt inside a session was always sealed loose. attest action chains onto the session's head by default when the actor is the session's actor (0.31.4); attest receipt never did, so a receipt minted in the minter's own session (an evaluator's grade in its grading session, a verifier's recompute report) failed chain_completeness under --strict on any other machine. Found by the evaluator kit's first run. attest receipt now follows the same rule: chained by default when --system is the session's actor, sealed loose with a hint when it is not, --parent <id> to chain on purpose, --no-parent to stay off the chain, and outside a session the parent is the subject when it is an artifact. The parent is signed into the statement.
  • The evaluator kit. A guide for an outside evaluator, written as the sequence it runs: what the lab seals and hands over, which key to pin under which power (session_host, nothing more), what package verify --strict must say before a grade is written, an evaluation.v1 grade that names the package by the digest that verified and points at its coverage receipt, and what to publish so a third party reaches the same verdicts with nothing but bytes and two public keys. examples/evaluator-kit/run.sh plays lab, evaluator and public on one machine with separate keystores, checks every promise the guide makes (including that a tampered copy fails and that the grade is not signed by the subject's key), and runs in CI. Guide: Evaluator kit.
  • Fix: attest receipt inside a session was always sealed loose. attest action chains onto the session's head by default when the actor is the session's actor (0.31.4); attest receipt never did, so a receipt minted in the minter's own session (an evaluator's grade in its grading session, a verifier's recompute report) failed chain_completeness under --strict on any other machine. Found by the evaluator kit's first run. attest receipt now follows the same rule: chained by default when --system is the session's actor, sealed loose with a hint when it is not, --parent <id> to chain on purpose, --no-parent to stay off the chain, and outside a session the parent is the subject when it is an artifact. The parent is signed into the statement.
  • Fix: the Claude Code gate blocked every tool call on a CLI without halt. The gate runs treeship halt list first and the script uses set -e, so on a 0.31.5 binary from npm (no halt subcommand yet) the hook died with a nonzero exit and Claude Code treated every tool call as a hook error. Found on a developer machine the moment the new gate script landed next to the published CLI. The halt check now fails open like every other error path in the gate, and a parity fixture runs the gate against a mock that has no halt and expects the call to be allowed.
  • Network scope: a host allow-list an agent is held to. treeship declare --network api.example.com,*.internal.net puts a network scope in the declaration (exact hosts or *.suffix; *.example.com also matches example.com), session start copies it into the manifest, and the sealed receipt's tool_usage now carries network_declared and network_off_scope, the recorded destinations that matched no pattern. package verify gains a network_scope row: PASS with the connection count when every destination was inside the scope, WARN naming the ones outside; no scope declared, no row, and the connections stand in side_effects unjudged. agent register --network signs the same list into the certificate and writes it on the card; attest card --network records it as capabilities.network and verify-capability reports it as declared network. The Claude Code gate refuses a WebFetch to a host outside a card's list with a signed blocked.v1 naming the host, and allows a host inside it; a curl inside Bash is still shell.exec to the gate. Parity fixtures cover both. The same host_in_scope rule is shared by the composer and the verifier.
  • coverage.v1: every session close now seals a coverage receipt. "We monitored this" carried no denominator: a sealed timeline said what the harness recorded and nothing about what it was in a position to see. session close now mints a coverage.v1 receipt chained onto the close artifact and sealed in the package. Declared: each harness state in the workspace with its status, potential coverage level, connection modes and the known gaps frozen at install time, summarised as declared_level (or none). Observed: the count of sealed events by type, hosts, agent instances and skipped log lines. Gaps: plain statements, including "only the session boundaries were captured" when no activity event exists. package verify gains a coverage row that names the receipt, the declared level and the observed count, and warns on packages that carry none; --strict never promotes it. Close prints a coverage: line and JSON adds coverage_artifact_id and coverage_declared_level. A declared level is the harness's potential and the receipt is the producer's own statement about its instrumentation; it does not prove that nothing happened outside it.
  • coverage.v1: every session close now seals a coverage receipt. "We monitored this" carried no denominator: a sealed timeline said what the harness recorded and nothing about what it was in a position to see. session close now mints a coverage.v1 receipt chained onto the close artifact and sealed in the package. Declared: each harness state in the workspace with its status, potential coverage level, connection modes and the known gaps frozen at install time, summarised as declared_level (or none). Observed: the count of sealed events by type, hosts, agent instances and skipped log lines. Gaps: plain statements, including "only the session boundaries were captured" when no activity event exists. package verify gains a coverage row that names the receipt, the declared level and the observed count, and warns on packages that carry none; --strict never promotes it. Close prints a coverage: line and JSON adds coverage_artifact_id and coverage_declared_level. A declared level is the harness's potential and the receipt is the ship's own statement about its instrumentation, signed by the ship key; it does not prove that nothing happened outside it.
  • OpenTelemetry export is in the default build. treeship otel and the auto-export from wrap and session close used to need a from-source rebuild with --features otel, which no npm or install-script user ever did. The feature is now on by default in every published binary; --no-default-features removes it and the commands say so. Nothing is exported until TREESHIP_OTEL_ENDPOINT is set. Export requests now carry a five-second timeout, so a collector that is down or slow costs seconds and never fails a close. The otel page now matches the binary: status and export output as printed, endpoint required with no default, export enabled by default once the endpoint is set, and close exporting the session.close artifact's span rather than "all artifacts". CLI tests send a span to a local one-shot collector and close a session against a dead endpoint.
  • A kill switch: treeship halt. treeship halt agent://<name> --reason "…" (or treeship halt '*') signs a halt.v1 receipt, chains it onto the active session and writes a marker; the Claude Code plugin's PreToolUse gate then refuses every tool call for that actor, before any card rule, and signs each refusal as blocked.v1 with operator_revocation. treeship halt --lift <actor> signs the lift and clears the marker; treeship halt list shows what stands. Both orders are sealed into the package, so a verifier sees when the switch was thrown, by whom, and when it was lifted. A marker with no signed artifact from this workspace is reported as ignored and not honoured. It reaches what the hooks route; a process an agent started outside them is not stopped, and the docs say so.
  • Receipts in pull requests: session close --receipt-dir, the Treeship-Receipt commit trailer, and a verify-receipts GitHub Action. .treeship/ is gitignored almost everywhere, so a sealed session package never reached the pull request it accounted for. session close --receipt-dir <DIR> copies the sealed package (with record.json) into a directory you commit, and close now prints a git trailer, Treeship-Receipt: <session_id> sha256:<digest of receipt.json>, to paste on the commit; JSON output adds receipt_digest, commit_trailer and receipt_copy. The composite action at .github/actions/verify-receipts installs a pinned treeship, runs package verify --strict on every package in the checkout against a trust roots file the repository commits, cross-checks the trailers on the PR's commits against sha256(receipt.json), writes a step summary with one row per package, and fails on any verdict other than verified, on a trailer mismatch, and, with require: true, on a PR with no packages; a PR with no packages otherwise says "0 packages" and warns, never a bare green. This repository runs it on itself (receipts.yml, receipts/trust_roots.json). Guide: Receipts in pull requests.
  • Fix: a package closed by an agent with its own key did not verify on another machine. session close signs the session.v1 close record with the actor's own key when the actor has one, and wrote it into the package after keys.json had already been composed from the chain's signers, so the package named every signer except the one on record.json. On the producing machine the key came from the local keystore and the package verified; on any other machine receipt_binding failed with "a key the package does not carry". Close now adds the record signer's public key to keys.json. Found by the verify-receipts check on the first package this repository committed. Regression test verifies such a package strictly from a second, fresh workspace.
  • The verdict invariant, as one suite. No green verdict is reachable without signature verification against a key the verifier trusts. Three advisories in four months (v0.10.4, v0.19, v0.31.2) shared that root cause and each got a test for its instance; verdict_invariant_cli.rs is the test for the class: eight verdict-printing commands (verify, verify-capability, verify-presentation, verify-profile, package verify, session report, workflow verify, vi verify) against flipped payloads, stripped signatures, unknown keys, forged ids, reordered chains, emptied packages and unpinned roots, each on its own copy of a real workspace, asserting a nonzero exit and no green verdict on either the text or the JSON surface. It runs as its own workflow with a badge. Write-up: docs/security/verdict-invariant.md.
  • Two false greens the suite found, fixed. verify-capability on a card whose envelope had its signatures stripped exited 0 and printed a check mark, because "no valid signature" was folded into the self-asserted grade; it now fails closed for an unsigned card or one no key this machine trusts can verify, and re-derives the id from the signed bytes. vi verify --local --require-attestation checked that the attestation artifact existed, not that it verified, so a flipped byte in the stored record still passed; it now verifies the attestation's signature under a trusted key, re-derives its id, and does the same for every artifact in the walked chain (local_chain_signatures row).
  • The Claude Code plugin gates tool calls on the agent card. A PreToolUse hook reads the session actor's registered card (treeship agent register --tools/--forbidden/--escalation), maps Read/Write/Bash/WebFetch/Agent and mcp__server__tool onto the card's capability names, and denies a forbidden tool with a signed blocked.v1 receipt (scope_violation), asks the operator for an escalation tool, allows a bounded one, and records an off-card call as an agent.note unless TREESHIP_GATE=enforce turns it into a refusal. A refusal is an artifact sealed with the session, not a log line. No card means no policy, and the hook fails open on every error. Parity fixtures cover all four outcomes; the mock treeship now answers session status --format json and the runner seeds cards from fixtures/<plugin>/agents/.
  • evaluation.v1: an evaluator's signed grade. The pacing proposals that pair a capability checkpoint with a certification ("a model with capability X carries certification Y") need the certification to be a record a stranger can check. evaluation.v1 is that record: what was evaluated (a model, agent, session or package, by digest), with which suite (by digest), in which environment, and what the evaluator concluded, with verdict in pass, fail, inconclusive enforced before signing. Attested with --subject on the graded artifact, it chains onto the thing it grades. The docs state the self-asserted rule: a grade signed by the subject's own key is not independent.

0.31.5 (2026-09-17)

  • The receipt's agent graph is written, not only defined. agent.spawned and agent.returned existed in the event vocabulary and the graph builder, and no producer emitted them: a session with subagents sealed with spawned_subagents: 0 and no parent_child edge. session event now takes both types (--agent-name is the child; the parent rides in --meta as spawned_by, the return target as returned_to or --destination), and the timeline prints them as spawned and returned. The Claude Code plugin adds SubagentStart and SubagentStop hooks that emit them, and its PostToolUse hook tags a subagent's tool calls with the subagent's instance name (<agent_type>#<id>) instead of the parent's, so the sealed receipt says which instance did what.
  • Two predicates for inference verification: verification.packet.v1 and verification.recompute.v1. The AI Futures Project's verification plan asks for AI workloads to be cut into discrete packets that a recomputation server can spot-check, and lists the integrity of what that server reports back as unexplored. These predicates are the record layer for that scheme. A prover signs a packet receipt (model, input and output digests, stream position, an optional reproducibility claim); a verifier signs a recompute receipt about it with --subject set to the packet, so the result chains onto the claim it checks and verify walks from one to the other. verdict is match, mismatch or inconclusive and anything else is refused before signing; method is free-form so DiFR, TOPLOC or bit-exact schemes need no registry change. Treeship does not do the recomputation and claims nothing about reproducibility; it makes the reports signed, chained, and verifiable offline by the other side. Guide: Verification reporting.
  • Retest of 0.31.4, the four items left open. attest receipt --subject <external ref> no longer records the subject as a storage parent, and the chain walk stops at a parent that is not an artifact id, so verify last passes on a receipt about ord_12345 (FR-4, the receipt half). session report --format json exits nonzero on a failed local verify on every path, including no hub attached, and nonzero on an upload error; the no-hub message no longer says the receipt verifies locally beside verification_status: fail (FR-7). verify-presentation prints a pin command with a real root name instead of <name>, and when the card's signature is unpinned it names the signing key id and the keys export line to ask the producer for (FR-5). The default --parent inside a session is scoped to the session's actor: another agent's receipt in the same workspace is sealed loose unless it passes --parent on purpose (P3).
  • Release: the npm landing wait asks for the exact version and waits fifteen minutes. wait-for-npm-version.sh compared the latest dist-tag, which npm's CDN updates minutes after the version itself, and gave up after five; on 0.31.4 it split the release across npm twice. It now asks whether <pkg>@<version> is installable, for up to 900 s.

Older releases

Read the complete release history in the repository.