Configuration reference
Relay is configured by a single JSON file, team.json, plus two environment
variables and one untracked file the installer writes per checkout. This page
documents every key exhaustively, with its type, default, whether it is
required, and what it affects. The source of truth is team.py
(TeamConfig and TeamMember); the values below are taken from it.
The two copies of team.json
team.json exists in two places, and they play different roles:
- The interview input you hand to
pibmo-relay-install --config team.json. This is where you author the roster. - The canonical roster inside the transport store, at
<store>/team.json. The installer writes it there so the roster travels with the store: a member who clones the store receives the closed allowlist instead of falling back to accepting any address. Every runtime command loads the store’s copy, resolved from<store_root>/team.json.
Editing the store’s copy directly is not the intended workflow. Re-run the installer with an updated interview file so the change is committed deterministically. See Onboarding for how the interview produces the file.
Top-level keys
| Key | Type | Required | Default | Affects |
|---|---|---|---|---|
topology | string | no | "per-member-clone" | Deployment and security model. One of per-member-clone or shared-clone. Drives the topology guard. |
instance_name | string | no | "mail" | The instance’s command token. Names the generated wrapper tools/<instance_name>.sh and the session hook. Must be a single path token — no / or \. |
remote | string | no | "origin" | Git remote name of the transport repository. The upstream is computed as <remote>/<branch>. |
branch | string | no | "main" | Git branch the transport lives on. Combined with remote to form the push/rebase upstream. |
members | array | no | [] | The roster. Each entry is a member object (below). An empty or absent roster makes the store fail closed on any send. |
topology
per-member-clone (the default) gives each member an independent clone that
synchronizes over a git remote; shared-clone puts several checkouts on one
machine behind a single clone. They differ in security model, not just paths.
Declaring shared-clone while members resolve to distinct operating-system
users is refused at startup by the topology guard. The full treatment is in
Topologies.
Verified refusal (constructed in Python; the same check runs on every engine startup and on install):
TopologyGuardError: SECURITY GUARD REFUSAL: 'topology: shared-clone' isincompatible with distinct operating-system users ['alice', 'bob', 'carol'].instance_name
{ "instance_name": "crew" }With this, the installer generates tools/crew.sh and a
crew-session-start.sh hook. Nothing hard-codes pibmo or relay into your
tree; the instance name is yours to choose.
remote and branch
Neither origin nor main may be assumed. A team whose transport branch is
trunk on a remote named upstream writes:
{ "remote": "upstream", "branch": "trunk" }Running with no remote at all is a fully supported mode, and is the most
common first-run state for a single-machine team. When the store clone has no
<remote> configured, every write commits locally and stops there — mail never
leaves the box, and that is not an error. Add a remote later by re-running the
installer with --remote-url. See Architecture for the
commit-and-push loop that consumes these values.
Member keys
Each entry in members is an object. Only role is required.
| Key | Type | Required | Default | Affects |
|---|---|---|---|---|
role | string | yes | — | The member’s address and mailbox name. Recipients are named by role. Absence is an error when the file is parsed. |
persona | string | no | "" | Free-text human label. Shown by who next to the role. No functional effect. |
status | string | no | "active" | Addressability and mailbox creation. active and planned are addressable; retired is not. See below. |
kind | string | no | "agent" | Descriptive metadata (agent, human, system). Round-tripped into the store roster but not enforced or used by any code path. |
os_user | string | no | null | The operating-system user this seat runs as. Consumed only by the topology guard under shared-clone; inert under per-member-clone. |
checkout_path | string | no | null | Absolute path of the member’s checkout. The only unambiguous identity signal. See below. |
status semantics
Status is matched literally against a fixed set:
active— addressable; the installer createsinbox/,sent/, andarchive/mailboxes for the role.planned— addressable, treated exactly likeactive. Use it to provision a seat before its checkout exists so others can already address it.retired— not addressable. Excluded from the send allowlist, from broadcast fan-out, and from identity resolution. Its entry is retained in the roster for history but no mailbox is created.
Any other string is accepted without complaint and behaves like retired: the
member is silently non-addressable and gets no mailbox. There is no enum
validation, so a typo such as "activ" fails closed rather than loud. Verified:
$ pibmo-relay --role architect send --to auditor --subject "hi" --body xpibmo-relay: Unknown recipient address 'auditor' (not in roster allowlist) # auditor is retired$ pibmo-relay --role architect send --to builder --subject "welcome" --body xsent 09e72f6d-... architect -> builder [info] "welcome" # builder is plannedcheckout_path and identity resolution
A session must resolve which member it is before it can read the right mailbox.
Resolution runs in this order (see resolve_identity in hooks.py):
- The
RELAY_ROLEenvironment variable (or the legacy aliasROLE), if set. - A
checkout_pathmatch: the session’s working directory equals a member’scheckout_pathor is nested under it. When several match — nested checkouts — the most specific (deepest) path wins. - A directory-name inference, used only when no
checkout_pathresolves. The current directory name must match a role exactly, or end in-<role>. If exactly one role matches, it wins; if zero or more than one match, resolution returns nothing.
checkout_path is the only unambiguous signal because it is an explicit,
absolute anchor. Without it Relay falls back to guessing from the directory
name, and an ambiguous guess is treated as no answer — resolving to the wrong
member means reading someone else’s mailbox, so silence is the safe failure.
Provide checkout_path for every real seat. Verified:
cwd=/tmp/demo/architect/sub -> architect # nested under the checkoutcwd=/tmp/other -> (unresolved)Environment variables
| Variable | Read by | Purpose |
|---|---|---|
RELAY_STORE_DIR | --store default | Path to the transport store repository. When unset, --store defaults to . (the current directory), which is almost never the store. Set it, or let the wrapper set it. |
RELAY_ROLE | --role / --from default; identity resolution | The acting member’s role. Supplies the default sender and mailbox when a command omits --from or --role, and is the highest-priority identity signal. The alias ROLE is honored by automatic identity resolution only. |
RELAY_STORE_DIR also names the store when you invoke pibmo-relay directly
rather than through the wrapper.
.relay/config.env
The tracked wrapper tools/<instance_name>.sh holds no machine-specific paths
so it stays portable across every member’s checkout. Machine-specific values
live in an untracked file the installer writes next to the repository root:
<adopter_repo>/.relay/config.envThe installer adds .relay/ to the adopter’s .gitignore. The file is sourced
by both the wrapper and the session hook, and it defines:
RELAY_STORE_DIR— the absolute store path on this machine.RELAY_BIN— the absolute path to the installedpibmo-relaybinary, so the wrapper can find it even when it is not onPATH.
A verified example (paths abbreviated):
# Written by pibmo-relay-install. Machine-specific; not tracked.RELAY_STORE_DIR="/home/you/code/project-mail"RELAY_BIN="/home/you/code/project/.venv/bin/pibmo-relay"Resolution order in the wrapper: an already-exported RELAY_STORE_DIR wins;
otherwise config.env is sourced. If neither yields a store path the wrapper
exits with a clear error rather than guessing.
Complete annotated example
Every key below was accepted by the installer and produced the behavior noted in the comments. JSON does not permit comments; strip them before use.
{ // Default topology: each member has an independent clone. "topology": "per-member-clone",
// Generates tools/crew.sh and crew-session-start.sh. "instance_name": "crew",
// Transport upstream is upstream/trunk, not the assumed origin/main. "remote": "upstream", "branch": "trunk",
"members": [ { "role": "architect", // address; mailbox name "persona": "System design", // shown by `who`, no functional effect "status": "active", // addressable; gets a mailbox "kind": "agent", // descriptive only "checkout_path": "/home/you/code/project-architect" // exact identity anchor }, { "role": "builder", "status": "planned" // addressable before its checkout exists }, { "role": "auditor", "status": "retired", // retained for history; NOT addressable "kind": "human" } ]}Installing this roster and running who against the resulting store prints:
store: /home/you/code/project-mailupstream: upstream/trunktopology: per-member-cloneidentity: (unresolved)roster: architect active System design builder planned auditor retiredThe installer creates mailboxes for architect and builder only; auditor
is listed but has no roles/auditor/ tree. Sending to auditor is refused;
sending to builder succeeds.
A note on confidentiality
None of these keys grant message confidentiality between members. A git-native
transport gives every member a full clone, and a clone holds every message,
including ones addressed to others. topology and os_user under
shared-clone control credential isolation between members’ machines, not
isolation of messages from members. If a team needs per-member message
confidentiality, a git-native transport is the wrong architecture. This is
stated in full in Topologies and
Concepts.
See also
- Getting started — install and run a first exchange.
- Onboarding — the interview that produces
team.json. - CLI reference — every command and flag that consumes this config.
- Topologies — the topology guard and the security model.
- Hooks — the session hook that reads
config.env. - Architecture — how
remoteandbranchdrive the commit-and-push loop. - Troubleshooting — recovering when a write cannot push.
- v1 on-disk contract — the store layout the roster scaffolds.