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 revokerecords the withdrawal in the local journal, and a later use of that grant is refused there.verify --strictfails when a handoff names artifacts this store does not hold. The missing ids are in the JSON either way.session start --configwrites 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.
wraphonorsrequire_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
--systemis in the signed statement.--no-parentstill keeps a receipt off the chain.--chainremains and is already the default. - A linked worktree keeps the main checkout's ship.
treeship init --worktreewrites 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_URLis 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_certexample carries--agent(README, onboard, bundle, verify, what-to-expect and the five-minute post),resolvedocumentskey_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_certpins 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-keywrites it, and the pin lines fromkeys export --agent <uri>andonboardcarry it. A--kind agent_certline from an earlier release (no--agent) still lets that key's artifacts verify and import, butresolve,verify-capabilityandverify-presentationnow 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 nameagent registerwrote 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;
resolvepicks 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 fromresolve,verify-capability,verify-presentationand the SDKs carrykey_bound_reason(pin_unscoped,pin_scoped_to_other,not_pinned,key_not_verified,agent_uri_invalid) for scripts. A revocation with reasoncompromisedapplies to every card of that key;trust removeremains the definitive answer to a compromised key. - Receipts no longer carry the machine's hostname.
host_idwashost_<hostname>and appeared on every event and in the hosts table, eleven times in a typical receipt, untouched by path redaction. UnsetTREESHIP_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_IDstill 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 onboardprinted 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 quickstartmixed 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-setupand an idledaemon stopwrote 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-lockfilesjob 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 prefersRAILWAY_GIT_COMMIT_SHAover aHUB_COMMITleft on the service, so a connected-repository deploy never reports a stale commit;release.sh prepareno 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 preparecannot 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, runsrelease.sh refresh-lockfilesand opens the lockfile PR itself.RELEASING.mddocuments the order of operations, whynpm ciat 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 upuploads have no.git, so/v1/versionreportedcommit: unknown.scripts/hub-deploy.shsetsHUB_VERSION,HUB_COMMITandHUB_BUILT_ATon the Railway service (the Dockerfile takes them as build args) and then uploads. (N-34.) - Revoking an asserted capability card takes effect.
revoke-capabilityon a card that is not key-bound printed "revoked" and exited 0, butverify-capabilityandresolvekept 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 aconfig.jsonbeside 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--configwith the real path if you keep.treeshipbehind a link. - Session commands record into the session's own workspace. With
TREESHIP_CONFIGexported 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 whoseconfig.jsonsits beside the activesession.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;--configstill wins, andTREESHIP_CONFIGapplies 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 --summaryis path-redacted. The operator's summary (and--headline,--review) landed in the signed receipt as written, so absolute home paths reachednarrative.summary, the timeline's closing entry andpreview.htmlwhile every other field was already~/.... The text is now rewritten the same way before signing, andcloseprints 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 failedchain_completenessunder--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 signedsession_refnames 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 unpublishasks 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,--yesis 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--helpprints, 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'snot:list disclaims -- README.md's"The Hub stores immutable bytes"passed underhub-storage-write-onceeven though that claim's ownsaysfield negatesimmutable. The check now derives, per claim, which disclaimed words itssaystext actually negates (an affirmed word with its own qualifier, likerekor-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'ssays, likecheckpoint-not-witnessed, isn't itself a negation ofwitnessedfor the citing claim; the marker syntax and link URLs on a scanned line aren't prose and shouldn't count as hits), addedscripts/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 returns409unless the replay is byte-identical, and410for 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, whosedeterminism/merkle_rootchecks 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"). Registeredhub-cannot-forge-signed-verificationwith 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 inhub-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_certvalidate the id shape; a human label goes in--label, not the positionalKEY_ID) -- verified the failure, then the fix, against the built binary; replaced with akey_<16 hex>-shaped placeholder matchingcli/trust.mdx's own existing example. - npm lockfiles point at the published 0.31.11 packages.
scripts/release.sh refresh-lockfilesafter publish, sonpm ciresolves@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) affectedpackages/sdk-ts,packages/verify-js,bridges/mcpandbridges/a2a-- dev-only, but bumpedvitestto^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: incompletetoSSG()path-traversal fix, unboundedparseBody()nesting, and a query-parser/fragment cache-key differential) reachedbridges/mcptransitively through@modelcontextprotocol/sdk; pinned via apackage.jsonoverridesentry (^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) reachedschemastransitively throughajv; same fix, anoverridesentry (^3.1.6) to stay on the 3.x lineajvdeclares 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.mjsfixtures.npm ci(what CI actually runs) works cleanly from every regenerated lockfile;npm installalone 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-depsto 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.htmlandcertificate_template.html's sharedesc()helper now covers"and'as well as&/</>, so a field placed inside a quoted HTML attribute (a capability'stitle, for example) can't end that attribute early. Fields meant to be counts (authority.checked/violations/unverified/bearer, an approval grant'sdelegation_hopsandscope.max_uses) are coerced through the existingnum()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 ownreceipt.jsonagainst 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 bytreeship installinto the shell) used the store an exportedTREESHIP_CONFIGnamed, not the project's.treeship/config.jsonbeside theconfig.yamlit had found, so a session started with--configin 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, thenTREESHIP_CONFIG, then discovery. The project config is opened as a discovered config, so a cloned repository'sconfig.jsongets every discovery check (stores inside its own.treeship, noextendsoutside the home, no linked.treeship) before any key is used. -
The lockfile check also compares the package's own version. A
package-lock.jsonwhose root version lagspackage.json(the OpenClaw plugin's read 0.10.3 at 0.31.10) failsscripts/check-lockfile-sync.py; the plugin's lockfile is refreshed,scripts/lockfile-pin.pystamps those fields at release time, and the plugin is inrelease.sh's lockfile lists. -
Clippy runs on wasm32 in CI (PKG-3).
std::fs::Filehas noDropon that target, so an explicitdrop(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. -
wrapexits 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, sotreeship --format json wrap -- npm test && deploydeployed after failing tests; text mode was right. Every format, and--quiet, now propagates the code (128 + signal for a signal death). -
--format jsonleaves 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 onattest action --v2outside the grant's scope and ontrust add --replace, and parsers failed with "Extra data". -
merkle publishandcheckpoint --publishreport what reached the hub.--format json merkle publishwrote 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 jsoncarries the same underpublish, so a caller can tell the checkpoint was published; text mode is unchanged. Thecheckpointhint names the whole root, not a truncated onemerkle verifywould 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-challengeandsession join(text andcountersign_hintin JSON) name every flag the host needs:--challenge,--challenge-issued-atand--challenge-response, and--pending <file>in place of the participant id when the join was written with--outfor a host on another machine. The host used to learn the missing flags from exit 4. -
match --classandroom create --delegatevalidate 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 canonicaled25519:form. -
agents removeof a card that is not there is an error, not "removed" with exit 0. -
A second
initsays 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 openon 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 --endpointdrops a trailing slash, which used to produce//v1/dock/challengeand a 404. -
init --helpno 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 addprinted one success document per detected harness; it now prints one document listingconfigured,skipped,failed,detectedandtreeship_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 theenvelopefield of the result.session reportwith no hub attached put a report carryingerroron stdout and exited nonzero; it is an error on stderr now, with--no-uploadnamed for the local verdict. The JSON contract test no longer skipsadd. -
checkpoint --publishreports a failed publish. In JSON mode the sealed checkpoint is printed withpublish: {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". -
wrapexit codes match the shell. A child ended by a signal exits128 + signal(it exited 255 from a-1) and the JSON output carriessignal; a command that cannot be started exits127with 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.yamlabove$HOMEis 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 onmainbetween 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 inpackages/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 forcesscripts/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.mddocuments the two 0.31.10 security fixes:package verifyaccepting 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, andREADME.md's security history, now name it.docs/claims.ymlgainspackage-verify-binds-receipt, describing whatreceipt_bindingandsigner_trusteach prove.cli/package.mdx'sreceipt_bindingrow,cli/trust.mdx(--replace, and whichtrust addpin hints still print--yes),cli/session.mdx,concepts/multi-agent-sessions.mdx, andguides/handoffs.mdxare updated to match 0.31.10's actual behavior, verified against source rather than carried over from the pre-fix wording. -
sync-changelog.mjs --checkno longer compares the Unreleased section. It's exactly whateverchangelog.d/fragments happen to be onmainright 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 churnchangelog.d/was built to remove.--checknow 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
*.mdfiles andskills/, 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 scansskills/andintegrations/too. The command and flag checks take extra roots (--warn-root) and scanskills/,integrations/and the root*.md. The https one-liner check is a Python script with an explicit path boundary and a unit test, replacing agit grep \bthat 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.yamlnow serves the speccheck-hub-openapi.pyactually 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 (noschemas:section,treeship.dev/dock/activateinstead of the correctedtreeship.dev/hub/activate, thinner response bodies);docs/content/docs/api/hub-openapi.yaml, the richer and route-checked copy, sat underdocs/content/where fumadocs never serves raw files, so nothing a visitor could reach was ever checked againstpackages/hub/main.go. Merged into one file atdocs/public/api/hub-openapi.yaml(the served location) and repointedscripts/check-hub-openapi.pyat 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 unpublishhad no CLI reference page section --docs/api/receipt-delete.mdxalready linked to/docs/cli/hubfor it, but the page never documented the command. Added a full## treeship hub unpublishsection (usage, options, the permanence callout) and its row in the commands table.docs/claims.yml'shub-storage-write-onceclaim 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 againstdb.TombstoneSessionandInsertSessionWriteOnce'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.
/integrationsbecame 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./slugagainst/, not/integrations/. Fixed at the source --scripts/gen-integrations-index.pygeneratesdocs/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 outsidedocs/content/).docs/scripts/check-internal-links.pyonly 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-codelink and confirming the checker now flags it, then confirming it passes clean again after reverting.docs/scripts/sync-changelog.mjsgained a--checkmode (mirroringsync-feature-matrix.mjs's), wired intodocs-driftinci.yml, so the committedabout/changelog.mdxcan no longer drift fromCHANGELOG.mdsilently. -
CLI docs re-test fixes, area A (v0.31.10 retest). Fixed fictional or stale commands verified against the released binary:
agent-cards.mdx'sagent add --kind(no such flag;manualprovenance has no producer at all),coverage-levels.mdx's v0.9.9agent invite/join --invite(now the realsession invite/session join),ONBOARDING.md'strust-template(real commands areinit --template/template apply, plus the missing 8th template),zk-risc0/README.md'sprove --engine --chain(real path isprove-chain <session_id>),AGENTS.md's nonexistentweb/directory andhub.treeship.devhosting claim,threat-model.md'spackage import(real:bundle import) andcommand-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 --resolvewith no--by,session countersignwith neither a participant id nor--pending).zk.mdxandzero-knowledge.mdxhad stale callouts about bugs since fixed (zk-setup's hint text now prints the real command;verify-proofis 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'ss.hub.status()result shape ({connected, endpoint?, hubId?}, not{attached});sdk/overview.mdxnow says four modules (attest/verify/hub/session, matching the realShipclient) and listssession_eventfor Python;python.mdxgainedsession_event()/SessionEventResultandTreeship.ensure_cli()/BootstrapResultsections that didn't exist.mcp.mdx: the unqualified "Treeship never breaks your MCP workflow" is now qualified by the real halt/TREESHIP_STRICTbehavior, added a Server section documenting the real 9treeship-mcptools (was undocumented entirely), removed the fictionalTREESHIP_DEBUG(real forbridges/a2a, notbridges/mcp) and added the realTREESHIP_STRICT.bridges/mcp/README.md: "5 tools" -> 9 (named), "pure WASM" verify claim fixed (the CLI's native verifier isn't WASM;@treeship/verifyis), "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 hardcodedv0.9.4(skill metadata + prose) replaced with an independent skill version, matching the convention other skill files already use.cli/overview.mdxandguides/workflows.mdx: the fictionalTREESHIP_LASTenv var replaced with the real.lastfile /lastkeyword mechanism, andwrap's real parent-resolution priority (--parent>TREESHIP_PARENT>.last);workflows.mdx's approval-chaining example also had--parentdoing duty for approval-nonce consumption, which isn't what it does -- rewritten to useattest action --approval-nonce, matchingwrap'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 testsx2,167KB gzipped,25+ commands,4 vectors); fixeddock.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 theTREESHIP_ACTOR/TREESHIP_PARENT/TREESHIP_TOKENenv-var-fallback bullet (onlyTREESHIP_PARENTis real for the CLI;TREESHIP_ACTORis MCP-bridge-only;TREESHIP_TOKENreads 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.mdx3) said thetreeship-clicrate was "orphaned at v0.4.0" -- crates.io shows all 7 versions yanked now, a stronger and different failure mode (cargo installfails outright, not just installs something stale).hermes.mdx/integrations/hermes/README.md's smoke-test download pinnedv0.15.0, a dead tag; switched to GitHub'sreleases/latest/download/alias so it can't go stale again.packages/rig/README.md's "treeship-core 0.23.0" corrected to match its ownCargo.tomlpin (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.mdsoftened 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 fictionalsession.v1media type, explaining that the close record actually uses thereceipt.v1payloadType with an internalkind: "session.v1"field.packages/verify-js/README.md:verifyCertificate/crossVerifyexamples and prose omitted the requiredtrustRootsparameter 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/serverpaths, 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 viaagent registernot the spec'sattest card),transparency-log(treeship audit, including--watch), andwork-history(treeship history/treeship profile) all have real commands whose--helptext 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-provenancegenuinely remains unimplemented, left as-is.AGENTS.md: "ZK TLS... feature-flagged" fixed (there's no feature flag;zk-tls-setupjust 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 thetamper-evident, not tamper-proofqualifier (<!-- 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 missingtransparency_log; the zk-circom-native entry claimed anative.rsscaffold that doesn't exist (only commented-out Cargo.toml lines do). -
capability-map.jsonnow names hidden commands (row 110).gen-capability-map.pyparses the top-levelCommandenum inpackages/cli/src/main.rsfor#[command(hide = true)]attributes, converts each variant to its clap kebab-case name (honoring an explicitname = "..."override), and probes every one against the built binary (<cmd> --help,probed: true/false) to add a newcli_commands_hiddenarray (22 entries, all probing clean) alongside the existingcli_commandslist. A newHIDDEN_CLI_COMMANDSlist 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 ishub_push, notts.hub.push; the real TS hub-status shape is{connected, endpoint?, hubId?}; the MCP bridge's export isClient, notTreeshipMCPClient, with the real constructor signature andTREESHIP_ACTORenv var (not a constructor option);merkle publishonly 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_pushnotdock_push,ship()'s four modules notShip.init()/createCheckpoint()/createBundle());treeship-0-9-10-approval-authority.mdx'sjournal list/inspectgets a note pointing at the realapprovalsurface.docs/next.config.mjs: removed the/integrations->/integrations/claude-coderedirect now that/integrationshas its ownindex.mdx-- the redirect was making the real index page unreachable even thoughmeta.jsonlists 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-prooffail 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'sinstallfield) named a template that does not exist -- fixed to plaintreeship init.SKILL.md'streeship session start --skill lobster-cashused a flagsession startdoesn'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 regenerateddocs/content/docs/integrations/install.mdxanddocs/public/.well-known/treeship-agents.jsonfrom it, plusintegrations/openclaw-plugin/README.mddirectly. -
treeship onboardexamples that omitted a capability source.onboardrefuses without one of--from-harness,--tools-json,--from-a2aor--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, andspecs/per-actor-signing.md); the latter'sagent register --actoralso doesn't exist (the flag is--name) -- fixed both in the same file. -
docs.treeship.dev serves security.txt (RFC 9116).
/.well-known/security.txt404'd. Added as a route handler (app/.well-known/security.txt/route.ts, same pattern asapp/llms.txt) rather than apublic/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, withCanonicalpointing 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'sTreeshipA2AMiddleware(onTaskReceived,onTaskCompleted,onHandoff,decorateArtifact,mintTaskChallenge,admitTask) is a real, shipped class the resolver didn't know about (sametreeship.<method>(variable convention as theShipSDK, a different package); the resolver now recognizes it.check-cli-commands.py/check-cli-flags.py: the 7--warn-rootfindings were one pattern --ONBOARDING.mdandPLATFORM.mdeach carry a "full command surface" cheat-sheet in a bash-tagged fence (real commands,a|bshorthand for two subcommands on one line,attest approvalshown 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-rootflag (they previously supported only one hard root plus repeatable--warn-root) andskills/,integrations/and the root*.mdfiles moved from--warn-rootto--hard-rootinci.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 namedanchored, 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.mdandDESIGN.mdare now fully clean under the finer rule (down to 0 findings across all five), backed mostly by existingclaims.ymlentries plus two new ones for real, previously-unregistered properties:hub-storage-write-once(the Hub's own database is write-once perartifact_id/session_id--ON CONFLICT DO NOTHINGand a 409 on a second receipt upload -- distinct fromreceipt-tamper-evidence, which holds even with no Hub involved) andtransparency-log-client-reverified(audit/history/resolve --hubre-verify Merkle inclusion offline against the reader's own trust roots andauditwitnesses checkpoints across runs to catch a forking or regressing hub -- distinct fromrekor-artifact-anchoring, an external third-party log, and fromcheckpoint-not-witnessed, about whether a checkpoint is externally corroborated at all).check-claims.pyitself stays warn-only for now: 46 findings remain, all indocs/content/docs/**anddocs/content/blog/**, in progress as a follow-up. -
check-claims.py --strictis now the failing gate. The 46 findings left after the wave-1 false-positive fix (see the earlier changelog entry) were spread acrossdocs/content/docs/**anddocs/content/blog/**. Most (37 of 46) were real, backed claims that just needed a{/* claims:<id> */}marker on the exact line -- mostlytransparency-log-client-reverified(the Hub transparency-log surface:audit,history,resolve --hub, the/v1/agents/*and/v1/merkle/*APIs) andrekor-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.mdxandconcepts/session-receipts.mdxboth called receipts or packagesimmutable, the exact overclaimreceipt-tamper-evidenceexists to correct -- both now say "tamper-evident."concepts/trust-fabric.mdxusedwitnessedgenerically for file-change attribution (nothing to do with Rekor or checkpoints) -- reworded to "captured."concepts/agent-handshake.mdx'sair-gappedverification was generic offline-capability prose -- reworded to "fully offline verification." Three blog posts had the same genericair-gappedusage, reworded to "no network connection"/"offline or network-isolated."cli/audit.mdxhad 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 ismachine-bound, so a copied key file does not decrypt elsewhere -- directly contradicting the registeredkeystore-at-restclaim (nopassphrase; 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-signedDELETE /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 answers410 Gonewith 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/statssession counts. -
Session receipts carry no absolute home paths. Files under
$HOMEare 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$HOMEis redacted along with the literal one.session close --summaryis 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 withCache-Control: immutable;HEADis answered on everyGETroute instead of 405;DELETEandHEADare in the allowed methods. -
/v1/versionreports a real version on Railway. The Dockerfile's build args default fromRAILWAY_GIT_COMMIT_SHA, and an unset version falls back to the hub's ownReleaseconstant, which the release script stamps, instead ofdev. -
The shell hook ignores stale state. The installed zsh, bash and fish hooks now pass the finished command to
hook post; state left by ahook prefor 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, sogit commit -m x && echo okis recorded once and an empty Enter records nothing; a line history did not record is taken fromBASH_COMMAND, so nothing is ever signed for a command that did not run. The snippets are quiet undersetopt nounset, the hooks run with--quietso a matched command prints nothing into the prompt, and hook diagnostics go to stderr.treeship uninstallgives the file back byte for byte. A hook from an earlier release passes no command; there, state older than 24 hours is dropped.treeship installreplaces 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-codeinstalls the Claude Code plugin. It used to write~/.claude/mcp.jsonand report Claude Code as configured, but Claude Code never reads that file (its MCP servers live in~/.claude.jsonand a project's.mcp.json, its hooks in~/.claude/settings.json). The plugin is the real harness, hooks plus the MCP server, soaddnow runsclaude plugin marketplace add zerkerlabs/treeshipandclaude plugin install treeship@treeshipthrough theclaudeCLI 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 stalemcpServers.treeshipentry 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.v1record, name the sealedsession.close, and be signed by its signer or by the record key it names;session closenow names an agent's own record key insession.close, so pinning the ship key is enough. Behaviour change: a package closed before this release by an agent with its own key failsreceipt_bindingby default; read it with--structural.verifiedandsignatures-passnow require checks that passed (an artifact signature,receipt_binding,signer_trust), andstructural-passrequires 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 newapproval_signerrow; 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 failskey_id_collision.treeship verify <package>,treeship dashboardandsession reportreport the same verdict aspackage verify, andverify <package>honours--config. -
trust addprotects pinned key ids. Pinning a different public key under an already-pinned key id needs--replaceand 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/grantsrather than in the sealed set, is verified against its signature and its signed use limit; two approvals that share one nonce fail.package verify --structuraljudges trust with this ship's own keys too, andtrust addchecks this ship's own key ids against the workspace--confignames. -
--format jsonalways emits JSON.doctor,package inspect,pending,declare --show,otel status,templatesandversionwrote 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 orfound: 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,checkpointandmerkle proofare the follow-up (their file is changing in another fix). (0.31.9 full test, CLI-10.) -
doctorno longer reports auto-push as enabled. Theauto_pushsetting 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,checkpointandmerkle proofemit JSON in--format json.merkle statuswrote 0 bytes;checkpointandmerkle proofwrote 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 wholesha256: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 statusno longer kills the daemon, anddaemon startbackgrounds. The pid file holds<pid> <start epoch>, but the reader parsed the whole line as one number, so every read failed:statusreported "stopped", deleted the file, and the daemon (which exits when its pid file disappears) died of a status check;stopthen 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 startwithout--foregroundnow 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 rotatemoves the default everywhere. The keystore promoted the successor buthub attach,proveand the dashboard read the default fromconfig.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 writesconfig.jsonon 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::savefollows a project stub to the config it extends and writes atomically.hub attachrecordscreated_atas an RFC 3339 timestamp instead of"<epoch>Z". (0.31.9 full test, CLI-9.) -
An honest approval or room package verifies under
--stricton a second ship.--strictfailed 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, andchain_completenesstreated 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 signedapprovalNonce, 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 alocal_journalrow says it is not applicable and what was checked instead, and--strictdoes 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 verifychecked it like any other artifact and failed every honest room (treeship verifypassed 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 whatsigner_trustjudges. An invitation redeemed by more participants than itsmax_usesfails 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 countersignrequired the invitation's nonce to have been consumed in the host's own journal bysession 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. Nowsession join --out <file>writes the pending envelope for the host, andsession 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--challengeand--challenge-responseare 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'smax_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;--pendingalso 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 withrecord.jsondeleted look the same. The verifier treated both as legacy and downgraded thereceipt_bindingcheck 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 failsreceipt_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),--structuralreports what can still be checked, with verdictstructural-pass, neververified. Packages from before 0.31.2 are unchanged. Audit 2026-09-25, CLI-4; the cases are T2 vectors. -
merkle verifysays "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 ashub_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 beforetreeship trust add <key_id> ed25519:<key> --kind hub_checkpoint(no--yes: the key comes from the checkpoint itself).--format jsongives{"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 publishpoints at the attached hub's proof, not treeship.dev. After publishing to a self-hosted hub, the hint still saidtreeship.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 forapi.treeship.dev. Audit 2026-09-25, CLI-12 (the merkle half of W1-9). -
Every failure exits nonzero.
prove,prove-chainandverify-proofon a build without--features zkprinted a warning and exited 0, sotreeship verify-proof /nope.zkproofpassed a script's exit-code check. They now exit 5, a new code for "not compiled into this build", andotelon a build without the feature does the same.otel exportto an unreachable collector exited 0 after printing "✗ export failed";approval statusandapproval useson a grant nobody has heard of reported "unbounded, within max_uses" and exited 0;resolveon 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--helpand 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 zkhad 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 zkare gone (zk_statusduplicatedzk-setup;enqueue_proof_jobwas an unused wrapper), and a newfeature-matrixCI job checks--no-default-features,--features zk,--features zk-bonsaiand 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>andverifyReceipt()in@treeship/verifyboth 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 intests/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 pushorsession reportto a self-hosted hub, the CLI printedhttps://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_URLnames the site that renders pages for its data;TREESHIP_HUB_PUBLIC_URLnames 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>), andtreeship 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 reportprints no preview or package page links for a hub that has no pages;hub pushno longer suggeststreeship 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,
SHA256SUMSand a newtreeship-sbom.spdx.json(an SPDX SBOM of the Rust workspace'sCargo.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 withgh attestation verify treeship-<platform> --repo zerkerlabs/treeship. -
@treeship/verifyverifies 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'sartifact_idand fulldigest, and checks the signed close record against the exact bytes ofreceipt.json. It returns the CLI's verdicts:verifiedonly under keys you pin,signatures-passwhen a signer is known only from the package'skeys.json,structural-passforreceipt.jsonalone (scope: 'receipt-only') or for packages holding approvals (sealed, or carried as use records and grants underapprovals/), endorsements or rooms, whose rules it leaves totreeship package verify(scope: 'partial'), andfailedotherwise. The T2 vectors gainverify_jsandverify_js_pinnedcolumns: every tampered package isfailedor, for the two room vectors, capped atstructural-pass; none readssignatures-passorverified.verifyReceiptis unchanged and still structural only. -
Security headers added to docs.treeship.dev (W2-7).
docs/next.config.mjsnow sendsContent-Security-Policy,X-Frame-Options: DENY, andStrict-Transport-Security: max-age=63072000; includeSubDomains, matching the header settreeship.devPR #121 shipped forwww. 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 bydefault-src 'self'), and Google Analytics (googletagmanager.com/google-analytics.com) inscript-src/img-src/connect-src. No WASM allowance -- that's only needed on the main site's/verifypage. Verified with a production build, a localnext 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 withX-Forwarded-Proto) carriesStrict-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 -->), androom-sessions.mdx's checkpoint-root/OpenTimestamps design is now marked as a design sketch, not built (<!-- claims:checkpoint-root-anchoring -->).zero-knowledge.mdxcontradicted its own quarantine callout by saying the browser verifies Groth16 proofs "today";core-wasm/README.mdandzk-circom/README.mdmade the same overclaim and the latter referenced a nonexistentsetup/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 inthreat-model.md, and its staletrust add --kind merlke-checkpointexample / "no default trust root" claim (the built-in Sigstoretransparency_logkey is a default root).New:
docs/claims.ymlregisters every claim above with a status (shipped/next/designed/retired) and a proof path, andscripts/check-claims.py(wired intoci.yml'sdocs-driftjob) fails if a banned phrase (passphrase, machine-bound, tamper-proof, immutable, anchored, witnessed, air-gapped) appears in the filesclaims.ymlscans without a<!-- claims:<id> -->/{/* claims:<id> */}marker naming a real entry, or if a claim'sproofpath 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 genuinelyimmutabledeclared graph).PLATFORM.mdis 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.mdand 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>, aTreeship/Ship.init()class API,dock_push,expires_in, aPOST /v1/attestendpoint, a websocket stream, andTREESHIP_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'sship()factory (packages/sdk-ts), the Python SDK's scopedattest_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_pushcalls and itsconfig.toml.template's reference to a nonexistenttreeship integrations installcommand. The oversized Kimireferences/{hub_api,mcp_bridge,typescript_sdk}.mdfiles, 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 generatesinstall.mdxand the.well-known/treeship-agents.jsonmirror) marked the plugin🟢 shippedwithopenclaw plugins install @treeship/openclaw-pluginas the install command; that package was never published (npm viewreturnsE404) and the command fails. Downgraded to🟡 candidateand 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.mdleads with that path now. Also fixed the neighboring Kimi Code plugin row, which advertised atreeship integrations install kimi-codecommand 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.mdxsaid Linux arm64 had no binary until v0.25 -- it's shipped in every release since v0.25.1, and bothcurl | shandnpm install -g treeshipinstall it directly today, no cargo fallback.core-wasm/README.mdnamed a nonexistenttreeship-core-wasmnpm package and showed a signature and aninit()call that don't exist (correct:@treeship/core-wasm,verify_envelope(envelopeJson, trustedKeysJson)returning a JSON string, no init, twowasm-packbuilds not one).sdk/verify.mdxandsdk/typescript.mdxcarried a "core-wasm doesn't load under Node 22" warning that was fixed in v0.25.0 -- verified@treeship/verify@0.31.9loads and runs on Node 22.23.1 before rewriting it.sdk/python.mdxhad two contradicting warnings sayingattest_approval()andwrap()are "currently broken against CLI 0.24"; both work today (scoped approvals,--format jsonwrap) and the page already described the working behavior further down the same page.cli/setup.mdxsaidsetup --format jsonreturns{}; it returns a real structured payload (SetupResult) today. Also re-verified and dropped stalev0.9.x/v0.24version anchors insdk-ts/README.md,sdk-python/README.md, and fixed a fictionalShip.attestAction()mention inintegrations/agent-skills.mdx. -
/v1/verifydocumented as retired; artifact-id formula corrected (W3-5, DOC-3/DOC-7).AGENTS.md,PLATFORM.mdandONBOARDING.mddescribedGET /v1/verify/:idas 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 returns410with 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.mdxhad 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, anddeclareare implemented, tested, and shipped (severalstableindocs/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 thezkfeature --prove/prove-chain/verify-proofexit 5 ("not compiled into this build") without it, whilezk-setup/zk-tls-setuparen't feature-gated and run in every build. Flag tables were generated by hand-invokingtreeship <cmd> --helpon a local build and checked against the real CLI withdocs/scripts/check-cli-option-tables.py(179 rows across 35 commands, 0 mismatches) rather than typed from memory. Linked the new pages from the relevantfeature-inventory.ymlentries'docs:lists and added all five tocli/meta.json's sidebar. -
Unreleased judge flags labeled; hidden-command status made explicit (W3-6, DOC-5/DOC-9).
cli/judge.mdxdocumented--contract,--state-out,--resolve/--by/--decision/--route/--reasonas shipped; they're implemented onmainbut not in the latest tagged CLI (0.31.9 rejects them). Added "(next release)" markers to each option-table row and aCalloutat each section introducing them, pointing readers attreeship --versionvs. the CHANGELOG rather than assuming the docs match their installed binary. The generatedcommand-matrix.mdx(built from whatever this repo compiles, which can be ahead of the last tag) now says so in its own header, fromscripts/gen-cli-reference.py's template. Separately,docs/feature-inventory.ymlmarkedcheckpoint,merkle *,harness *,approval *,bundle *,agent registeranddeclarestablewith no indication they'rehide = trueinmain.rs(not intreeship --help) -- added ahidden: truefield (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 missingrekor-artifact-anchoringinventory entry (shipped 0.31.9) and roadmap entries forcheckpoint-root-anchoring,rfc3161-timestampsandopentimestamps. 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.mdxhad 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 outsidedocs/content/(verdict-invariant.md,TS-2026-002.md) now link to their GitHub blob URLs. The#the-gateanchor 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, whichcli/halt.mdxandreference/predicates.mdxalready used correctly.blog/multi-agent-skills.mdxlinked to a stale SDK anchor computed from an oldattest_approvalsignature; recomputed against the current one.integrations/meta.jsonwas missingbuzz,grok-botandrigfrom its sidebarpageslist even though all three.mdxfiles 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.5was signed and printed as 750%;attest endorsement --kind bogus,attest action --input-digest abcand--actor "",session event --meta '{bad'(silently dropped, whereattestrefused it),declare --valid-until notatime,history --class bogusand--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 issha256:plus 64 hex characters; an actor is ascheme://nameURI;--metais a JSON object; timestamps are RFC 3339 and--sincealso takes a duration back from now (30d,12h);--artifactsmust name artifacts in the store;--formatis text or json. The contract test pins each. (0.31.9 full test, CLI-14.) -
Output no longer contradicts itself.
verify-capabilityandresolveopened with a green "✓" headline for a revoked card and said REVOKED two lines down; the headline is now the verdict.vi checkprintedPASSfor every rule and thenFAILfor the same mandate; rules read as passed only when the mandate is satisfied.attest action --v2signed an action outside its grant's scope without a word; it now warns at signing time (the receipt still records the violation, andverifystill reports it).grant revokeon a revoked grant andjudge --resolveon a resolved judgement minted a second receipt as if nothing had happened; both now refuse and name the standing one.treeship uioff a terminal saidDevice not configured (os error 6); it now says it needs a terminal.dashboard --host 0.0.0.0said "local-only"; the mode line follows the bind address.approvesaid "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 addandkeys listshowed different fingerprints for one key (one hashed the key's text, the other its bytes); both now hash the bytes.verify-capabilityon 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;/healthjoins/healthz. The public hub had no version endpoint, so nobody could tell from outside whether a fix was deployed.GET /v1/merkle/checkpoint/latestaddsage_seconds, computed at read time, for a monitor to alert on. On the CLI,treeship checkpoint --publishseals and pushes in one command and exits nonzero if the push fails; an opt-incheckpoint.everyin config.yaml makes the daemon seal and publish on that interval, logging each publish, anddoctorreports 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 addfollows 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 projectTREESHIP.mdin 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>.mdinstead of editing the## Unreleasedsection ofCHANGELOG.md, so parallel PRs no longer conflict on every merge.scripts/release.sh preparefolds the fragments into a new## <version> (<date>)section (scripts/changelog.py assemble). The docs changelog page shows pending fragments as its Unreleased section. Thedocs-driftCI job validates fragments and fails a PR that adds bullets to## Unreleaseddirectly. -
Which workspace a command uses is never a guess. A project's
.treeship/config.jsonstub records the account's global config by absolute path, so under a differentHOME(a test harness, a container) with no--configorTREESHIP_CONFIG, discovery still routed to the original account's keystore and signed into its store. A discovered stub that extends a config outside the currentHOMEis now refused with the two ways to say which workspace you mean; an explicit--configorTREESHIP_CONFIGalways wins over discovery (a test pins it); and discovery never climbs aboveHOME.statusanddoctoralready 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 andintegrations/claude-code/README.md's Method 2 both instructed a manual MCP config at~/.claude/mcp.json-- the same stale path CLI-16 (#494) fixedtreeship add claude-codefrom writing to. Claude Code actually reads MCP servers from a project's.mcp.jsonor~/.claude.json's top-levelmcpServerskey; both docs now say so and point the copy-paste template at.mcp.jsonin the project root. -
Two-machine room-join docs (W1-3b follow-up).
session join --out,session mint-challenge,session answer-challenge, andsession 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.mdxgained full reference sections formint-challengeandanswer-challenge, and updatedjoin/countersignentries covering--out,--pending, the challenge flags, the 10-minuteTREESHIP_JOIN_CHALLENGE_WINDOW_SECSwindow, single-use enforcement, and the closed-session refusal.concepts/multi-agent-sessions.mdxgained a "Joining from another machine" walkthrough with the full flow and real CLI output, verified end to end against a locally built binary (two isolatedHOMEs 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 joinno longer suggests trusting the host's key blind. When the invitation's issuer is not pinned undersession_host, the hint now says to confirm the key with the host out of band first and offerstreeship trust add … --kind session_hostwithout--yes, the same rulemerkle verifyfollows 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-outand--resolveas "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--configat 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--configorTREESHIP_CONFIG. -
A flaky core test no longer fails CI at second boundaries.
bundle::tests::anchor_proofs_travel_and_merge_on_importbuilt 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'sreleasejob used onlygenerate_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 fromCHANGELOG.md(guaranteed present by tag time, sincescripts/release.sh preparerunschangelog.py assemblebeforetagever runs) and passes it as the release body;generate_release_notesstays 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.whland.tar.gzwere committed to the repo even though.gitignorealready excludespackages/sdk-python/dist/-- ignore rules don't untrack a file already committed. Removed both; nothing else under adist/path is tracked. -
Every advertised install one-liner is https:// (W0-3, SUP-2).
curl -fsSL treeship.dev/setup | shhad 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) tocurl -fsSL https://www.treeship.dev/setup | sh(or/install). Also updated every description of what the one-liner does: it now asks before runningtreeship add --all, and skips that step (not just the prompt) with no interactive terminal unlessTREESHIP_SETUP_YES=1is set. Added a CI grep (docs-driftjob) so a baretreeship.dev/setup//installcurl 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
subjectis what it endorses; storage chained it onto the most recent artifact, but the statement signed no parent, soverifyfell back to the subject and reportedSIGNED LINKAGE BROKEN — possible tamperingfor the endorsement and everything chained after it, sessions included. Endorsements now signparentId, 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 achain linkage partly unsignedwarning naming them (chain_linkage_unsignedinverify --format json, achain_linkageWARN inpackage verify), andpackage verify --strictfails them. A presentparentIdis 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 requirespackage verifyto 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/verifyruns (T2). A tampered package is neververifiedorsignatures-pass, in any mode. Flows and columns still wrong on main (endorsing a non-latest artifact, room countersigns in packages,merkle verifyon 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-pushorauto_push: truepush 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, andnpm/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-unwitnessedand--require-authorityexited 1 when they refused, but--format jsonstill readoutcome: "pass", failed: 0; a script reading onlyoutcomegot a green. Now a failed policy makesoutcomefailand is named in the newfailed_gateslist. The same split finding 30 closed for mandates, found again by the 0.31.9 QA run. -
package verifyno longer fails a package for carrying non-action artifacts. Theapproval-use-action-bindingrow checked every envelope underartifacts/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|routesigns ajudgement.resolution.v1receipt under the decider's URI, with the judgement as its subject, chained onto the session, recording what itoverrideswhen it contradicts the judge.package verify'sjudgementsrow 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.v1now carriesresponse_digest, the hash of the raw response bytes as received, andjudge.request_id, the judge's own id for the answer when it gives one (the Jev adapter passesx-typesafe-request-idthrough); 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-outwrites the canonical state the receipt'sstate_digestcommits to, for a verifier who re-runs the rules judge or checks what an outside judge was shown. -
TREESHIP_JUDGE_STRICT=1makes 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 whytreeship judgeitself never does. -
judgement.v1carries the decision contract.treeship judge --contract ticket-router@3recordscontract: {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/jevputs TypeSafe's Jev behind the judge contract: a dependency-free adapter that speaks the contract on one side andPOST /v1/systemoneon the other, pins the model version so the receipt names one, and marks every answerreplayable: 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, thejudgementsrow 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 unsignedkey_idis not consulted.halt listsays which reason applies (lifted by art_…, or not signed here) instead of "no signed artifact" for both. (Retest 0.31.8, T20.) -
verify --fullfails whatverifyfails. 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 anauthorityrow, theAUTHORITY INVALIDbanner follows it,--require-authorityis honoured, and every failure exits 1 in every mode. (Retest 0.31.8.) -
attest receipt --kind liststands 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 thehashedrekordtype cannot verify plain Ed25519 anyway. The failure was swallowed and the CLI printedrekor: pending. The hub now submits adsseentry, which Sigstore's staging log accepts, and returnsrekor.status(anchored,failed,skipped) with a reason and the full log entry. Re-pushing an unanchored artifact retries it. Separately,verify --max-unwitnessedtook witness times fromRecord.anchors[].observed_atand claimed times fromRecord.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 jsonand--fullignored 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'sintegratedTime. Claimed times come from each statement's signedtimestamp. The gate runs in every output mode. JSON gainsanchoring(coverage, atallyof verified, claimed-unverified, failed and rejected anchors, andgate). Existing anchors carry no proof and now report as claimed but unverified. -
Packages and bundles carry their Rekor proofs.
session closewrites each sealed artifact's proofs toanchors/<id>.json, andpackage verifyadds ananchoringrow: 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 exportincludes proofs underanchors, andbundle importmerges them with the local store's instead of replacing its anchors. -
transparency_logtrust roots. The Sigstore public-good Rekor key is built in.trust add <label> @rekor.pub --kind transparency_logpins another log, such as a private Rekor, and replaces the built-in key rather than adding to it. This is a new trust kind: atrust_roots.jsoncontaining 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
verifyby default. An action whose own mandate did not authorize it (out of scope, expired, wrong holder, revoked) verified asoutcome: pass, failed: 0, exit 0, withauthority_ok: falsetwo fields down; only--require-authoritychanged the exit code, and even then the JSON said pass. Now the top line isAUTHORITY INVALID, JSON reportsoutcome: failwith the verdicts counted infailedand named inauthority_failed, and the exit code is 1 with no flag.--require-authorityremains 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 --liftre-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 reportedlifted_by: <the lift>, not honoured, and removed. (T20.) - The payload's own
schemamust be the kind.attest receipt --kind confirmationsigned anevaluation.v1payload with a verdict outside its vocabulary, because validation dispatched on the flag alone. A payload naming a registered predicate as itsschemais now refused under any other kind.--kind listprints the registry, and--helpnames every registered predicate (a test keeps the two in step). (31, 32.) grant issue --expirytakes 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 --fullwraps 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 importnames 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 addprints that its store is machine-wide, whatever--configsays, and the trust page says how to isolate ships when testing.treeship judge --enforceexits 2 ondenyand 3 onask, 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
judgementsrow 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 jsonprintsreason_facts: the same answers as Zerker Reason premises under themodel-judgedauthority 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 judgeasks a judge typed questions about a tool call (noul,choice,score), holds the answers to a threshold, and with--attestsigns each as ajudgement.v1chained onto the session. The built-in judge istreeship-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-urlsends 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 withTREESHIP_JUDGE=1orTREESHIP_JUDGE=<url>, after the card and never instead of it: an answer over the bar denies with ablocked.v1(policy_threshold_exceeded) naming the judge and the questions,askasks 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 haltstopped 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 askstreeship halt listbefore everycallTooland, under a halt this ship signed against its actor (or*), refuses before the server sees the call and throwsTreeshipHaltedError. The A2A middleware refuses inadmitTask(halted, before the presentation is read) and inonTaskReceived(local tasks too). Every refusal is signed as the sameblocked.v1the 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; underTREESHIP_STRICT=1the bridges refuse instead.TREESHIP_A2A_UNVERIFIED=1never skips a halt. -
The gate reads the card
onboardwrites.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.onboardnow 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 gradedassertedhowever many pins the verifier held.verifynow also walks the signedagent_cert.v1in the store: a certificate that binds the actor to the signing key, signed by a key pinned undercert_issuer, inside its validity window, proves the binding. Without the certificate the grade staysasserted, and the docs say which pins a counterparty needs today:cert_issuerfor resolve, plus the agent'sagent_certfor import. -
Rotation is local, and says so.
keys rotatemints 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 thetrust removeto ask for; the trust-model and keys pages no longer claim verifiers can walk a rotation chain. -
A handoff reports the work it names.
verifyon 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 realtrust add(a key id, not a label). -
trust addrefuses a label where a key id is matched. Pins forcert_issuer,revokerandagent_certare matched against the signature's key id in the cert-chain walkverifyuses for actor proof; a pin under a label never matched anything and looked pinned. Those kinds now requirekey_<16 hex>orkey_agent_<16 hex>;--labelcarries the name.session_hostkeeps 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, andpackage verify'ssigner_trustrow, which matchescert_issuerandagent_certpins by key bytes too -- a separate check from the cert-chain walk above, and not id-based). -
bundle importnames the key and the fix. An unverified envelope now says which key signed it, the exacttrust addline to run (from the producer'skeys 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 exportprints is explained. The ship key exports four pin lines; each now carries a comment saying what it grants, andrevokeris called out as the power to revoke cards on the counterparty's machine, handed over deliberately.onboardexplains why both the ship pin and the agent pin are needed today, and which survives a rotation. -
A revocation names its signer.
verify-capabilitysaidissuer (ship) revokedfor any pinned revoker; it now saysself-revoked (the card's own key)orpinned 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_cnaming twelve artifacts now says so and lists candidates (first ten) instead ofnot 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 nameddeclaration.jsonand card records as the agent's writes. The runtime-artifact list is gone. -
session mint-challengeprints the outside-verifier pair (present --challengeandverify-presentation --challenge) alongside the room-join commands, and the handshake page mints its nonce with the CLI. -
treeship agentsis visible in--helpand 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_completenesswarning is expected and means what it says. -
A closed stdout no longer aborts a command.
treeship onboard | head -1died 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 verifyprinted thecoverageandnetwork_scoperows 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 aretryblock 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 verifygains aretriesrow 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.v1is 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 verifygains ajudgementsrow 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, numericminimum/maximumand digestpatterns 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.v1provenance grades,boundary.v1digests,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.pyanswers "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--snapshotappends 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 (
extendsthe global config) got its own journal while sharing the global key, grants and store: one--max-uses 1grant could be spent once from every such directory on the same machine, each use verifying1/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 registerwithout 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 asship://…, so no card ever matched; it now starts them asagent://claude-code(TREESHIP_ACTORoverrides), the actor the bridge signs as. The gate'sblocked.v1refusals 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_linkagebecause its parent lived only in the store record, not in the signature. Fixed by theattest receiptchain rule (above); the exact repro is a test. treeship verifyaccepts 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_bindingno longer contradictsreceipt_binding. It warned "not signed" on every package next to a PASS saying the close record bindsreceipt.json. It now passes when the close record is present and binds the body, and warns only on packages without one.signer_trustsays "this ship's own key" on the producer's machine instead of implying a third-party pin, and the pin command it prints carries--yesso 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.jsonand agent cards still surface. - A leftover project stub whose
extendstarget is gone used to makeinitsay "already initialized" andsession startsay "not initialized", each pointing at the other. Config loading now names the stub and the missing target and says what to remove;initanddoctorprint 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 thecdon long paths. - The MCP bridge's failure line has a reason in it (
spawn treeship ENOENTinstead of nothing) when the CLI is missing. - The Go hub client installs from outside the repo.
packages/hubis now modulegithub.com/zerkerlabs/treeship/packages/hub;go get …/pkg/hubclientresolves. - 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_issuerworkaround 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/verifysays thatverifyReceiptdoes not tie an artifact's digest to its id.
- Approval single-use held per config, not per ship (high). The Approval Use Journal followed the resolved config file, so a project stub (
- 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), whatpackage verify --strictmust say before a grade is written, anevaluation.v1grade 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.shplays 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 receiptinside a session was always sealed loose.attest actionchains onto the session's head by default when the actor is the session's actor (0.31.4);attest receiptnever did, so a receipt minted in the minter's own session (an evaluator's grade in its grading session, a verifier's recompute report) failedchain_completenessunder--stricton any other machine. Found by the evaluator kit's first run.attest receiptnow follows the same rule: chained by default when--systemis the session's actor, sealed loose with a hint when it is not,--parent <id>to chain on purpose,--no-parentto 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), whatpackage verify --strictmust say before a grade is written, anevaluation.v1grade 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.shplays 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 receiptinside a session was always sealed loose.attest actionchains onto the session's head by default when the actor is the session's actor (0.31.4);attest receiptnever did, so a receipt minted in the minter's own session (an evaluator's grade in its grading session, a verifier's recompute report) failedchain_completenessunder--stricton any other machine. Found by the evaluator kit's first run.attest receiptnow follows the same rule: chained by default when--systemis the session's actor, sealed loose with a hint when it is not,--parent <id>to chain on purpose,--no-parentto 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 runstreeship halt listfirst and the script usesset -e, so on a 0.31.5 binary from npm (nohaltsubcommand 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 nohaltand expects the call to be allowed. - Network scope: a host allow-list an agent is held to.
treeship declare --network api.example.com,*.internal.netputs a network scope in the declaration (exact hosts or*.suffix;*.example.comalso matchesexample.com),session startcopies it into the manifest, and the sealed receipt'stool_usagenow carriesnetwork_declaredandnetwork_off_scope, the recorded destinations that matched no pattern.package verifygains anetwork_scoperow:PASSwith the connection count when every destination was inside the scope,WARNnaming the ones outside; no scope declared, no row, and the connections stand inside_effectsunjudged.agent register --networksigns the same list into the certificate and writes it on the card;attest card --networkrecords it ascapabilities.networkandverify-capabilityreports it asdeclared network. The Claude Code gate refuses aWebFetchto a host outside a card's list with a signedblocked.v1naming the host, and allows a host inside it; acurlinsideBashis stillshell.execto the gate. Parity fixtures cover both. The samehost_in_scoperule 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 closenow mints acoverage.v1receipt 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 asdeclared_level(ornone). 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 verifygains acoveragerow that names the receipt, the declared level and the observed count, and warns on packages that carry none;--strictnever promotes it. Close prints acoverage:line and JSON addscoverage_artifact_idandcoverage_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 closenow mints acoverage.v1receipt 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 asdeclared_level(ornone). 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 verifygains acoveragerow that names the receipt, the declared level and the observed count, and warns on packages that carry none;--strictnever promotes it. Close prints acoverage:line and JSON addscoverage_artifact_idandcoverage_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 oteland the auto-export fromwrapandsession closeused 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-featuresremoves it and the commands say so. Nothing is exported untilTREESHIP_OTEL_ENDPOINTis 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:statusandexportoutput as printed, endpoint required with no default, export enabled by default once the endpoint is set, and close exporting thesession.closeartifact'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 "…"(ortreeship halt '*') signs ahalt.v1receipt, 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 asblocked.v1withoperator_revocation.treeship halt --lift <actor>signs the lift and clears the marker;treeship halt listshows 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, theTreeship-Receiptcommit trailer, and averify-receiptsGitHub 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 (withrecord.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 addsreceipt_digest,commit_trailerandreceipt_copy. The composite action at.github/actions/verify-receiptsinstalls a pinnedtreeship, runspackage verify --stricton every package in the checkout against a trust roots file the repository commits, cross-checks the trailers on the PR's commits againstsha256(receipt.json), writes a step summary with one row per package, and fails on any verdict other thanverified, on a trailer mismatch, and, withrequire: 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 closesigns thesession.v1close record with the actor's own key when the actor has one, and wrote it into the package afterkeys.jsonhad already been composed from the chain's signers, so the package named every signer except the one onrecord.json. On the producing machine the key came from the local keystore and the package verified; on any other machinereceipt_bindingfailed with "a key the package does not carry". Close now adds the record signer's public key tokeys.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.rsis 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-capabilityon 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-attestationchecked 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_signaturesrow). - The Claude Code plugin gates tool calls on the agent card. A
PreToolUsehook reads the session actor's registered card (treeship agent register --tools/--forbidden/--escalation), maps Read/Write/Bash/WebFetch/Agent andmcp__server__toolonto the card's capability names, and denies a forbidden tool with a signedblocked.v1receipt (scope_violation), asks the operator for an escalation tool, allows a bounded one, and records an off-card call as anagent.noteunlessTREESHIP_GATE=enforceturns 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 mocktreeshipnow answerssession status --format jsonand the runner seeds cards fromfixtures/<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.v1is 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, withverdictinpass,fail,inconclusiveenforced before signing. Attested with--subjecton 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.spawnedandagent.returnedexisted in the event vocabulary and the graph builder, and no producer emitted them: a session with subagents sealed withspawned_subagents: 0and noparent_childedge.session eventnow takes both types (--agent-nameis the child; the parent rides in--metaasspawned_by, the return target asreturned_toor--destination), and the timeline prints them asspawnedandreturned. The Claude Code plugin addsSubagentStartandSubagentStophooks 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.v1andverification.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--subjectset to the packet, so the result chains onto the claim it checks andverifywalks from one to the other.verdictismatch,mismatchorinconclusiveand anything else is refused before signing;methodis 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, soverify lastpasses on a receipt aboutord_12345(FR-4, the receipt half).session report --format jsonexits 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 besideverification_status: fail(FR-7).verify-presentationprints 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 thekeys exportline to ask the producer for (FR-5). The default--parentinside a session is scoped to the session's actor: another agent's receipt in the same workspace is sealed loose unless it passes--parenton purpose (P3). - Release: the npm landing wait asks for the exact version and waits fifteen minutes.
wait-for-npm-version.shcompared thelatestdist-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.