Skip to content

This page is written to be read by an AI coding agent. You do not follow it yourself; you hand it to a coding session running in your repository, and the session interviews you, writes the configuration, runs the installer, and checks that mail actually flows.

Point your agent at this page

Open a coding session in the repository you want to adopt Relay, and give it this instruction:

Set up Pibmo Relay in this repo by following https://pibmo.com/relay/onboarding/.

The session will fetch this page and work through it top to bottom, asking you the questions in section 2 one at a time. The rest of this page is addressed to that session.

If you are an agent reading this: follow it top to bottom. Ask the questions in section 2 one at a time, in order, and wait for an answer before moving on. Do not guess an answer you were not given. Do not run the installer until section 4. Everything you need to decide is a question to the human, not a default you may choose for them.

A human can also follow this page by hand. It is the same sequence either way.


1. What you are setting up

Relay carries addressed, read-tracked mail between agent sessions over a git repository. Setting it up means deciding four things:

DecisionWhy it cannot be defaulted
Who the members areOnly you know your team’s roles and where their checkouts live
Where the transport repository livesIt must sit outside your product repository, and whether it has a remote at all depends on how your team works
Whether members are separate operating-system usersThis changes the security model, not just the paths
What the instance is calledRelay never assumes its own name in your tree

Before you start

The human needs git on PATH, a configured git identity (git config user.email, because the installer makes a commit), and Python 3.9 or newer. Check those, and whether the pibmo-relay CLI is already installed:

Terminal window
python3 --version && git --version && git config user.email && pibmo-relay --version

If pibmo-relay is missing, install it — ask the human before running anything that changes their machine. Relay is installed from source and is not on any package index, so the installer requires you to say where from:

Terminal window
curl -fsSL https://pibmo.com/relay-install.sh \
| RELAY_SOURCE=<git URL or path to a checkout> sh

Do not pass a bare package name. pibmo-relay is unregistered on PyPI; anything resolving under that name is not Relay. The installer refuses a bare name rather than resolving it, so this fails closed rather than installing something else.

That installs two commands, pibmo-relay (the client) and pibmo-relay-install (the installer), using uv, pipx, or pip, whichever is present. If the command is still not found afterward, the tool’s bin directory is not on PATH; see Troubleshooting.


2. The interview

Ask these in order. The answers become team.json.

Q1. What should this instance be called?

It becomes the command your team types, as tools/<name>.sh. Something like team-mail, crew, or desk. Relay hard-codes nothing about its own name.

Q2. Who are the members?

For each member, collect:

  • role — the address other members send to. Lowercase, no spaces (lead, analyst, web). This is a mailbox name, not a job title.
  • persona — optional human-readable name, purely routing shorthand.
  • checkout_path — the absolute path of that member’s working copy. Strongly recommended: it is the only unambiguous way a session can tell which member it is. Without it, Relay guesses from the directory name and refuses to guess when the answer is ambiguous.
  • statusactive, planned, or retired. Planned members can receive mail; retired members cannot be addressed.

Q3. Are the members separate operating-system users?

This is a security question, so ask it plainly and do not infer the answer from the paths.

  • No, all checkouts run as one OS user (the common case: several directories under one login). Either topology works. Use per-member-clone.
  • Yes, each member is a separate OS user, so credentials are isolated. You must use per-member-clone. Relay will refuse shared-clone here, because the directory permissions that make a shared clone work are exactly the permissions that let one user read and write another’s mailbox.

Record os_user per member if the answer is yes. The security reasoning behind this question is in Topologies and the security model.

Q4. Where should the transport repository live, and does it need a remote?

Two separate questions. Ask both.

Location. It must be outside every product repository. Mail is committed and pushed, so a store inside your project would put agent chatter into your project’s history and push it to your default branch. The installer refuses this outright. A sibling directory is the usual answer: for a project at ~/code/myproject, the store goes at ~/code/myproject-mail.

Remote. Ask the human directly, because both answers are legitimate:

  • No remote (local only). Everything runs on one machine. Mail is committed locally and never pushed. Simplest, no network, no hosting. This is a real supported mode, not a degraded one. sync and flush will report that no remote is configured, which is expected.
  • A shared remote. Members have their own clones and synchronize through it. You need a git URL every member can reach and push to — a private hosting repo (a clone holds every message), or, for several checkouts on one machine that you still want to sync through a repo, a local bare repo (git init --bare ~/code/project-mail.git) whose filesystem path is the URL.

If they choose a remote, also ask what its default branch is called: not every repository uses main, Relay will not assume, and the branch set in the roster must match the transport’s actual default branch or the first push fails.

Q5. Should sessions be told about new mail automatically?

Recommended: yes. The installer writes a SessionStart hook so each session opens with a banner listing unread mail. Without it the transport still works, but mail sits unread until someone thinks to check, which is the failure mode this whole system exists to prevent. See Host integration.


