Team and agents

Joining, approving, revoking; AI reviewers as members.

Every device is a member with its own keys. People, second laptops, CI boxes and AI reviewers all join the same way; what they may do is decided at approval.

Joining from a new device

On the device that wants access:

$ secretree join --vault git@gitlab.com:team/app-vault.git --name bob-laptop
keys stored in macOS Keychain (service secretree).
join request 9397afa7 pushed to the vault; a member approves it with:
  secretree member approve bob-laptop

The request (vault id, device name, age public key, OpenSSH public key; nothing secret) is encrypted to the current members and pushed into the vault as requests/<id>.json.age. The host sees a random file name. The joiner needs push access to the host and nothing else.

Where the joiner cannot push, join --no-push --out join.txt writes the request to a file to send by any channel; secretree member request prints it again later from a configured repository.

Approving

Any member sees what is waiting and decides:

$ secretree member pending
  bob-laptop       age1n92…  requested 2026-09-19 10:41  (id 9397afa7)
$ secretree member approve bob-laptop
member added: bob-laptop (age1n92…)
generation 000010 written (full) for the new recipient set
request 9397afa7 removed from the vault; bob-laptop can now clone
$ secretree member deny carol            # removes a request without adding anyone

A request file received by hand is approved with member add --request join.txt.

vault.json is re-signed with the new recipient and signer, and a full generation is written to the new recipient set right away, so the newcomer can clone the complete history. Backup snapshots made before this point stay opaque to them: their signatures and chain position are still verified, their contents are not readable by the new key.

Without a request file, the keys can be given directly:

$ secretree member add --name ci-box --recipient age1… --signer "ssh-ed25519 AAAA… ci-box"

--signer is optional; a member without one can read but not write.

Listing

$ secretree member list
vault 7cac93dda9ebaed9: 3 recipient(s), 3 signer(s)
  alice            age1sdwe…  signer SHA256:ELUg…  (this device)
  bob-laptop       age1n92m…  signer SHA256:Nc3k…
  review-bot       age1l0y6…  signer SHA256:7Q2x…  [agent]

Removing and revocation

$ secretree member remove --name bob-laptop
member removed: age1n92…
generation 000012 written (full) for the new recipient set

The recipient is dropped, so every later generation is unreadable to that key. The signer line moves into revoked_signers with the last generation and ledger entry it may have signed: history stays verifiable, and a revoked key that still holds a push credential for the host cannot append anything other members will accept. The removed member's next git fetch fails.

What they already fetched stays theirs; zero knowledge cannot un-send.

Agents

An AI reviewer, a linter bot or any automation is a member with the agent role:

$ secretree member approve review-bot --role agent
agent added: review-bot (age1…); its approvals do not count and it cannot merge

Agents read, comment and review. Their approvals never count toward required_approvals, they cannot merge (even with a human approval present), and their comments carry an agent mark in pr show and in the UI. The agent runs as a runner job; a working reviewer built on the Claude CLI ships as agents/review.sh in the repository.

Several repositories in one vault

A vault may hold several repositories, each under its own random repo-id. secretree init in a second repository against the same vault adds one; secretree clone <vault>#<repo-id> and git clone secretree::<vault>#<repo-id> pick one when the vault holds more than one. All members of the vault see all its repositories; use separate vaults for separate access.