Skip to Content
Plural

Security

The threat model — what a guest, a host, a relay and a team member can and cannot do.

Plural's one rule is that the host always decides. This table is the threat model behind that rule: each row is a threat, and the mitigation actually in the code.

ThreatMitigation in code
Guest runs commands on hostTool calls always need host approval; a run containing a guest prompt is tainted in both prompt modes
Guest floods the agent with prompts or spends the host's budgetreview mode makes the host approve every prompt; viewers cannot prompt; host aborts with /abort
Guest reads or changes files outside the session folderFile tools refuse paths outside the workspace; a run with a guest's prompt can reach a folder only after the host answers a red card with read or read + write, asked every time and never remembered; a read-only grant cannot write; protected paths stay denied inside a grant. bash is not covered: it is unsandboxed and relies on host approval
Prompt injection via guestProvenance taint forces ask for write/exec
Compound shell bypass (ls; rm)Allow rules ignored for compound commands → ask
Session always overriding denyBase deny rules are evaluated first and cannot be overridden
Compromised relayHost signs tokens and checks permissions; relay only routes
Token theft24h TTL, room binding, token bound to connection, revoked participants removed from state
API key typed in the client reaches the wrong partyKeys go only through the local stdin pipe between a client and the host it started, never over the protocol or a relay
Claude OAuth token exposed to a participantOAuth tokens stay in the host's owner-only local data file and are never sent over the room protocol or relay
Secret leakagePath deny-list, env scrubbing for bash, redaction before append
Agent badgering the host with variants of a rejected actionRejection ledger per run and class, Block (session) deny rule
Agent or guest widening its own permissionsPolicy rules are events changed only by host commands; an allow rule never overrides a built-in deny or the guest taint; rules are never read from the workspace the agent can write
Laundering injected text through a conversation summaryThe summary is presented to the model as untrusted notes, runs keep their merged provenance, and write/exec by guest-originated turns still ask
A third-party skill steering the agentOnly the host installs skills: by command, or by approving the agent's skill_install like an edit, which plan mode refuses; archives are size-capped, links are skipped and paths may not leave the skill folder; the skill tool reads only inside an installed skill and keeps the secret deny-list
An MCP server acting with the host's credentialsOnly the host adds or enables servers; every MCP call is an exec that asks the host, keeps the guest taint and is refused in plan mode; server env and headers stay in the host's files and are added to the redactor; tool hints like readOnlyHint are not trusted
A leaked room link lets a stranger into the roomThe link only lets a person ask: by default the host answers each request, the person sees nothing of the room until then, at most five wait for two minutes each, and /invite revoke replaces the code; with admission=open the link is a bearer secret and the docs say to keep it private
A viewer spams the host with requests to writeA person has one request at a time, waits five minutes after a refusal, and only a few can wait at once
A guest answers a request for the host, or sees who else askedaccess.decide is host only, and a request and its answer go only to the host and the asker
A person let write gets the agent to do more than a guest couldNothing changes for the agent: their prompts are guest-originated, write and exec still ask the host, and review mode still holds every prompt
A stranger reads who is knocking, or answers for the hostRequests, their resolutions and the waiting list go only to the host, and join.decide is host only
A private message read by another guestThe host delivers a chat.posted with a recipient only to its author and its addressee: live events, history, replay and the snapshot are filtered per connection; the host, its log and the relay can still read it, so it is private from guests only
Chat text steering the agentChat events are not part of the conversation the agent is built from, so no chat text reaches the model
A stranger lists or opens a team's sessionsThe directory answers only a valid member certificate plus a signature over a fresh nonce, and a host checks both against the team key it pinned
A member shows a card under someone else's name, or takes over their roomThe directory writes the owner of a card from the member's certificate, never from the card, and refuses a room that already belongs to another member
A member floods the directory with cards or requestsA card frame is at most 8 KB, a member can show 5 sessions, at most 2 card frames a second per connection, request bodies up to 16 KB, and a card not renewed within a minute is dropped
A removed member keeps showing sessionsRemoval drops their cards at once and the next frame on their open connection closes it
A stranger walks into a team session with a copied certificateThe join carries a signature made with the member's key over the room, a timestamp and a nonce; a certificate of another team, an expired one, a wrong room, a stale timestamp and a repeated nonce are all refused
Someone the team removed keeps a seat in a sessionThe host checks the team roster before admitting a team member and again whenever a team member reconnects with a token; the roster is cached for 30 seconds, a failed refresh is forgiven for 5 minutes and then the host refuses
A kicked team member comes back with their certificate/kick bans the member for the rest of the session and the ban is stored with the session
A team member in a session that is not shared with the teamOnly a host that publishes to the team (the default inside a team, off with --private) takes a team join; any other host refuses it
A teammate reads what my agent changedA team session publishes the paths its agent edited in the last 30 minutes and the repository's address without credentials to the team; a private session publishes nothing, and file contents, prompts and shell commands never leave the host
A card claims another repository or other files to cause false warningsThe owner of a card comes from the certificate, a warning is only advice and blocks nothing, and a path outside the workspace is not recorded
A briefing carries instructions that steer the receiving agentThe briefing is put to the model as notes from a colleague that nobody checked, with a header saying so; the person reads it and must /ask give it; and after that every run of the session counts as guest-originated, so writing and running anything asks the host even in auto mode, also after a compaction and a restart
A briefing leaks what the owner did not want sharedThe owner agrees to each request unless they chose team, can switch it off, and the facts come from the tool log, not the model; secrets and paths on the deny list are left out, the text goes through the redactor and is capped
A question steers the owner's modelThe question is quoted to the model as a question to answer, never as an instruction, the call has no tools, and the answer is only text that the asker reads
Someone floods the owner's model with requestsOne request per person at a time, a minute between them, a few waiting at most, and only the host's agreement starts a call
A teammate who runs a fake session replays the proof a member made for it to a real hostThe proof signs the address it was made for as well as the room, and a host accepts only its own address, so what a member signed for one server is useless at another
A member removed from the team keeps watching a session they are already inThe host asks the team roster every 30 seconds and revokes and disconnects connected team members who are no longer in it
A host that runs for weeks loses its place in the teamThe host renews its certificate while it runs and reconnects with the new one
A peer's name steers the receiving modelThe source of an imported briefing may only be letters, digits, spaces and a few name characters, and the TUI uses the name in the member's certificate, not the one the remote host declares
A stolen member certificateThe certificate alone is useless without the member's private key, it expires, and removing the member revokes it
A compromised directory adds a memberThe new member can at most become a viewer of sessions that admit team viewers; writing always needs the host to approve access.request
A viewer reads what the owner did not shareSessions are private and unpublished by default; the card holds no prompts, replies or file contents, only names, repository, status and changed paths
Context shared between agents steering the other agentA shared summary reaches the model as untrusted notes, keeps the guest taint and never changes the system prompt
Slow/flooding clientsBounded queues, rate limit and size limit on relay
A chat service, or someone with the import link, pushes text into the agent's contextThe address carries a secret made by the host and kept in its state directory; the MCP gateway (a separate service) keys hosts by its hash, caps size and open offers and stores nothing, and the host connects out to it; what arrives waits for the host's answer on a card and goes to the agent's notes as background with secrets removed, never as instructions

Out of scope for MVP

End-to-end encryption (the relay sees content) and Docker isolation.

See the source for the exact modules behind each row, starting with core/roles.py, core/policy/engine.py, host/policy_gate.py and relay/presence.py in the Plural repository.