Skip to content

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 clonesOne per memberOne, shared by all writers
How writes reach othersPush 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 controlEach member commits in its own clone; the rebase-before-push loop reconcilesA single exclusive lock serializes writers on the one clone
OS usersMay be distinct; each member can run as its own operating-system userMust be a single operating-system user
IsolatesCredentials and compute between members, plus the transport history each member holds locallyNothing 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-mail
install error: SECURITY GUARD REFUSAL: 'topology: shared-clone' is incompatible
with distinct operating-system users ['alice', 'bob', 'dev']. Shared local clone
directory permissions dissolve credential isolation and allow direct inbox
modification across users. Use 'topology: per-member-clone' for multi-user
isolation.

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-mail
install error: SECURITY GUARD REFUSAL: 'topology: shared-clone' requires proof
that every member is the same operating-system user, and that cannot be
established for ['builder', 'planner']. A shared clone gives every member the
file permissions to read and write every other member's mailbox, so this is
admitted only when it is known to be one user -- an unstated user is not assumed
to be yours. Fix it by adding "os_user" to each member in team.json, or by
setting 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 who
store: ~/code/acme-app-mail
upstream: origin/main
topology: shared-clone
identity: (unresolved)
roster:
planner active Plans the work
builder active Writes the code

If 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 distinct
operating-system users ['alice', 'bob', 'dev'], so the transport must be
authenticated -- but 'origin' resolves to a local path (...). A local bare
repository shared between operating-system users cannot deliver credential
isolation: making it writable by every member also makes its hooks/ writable, so
a planted post-receive hook runs as the pushing user, and the same permissions
allow force-push and forgery of another member's mailbox beneath the address
allowlist. Use ssh to a dedicated non-member account that is login-locked to a
forced command with a distinct key per member, or a private git host that
authenticates 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

PropertyGuaranteed?Notes
AddressingYesClosed roster allowlist; mail to a non-member is refused.
Read-trackingYesPer-member append-only marker files under roles/<role>/flags/ record read and archived state.
Delivery durabilityYesEvery 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 resistanceYesNewlines and control characters rejected in headers; duplicate or unknown header keys refused at parse time.
Message confidentiality between membersNoEvery clone holds every message and its full history. Git has no path-level access control.
Credential and compute isolationOnly under per-member-clone with distinct OS usersIsolates members’ machines and tokens; does not isolate messages.
Authenticated sender identityNoThe from: role is self-asserted and checked only against the roster.
Confidentiality under shared-cloneNoAll writers share one group-writable clone; each can read and write the others’ mailboxes.

See also

  • Configuration — the topology, os_user, remote, and branch keys.
  • 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.