3. Write the configuration

Produce team.json in the adopting repository from the answers. A worked example for a three-member team on one machine with no remote:

{
"instance_name": "team-mail",
"topology": "per-member-clone",
"members": [
{
"role": "lead",
"persona": "Ada",
"status": "active",
"checkout_path": "/home/you/code/project-lead"
},
{
"role": "analyst",
"persona": "Grace",
"status": "active",
"checkout_path": "/home/you/code/project-analyst"
},
{
"role": "web",
"status": "planned",
"checkout_path": "/home/you/code/project-web"
}
]
}

With a shared remote whose default branch is trunk, add:

{
"remote": "origin",
"branch": "trunk"
}

Show the human the file and get confirmation before running anything.

Full key-by-key documentation: Configuration reference.


4. Run the installer

The interview configures; this script installs. Everything from here is deterministic and re-runnable.

Terminal window
pibmo-relay-install \
--adopter-dir . \
--config team.json \
--store-dir ../project-mail

Add --remote-url git@github.com:you/project-mail.git if the team chose a remote in Q4. Omit it entirely for local-only. If you omit --store-dir, the installer defaults to a sibling directory named <repo>-mail.

The installer will:

  1. Create and git init the transport store, on the configured branch.
  2. Create a mailbox skeleton for every member and commit it, so a member who clones the store receives their mailboxes and the roster.
  3. Write tools/<instance>.sh, a wrapper holding no machine-specific paths, so it is safe to commit and share.
  4. Write .relay/config.env with this machine’s paths (RELAY_STORE_DIR and RELAY_BIN), and add .relay/ to .gitignore.
  5. Register the SessionStart hook in .claude/settings.json, merging with whatever is already there. Pass --no-hooks to skip this step.

Re-running it with the same configuration reports that nothing changed. If it refuses because the store would sit inside the adopting repository, that is the guard in Q4 doing its job; pick a location outside the repo.

Repeat for each member’s checkout

How this works depends on whether the team has a remote:

  • With a shared remote (per-member-clone): run the same command in every member’s working copy, pointing --store-dir at that member’s own clone of the store. Each member has their own clone and they synchronize through the remote. The first member’s install publishes the store to the remote; every later member’s install clones it.
  • On one machine with no remote: there is nothing to synchronize through, so separate clones would never see each other’s mail. Point every checkout’s --store-dir at the same store directory instead — one shared store that all seats read and write. This is the natural fit for shared-clone; the worked example above uses no remote, so its checkouts must share one store.

In short: separate clones need a remote to exchange mail; a single-machine team with no remote shares one store directory. See Topologies & Security.


5. Verify it works

Do not report success until a message actually crosses from one checkout to another. Under per-member-clone (this guide’s example) each member has their own clone of the store: a send commits and pushes to the transport, and the recipient pulls it with sync. So the check spans two checkouts.

In the sender’s checkout (here lead — run these from that working copy):

Terminal window
./tools/team-mail.sh who # identity resolves to: lead
./tools/team-mail.sh send --to analyst \
--type request --subject "Setup check" --body "Reply if you can read this."
# sent 5a253779-... lead -> analyst [request] "Setup check"

In the recipient’s checkout (analyst, a different working copy):

Terminal window
./tools/team-mail.sh sync # pull from the transport -- see the note below
./tools/team-mail.sh list # [unread] from=lead ... "Setup check"

Inside a member’s checkout you do not pass --role / --from: the member is resolved from its checkout_path. who printing identity: (unresolved) means this checkout could not be matched — set checkout_path for that member and re-run the installer, or export RELAY_ROLE. And ./tools/team-mail.sh is just the pibmo-relay command with this checkout’s store preset.

The most common “it looks broken” is a missing sync. With separate clones (per-member-clone), a message the sender pushed is not in the recipient’s clone until the recipient runs sync. The session banner reports what is already in the local clone and does not fetch, so run sync when you sit down to a session or before checking mail. A shared-clone single-store team has one store and no sync step — every seat sees each write immediately.

Then start a fresh session in the recipient’s checkout and (after a sync) confirm the unread-mail banner appears. If it does not, see Host integration.


6. Tell the team what it does not do

Say this out loud to the human before you finish, because it is the property people most often assume backwards:

Relay provides addressing and read-tracking. It does not provide confidentiality between members. The transport is a git repository, git has no path-level access control, and any member who can clone it holds every message ever sent, including those addressed to someone else. Sender names are self-asserted and are not authenticated.

Under isolated operating-system users you additionally get credential and compute isolation. That is isolation of members from each other’s machines, not of messages from other members.

If a team needs per-member message confidentiality, a git-native transport is the wrong architecture and no configuration of Relay will provide it. The full security model is in Topologies and the security model and Architecture.


Where to go next