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.