trust
Manage pinned trust roots — the issuer keys your verifiers accept, one power per pin.
Your trust-root store is the policy half of every verification: nothing is trusted because an artifact carried it, only because you pinned it. Pins live in ~/.treeship/trust_roots.json (mode 0600; override path with TREESHIP_TRUST_ROOTS).
Subcommands
treeship trust list
treeship trust add <key-id> <public-key> --kind <kind> [--agent <uri>] [--label <text>] [--yes] [--replace]
treeship trust remove <key-id> [--yes]Kinds — one power per pin
| Kind | Pinning a key under it means |
|---|---|
agent_cert | Accept this agent key's certificates, cards, and receipts (leaf pin). With --agent <uri>, the key's capability cards for that agent are key-bound; see below |
cert_issuer | Accept agent certificates this ship key issues — the chain-walk pin |
hub_checkpoint | Accept this key's signed Merkle checkpoints (staples, transparency) |
hub_org | Pin an org checkpoint signer. This is not a single-use approval ledger |
revoker | Honor capability revocations this ship key signs |
session_host | Honor multi-agent room invitations this host key mints |
transparency_log | Accept this Rekor log's signed entry timestamps and checkpoints as witnessed time. ECDSA P-256, given as @rekor.pub or ecdsa-p256:<base64url DER>. Sigstore's public-good log is built in; any pin of this kind replaces it |
The deprecated ship kind is rejected with guidance — it used to grant three unrelated powers at once:
✗ the `ship` trust kind is deprecated and no longer honored …
Pin the specific power instead: hub_org / cert_issuer / revokerLegacy ship entries in an existing file still parse but are inert until re-pinned.
Scope
Trust roots are machine-wide: ~/.treeship/trust_roots.json (or TREESHIP_TRUST_ROOTS) is consulted by every workspace on the machine, project-local ones included, and trust add writes there whatever --config or TREESHIP_CONFIG says (its output names the path). Testing several ships on one machine therefore needs a separate HOME or TREESHIP_TRUST_ROOTS per ship, or a pin from one take silently satisfies the next. Pinning a key inside one project trusts it in every other project you verify from here. The Approval Use Journal, by contrast, follows the keystore: one journal per ship. A project workspace with its own keys under its own .treeship/ has its own journal; a project stub that extends the global config shares the global one.
Adding a pin
treeship keys export on the counterparty's machine prints the exact commands. The public key is ed25519:<base64url> (bare base64url also accepted):
treeship trust add key_9f8e7d6c ed25519:K8V6… --kind cert_issuer --yesWithout --yes the CLI shows the key's SHA-256 fingerprint and asks for confirmation; non-interactive use (including --format json) requires --yes. Adding a pin for a (key_id, kind) pair that's already pinned to the same public key just re-confirms it.
A key id is a label, not a secret, so re-pointing one at a different public key needs --replace: trust add refuses otherwise, naming both fingerprints (the one pinned, the one you're trying to add) so you can tell a legitimate key rotation from a forged package quietly reusing a familiar id. This check runs across every kind the id appears under, not just the one you're pinning now. With --replace, it proceeds and prints replacing <key_id>: fp <old> -> fp <new>.
✗ key_9f8e7d6c is already pinned (cert_issuer, label "acme-corp") with a different public key: pinned fp a1b2c3d4e5f6a7b8, this key fp 1122334455667788. Key ids are labels; this is either a rotated key or a different signer reusing the id. If you have confirmed the new key out of band, re-run with --replaceThis ship's own key ids (the ones treeship keys export lists) are a separate case: trust add refuses to re-point one of them to a different public key even with --replace. A package signed under your own key id by a key that isn't yours is not yours, whatever flag you pass.
package verify's signer_trust row and session join's SessionHost hint both print a ready-to-paste trust add command for an unpinned key, and neither appends --yes anymore: pasting the line stops at the fingerprint prompt instead of skipping straight past it. The session join hint also says explicitly to confirm the key with the host out of band -- over a channel the invitation itself didn't travel on -- before running it.
remove deletes all pins for the key ID, across every kind.
Scoping an agent pin
A capability card is key-bound when its own key signed it and that key is pinned under agent_cert as the agent the card names: treeship trust add key_… ed25519:… --kind agent_cert --agent agent://deployer. The scope is what stops a key you pinned as one agent from signing itself into another agent's name; a card for agent://payments signed by the key pinned as agent://deployer verifies as a signature and is graded asserted, and resolve and verify-capability say which pin they found. treeship agent register --own-key writes the scope for the agent's own key, and the trust add lines that keys export --agent <uri> and onboard print carry it.
--agent takes one spelling: scheme://name[/path], lower-case scheme, no trailing slash, no whitespace, query, fragment or .. segment; agent://alice/ is stored as agent://alice and anything else is refused (exit 4), so a pin and the card it is compared with cannot differ by an accident of spelling. Re-pinning a key id as a different agent, or dropping its scope, changes what the pin vouches for and needs --replace, like a different public key does; pinning one public key under two key ids as two different agents prints a warning. In --format json the verdicts of resolve, resolve --hub and verify-presentation carry key_bound_reason: not_pinned, pin_unscoped, pin_scoped_to_other or key_not_verified, null when the card is key-bound.
A pin written before --agent existed has no scope. If its label is what agent register wrote, the bare agent name or the agent:// URI, that is taken as the scope. Any other unscoped pin still lets the key's artifacts verify and import, but binds no card; the output names the fix: treeship trust add <key-id> <public-key> --kind agent_cert --agent <uri> --replace --yes. A pin is never silently promoted to key-binding.
Compromised keys
A revocation whose reason is compromised (treeship revoke-capability <card> --reason compromised) retires the key, not just the card: every card that key minted, older or newer, is treated as revoked by every verifier that sees the revocation. The definitive answer to a compromised key is still to unpin it, treeship trust remove <key-id>, so that nothing it signs verifies here at all.
JSON output
list→{ "status": "ok", "path", "roots": [ { "key_id", "public_key", "kind", "label", "added_at" } ] }add→{ "status": "ok", "message": "trust root added", "key_id", "kind", "public_key", "fingerprint", "replaced", "path" }remove→{ "status": "ok", "key_id", "removed", "count", "path" }
Pin only the powers you intend to grant. Pinning a hub's key under hub_checkpoint for transparency does not — and must not — let that hub mint certificates or kill your capabilities; that isolation is the entire point of the kind split.