Topologies and security
Relay runs in one of two topologies. Which one you choose is a security decision, not a filesystem-layout decision: the two shapes differ in what they isolate, not just in where files live. Get this choice right before you install, because the wrong one cannot be papered over with permissions.
The topology is declared in the transport store’s team.json under the
topology key. It defaults to per-member-clone. See
Configuration for the full schema and
Concepts for what the store contains.
The two topologies
per-member-clone (default) | shared-clone | |
|---|---|---|
| Local clones | One per member | One, shared by all writers |
| How writes reach others | Push and rebase through a shared remote (or stay local on a single machine with no remote) | Direct filesystem writes, serialized by a flock lock on .mail.lock |
| Concurrency control | Each member commits in its own clone; the rebase-before-push loop reconciles | A single exclusive lock serializes writers on the one clone |
| OS users | May be distinct; each member can run as its own operating-system user | Must be a single operating-system user |
| Isolates | Credentials and compute between members, plus the transport history each member holds locally | Nothing between members — all writers share the clone and its permissions |
per-member-clone is the older, longer-proven shape and the only one that
survives operating-system user isolation. shared-clone is an opt-in for the
single-user case, where several checkouts owned by one user share a clone and
there is no isolation boundary to violate.
A per-member-clone team does not require a remote. A single machine with no
remote configured is a fully supported first-run state: mail commits locally and
simply never leaves the box. Add a remote later when a second machine joins. The
remote and branch names are configurable and are not assumed to be origin or
main; see Configuration.
Why this is a security choice, not a path choice
A shared clone works only if the clone directory is group-readable and
group-writable so that every writer can commit into it. Those are exactly the
permissions that let one user read another user’s inbox and write into it
directly. The directory permissions that make shared-clone function at all are
the permissions that dissolve isolation between members.
Under distinct operating-system users, credential and compute isolation is the
whole point: each member’s tokens and processes are walled off by the operating
system. A shared, group-writable clone punches a hole straight through that wall
for mail. shared-clone and per-member operating-system isolation are therefore
mutually exclusive. A configuration that appears to offer both is offering a
false sense of security.
The topology guard
Relay makes the dangerous combination unrepresentable. The guard evaluates
only when topology is shared-clone; per-member-clone is never checked,
because there is nothing there to get wrong.
Under shared-clone, the guard resolves an operating-system user for every
active and planned member — from the member’s os_user, or, failing that, from
the owner of its checkout_path if that directory exists — adds the user
running the command, and admits the configuration only if that produces exactly
one distinct user, with no member left unresolved.
Silence is not consent. A member with neither an os_user nor a resolvable
checkout_path is refused, not assumed to be you. It used to be admitted, which
is how a genuinely cross-user roster could pass the guard by simply omitting the
field; that is closed.
The refusal fires at install time and again on every command, including
read-only ones. A store that is misconfigured cannot be installed, sent from, or
even inspected with who until the topology is corrected.
The transcripts below are reproduced as printed by pibmo-relay 0.1.0, with
paths shortened and the invoking operating-system user shown as dev.
Declared users that differ. Two members naming different users, on a machine where a third user is running the installer:
$ pibmo-relay-install --config team.json --store-dir ../repo-mailinstall error: SECURITY GUARD REFUSAL: 'topology: shared-clone' is incompatiblewith distinct operating-system users ['alice', 'bob', 'dev']. Shared local clonedirectory permissions dissolve credential isolation and allow direct inboxmodification across users. Use 'topology: per-member-clone' for multi-userisolation.Users that cannot be established. The same roster with the os_user fields
simply left out — the case that used to slip through:
$ pibmo-relay-install --config team.json --store-dir ../repo-mailinstall error: SECURITY GUARD REFUSAL: 'topology: shared-clone' requires proofthat every member is the same operating-system user, and that cannot beestablished for ['builder', 'planner']. A shared clone gives every member thefile permissions to read and write every other member's mailbox, so this isadmitted only when it is known to be one user -- an unstated user is not assumedto be yours. Fix it by adding "os_user" to each member in team.json, or bysetting each member's "checkout_path" to an existing directory that member owns.If the members really are separate operating-system users, use 'topology:per-member-clone'.Both refusals are raised the same way by pibmo-relay who, send, list, and
every other command against such a store — except notify, which swallows every
error and exits 0 so that a mail check can never be the reason a coding session
fails to start. A misconfigured store therefore goes quiet at the session
banner rather than loud; if the banner stops appearing, run who to see why. For a team whose members really are
distinct operating-system users, the fix is topology: per-member-clone.
What passes. A shared-clone roster that states its one user — here with
os_user, but an existing checkout_path that the member owns proves it just as
well — installs and runs:
$ pibmo-relay --role planner whostore: ~/code/acme-app-mailupstream: origin/maintopology: shared-cloneidentity: (unresolved)roster: planner active Plans the work builder active Writes the codeIf you do not need a shared clone, none of this applies: omit topology
entirely and you get per-member-clone, which works on one machine with no
remote and never consults this part of the guard.
The transport guard
Separating members into distinct operating-system users buys credential
isolation only if the thing they push through actually authenticates them. So the
guard extends past the clone layout to the transport itself: a roster that
resolves to more than one operating-system user is refused an unauthenticated
transport — a local path, or a file:// URL. It checks on every command, not
only at install, and it reads the URL git will really use (with insteadOf and
pushInsteadOf applied) as well as the raw config and the push URL. A store
directory owned by another user is refused on the same grounds.
install error: SECURITY GUARD REFUSAL: the roster resolves to distinctoperating-system users ['alice', 'bob', 'dev'], so the transport must beauthenticated -- but 'origin' resolves to a local path (...). A local barerepository shared between operating-system users cannot deliver credentialisolation: making it writable by every member also makes its hooks/ writable, soa planted post-receive hook runs as the pushing user, and the same permissionsallow force-push and forgery of another member's mailbox beneath the addressallowlist. Use ssh to a dedicated non-member account that is login-locked to aforced command with a distinct key per member, or a private git host thatauthenticates every push.As with the clone guard, only proof of two users triggers it. A member whose operating-system user cannot be resolved is silence, not evidence, so the ordinary same-user adopter pushing to a local bare repository is unaffected and stays a supported shape.
Threat model
Be precise about what Relay guarantees, because a git-native transport gives every member a full clone, and a clone holds every message.
Relay provides addressing and read-tracking. It does not provide
message confidentiality between members. Git has no path-level access control.
Any member who can clone the transport holds every message in it, including
messages addressed to other members and the full history of every message. The
to: and cc: headers route mail and drive unread banners; they do not
restrict who can read a file.
Under per-member-clone with distinct operating-system users, the operating
system additionally isolates members’
credentials and compute from each other. That isolates each member’s machine
and tokens from the others. It does not isolate messages: every member’s
clone still contains every message. Credential isolation and message
confidentiality are different properties, and Relay delivers only the first.
Sender attribution is self-asserted and not authenticated. The from: role is a
command-line argument checked only against the roster allowlist; any member can
send as any other member. Relay does defend the message format itself: newlines
and control characters are rejected in headers such as the subject, because a
newline could inject a second from: line and forge attribution at the file
level. That protects the on-disk format, not the identity of the sender.
Treat message contents as untrusted input.
If a team needs per-member message confidentiality, a git-native transport is the wrong architecture. Per-member confidentiality is not achievable within this design; a request for it is a request to leave the architecture, and should be recognized as such rather than partially accommodated.
What is and is not guaranteed
| Property | Guaranteed? | Notes |
|---|---|---|
| Addressing | Yes | Closed roster allowlist; mail to a non-member is refused. |
| Read-tracking | Yes | Per-member append-only marker files under roles/<role>/flags/ record read and archived state. |
| Delivery durability | Yes | Every write commits locally; if a push cannot complete it retries with backoff, and the local commit survives for pibmo-relay flush to send later. |
| Header-injection resistance | Yes | Newlines and control characters rejected in headers; duplicate or unknown header keys refused at parse time. |
| Message confidentiality between members | No | Every clone holds every message and its full history. Git has no path-level access control. |
| Credential and compute isolation | Only under per-member-clone with distinct OS users | Isolates members’ machines and tokens; does not isolate messages. |
| Authenticated sender identity | No | The from: role is self-asserted and checked only against the roster. |
Confidentiality under shared-clone | No | All writers share one group-writable clone; each can read and write the others’ mailboxes. |
See also
- Configuration — the
topology,os_user,remote, andbranchkeys. - Concepts — what the transport store holds and how mail is addressed.
- Getting started — installing an instance.
- Onboarding — the guided setup that produces
team.json. - Architecture — the transport engine and the invariants it enforces.
- Troubleshooting — recovering from a guard refusal or an exhausted push.
Verified against pibmo-relay at commit ca19ce6 on 2026-08-09. The tool moves; if a
command here disagrees with the one on your machine, the tool is right.