Skip to content

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

KeyTypeRequiredDefaultAffects
topologystringno"per-member-clone"Deployment and security model. One of per-member-clone or shared-clone. Drives the topology guard.
instance_namestringno"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 \.
remotestringno"origin"Git remote name of the transport repository. The upstream is computed as <remote>/<branch>.
branchstringno"main"Git branch the transport lives on. Combined with remote to form the push/rebase upstream.
membersarrayno[]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' is
incompatible 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.

KeyTypeRequiredDefaultAffects
rolestringyesThe member’s address and mailbox name. Recipients are named by role. Absence is an error when the file is parsed.
personastringno""Free-text human label. Shown by who next to the role. No functional effect.
statusstringno"active"Addressability and mailbox creation. active and planned are addressable; retired is not. See below.
kindstringno"agent"Descriptive metadata (agent, human, system). Round-tripped into the store roster but not enforced or used by any code path.
os_userstringnonullThe operating-system user this seat runs as. Consumed only by the topology guard under shared-clone; inert under per-member-clone.
checkout_pathstringnonullAbsolute 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 creates inbox/, sent/, and archive/ mailboxes for the role.
  • planned — addressable, treated exactly like active. Use it to provision a seat before its checkout exists so others can already address it.
  • retirednot 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 x
pibmo-relay: Unknown recipient address 'auditor' (not in roster allowlist) # auditor is retired
$ pibmo-relay --role architect send --to builder --subject "welcome" --body x
sent 09e72f6d-... architect -> builder [info] "welcome" # builder is planned

checkout_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):

  1. The RELAY_ROLE environment variable (or the legacy alias ROLE), if set.
  2. A checkout_path match: the session’s working directory equals a member’s checkout_path or is nested under it. When several match — nested checkouts — the most specific (deepest) path wins.
  3. A directory-name inference, used only when no checkout_path resolves. 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 checkout
cwd=/tmp/other -> (unresolved)

Environment variables

VariableRead byPurpose
RELAY_STORE_DIR--store defaultPath 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 resolutionThe 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.env

The 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 installed pibmo-relay binary, so the wrapper can find it even when it is not on PATH.

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-mail
upstream: upstream/trunk
topology: per-member-clone
identity: (unresolved)
roster:
architect active System design
builder planned
auditor retired

The 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