Pull requests and reviews
Open, review, merge; policy; the local UI.
A pull request is data, not a service. Title, base and head, every comment, approval, check result and state change is a signed event under refs/secretree/collab, synced through the encrypted remote like any branch.
Opening
Open from the branch; the branch is pushed for you when it is not on the vault yet:
$ secretree pr open --title "Add retry helper" --body "Wraps flaky network calls." --head feature/retry --base main
pull request #1 opened: Add retry helper (feature/retry → main, 900c2a69)
--head defaults to the current branch, --base to main (or master). The title may also be given as the first argument.
Listing and showing
$ secretree pr list
#1 open Add retry helper feature/retry → main by bob-laptop ✓1 [✓ci]
$ secretree pr list --all # merged and closed too
✓1 is one approval of the current head; [✓ci] a green check named ci.
$ secretree pr show 1
#1 Add retry helper [open]
feature/retry → main (900c2a69 → fd72f765) by bob-laptop on 2026-09-18 10:40
Wraps flaky network calls.
approvals: 1/1 [alice] changes requested: []
check ci: success ./.secretree/ci passed in 4s
[09-18 10:41] alice commented on src/retry.go:12@900c2a69 (id 3f1a9c2e):
Should the delay grow between attempts?
[09-18 10:42] alice: approve @900c2a69 Fine as a first version.
src/retry.go | 15 +++++++++++++++
Pull requests are referenced by number (1, #1) or by a prefix of their id.
$ secretree pr diff 1 --stat # the change against the base (or the merge, once merged)
$ secretree pr checkout 1 # switch to the head branch, tracking the vault
Commenting
$ secretree pr comment 1 -m "Should the delay grow?" --path src/retry.go --line 12
comment added to #1
A comment with --path and --line is anchored to that line of the head commit at the time. When the head moves, the comment follows the line through git diff to its new number and is shown inline as "followed from line 12 of 900c2a69". If the line itself was changed or removed, the comment is marked outdated and stays in the conversation only. A comment without --path is a general one.
Threads are closed by anyone:
$ secretree pr resolve 1 3f1a
#1: thread 3f1a9c2e resolved
Reviewing
$ secretree pr approve 1 -m "Looks good."
$ secretree pr request-changes 1 -m "Cap the delay."
$ secretree pr review 1 --verdict comment -m "No blocking issues."
A review is recorded for the current head commit. Only reviews of the head that is actually merged count; a new push needs new approvals. Agents' approvals never count.
Policy
$ secretree policy --approvals 1 --checks ci
wrote .secretree/policy.json; commit it on the base branch to enforce it
{
"required_approvals": 1,
"required_checks": ["ci"]
}
The policy is read from the base branch at merge time. It is enforced by the client that merges and verifiable by every other client, because approvals and checks are signed events: a merge that violates it is detectable, not silently possible. Host-side branch protection still applies to raw git push of the base branch, which secretree does not prevent.
Merging and closing
$ secretree pr merge 1 # merge commit
$ secretree pr merge 1 --method squash
$ secretree pr merge 1 --method ff
#1 merged into main as 4ebfacfe
$ secretree pr close 2
The merge happens in a temporary worktree, is pushed through the helper, and is recorded as a state event. Refusals are explicit:
cannot merge: needs 1 approval(s) of 900c2a69, has 0
cannot merge: required check "ci" has not run for 900c2a69
cannot merge: changes requested by alice
cannot merge: this device is an agent; a person has to merge
The local UI
$ secretree ui --open # http://127.0.0.1:7391
$ secretree ui --install # keep it running as a login service
Pages: branches and tags, file tree, files with syntax highlighting and line anchors (/blob/<ref>/<path>#L10), blame, history, commit diffs, search across every branch, the pull request list and each pull request with its diff, inline comments (a + next to any line opens a comment form), Markdown in descriptions and comments, reviews, checks with logs, the merge and close buttons, and Resolve on each thread. Two more pages show the guarantees: vault (the remote exactly as the host sees it, the generations, vault.json, last backup and proof) and ledger (every deliberate disclosure). The header carries the restore-proof badge on every page. Light and dark themes follow the system. Writes go through the same code as the CLI and are signed by this device's key. No script is loaded from anywhere.
The UI listens on loopback only. --listen 0.0.0.0:7391 is allowed for a private network (Tailscale, WireGuard) and prints a warning, because everything it serves is plaintext.
secretree link src/retry.go:12 prints a permalink pinned to the current commit. Host go.html from the site repository on any static host to make such links open in the local UI for everyone who holds a key.