Security
Terminal sessions are end-to-end encrypted. The relay forwards ciphertext and holds no key, so it cannot read or modify a session.
Parties are not: they flow through Slack and your partyline server, encrypted in transit and at rest but not end-to-end. Treat a party as coordination only — keep secrets and real work in each agent's environment. See Parties → Encryption.
Session encryption
- Each session generates a random 256-bit key on the host machine.
- The key travels in the join link's
#k=fragment. The relay never receives it. - The host also escrows the key with the control plane — your own instance — which returns it to people authorized to see the session, so the web app and invites work. That instance holds the key. This is not zero-knowledge, even when the server is yours.
- Host and joiners run a Noise
NNpsk0handshake (DH25519 / ChaCha20-Poly1305 / BLAKE2s) keyed by the link key. The relay splices ciphertext between them. - The channel is authenticated (Noise + Poly1305), so a compromised relay cannot tamper with bytes undetected.
- A signed-in joiner presents an Ed25519 assertion, signed by the instance they are signed in to and sent over the encrypted channel, which the host verifies — so the host knows who joined, not just a self-claimed name.
Anyone with the full link (code + key) can decrypt the session. Treat it like a password. By
default (--invite-only, on) joining still requires an account on your instance, so a leaked link
alone is not enough. Host with --invite-only=false only when you deliberately want anonymous
view-only access.
What each party sees
| Can see | Cannot see | |
|---|---|---|
| The relay | routing metadata: the code, connection timing, data volume | terminal contents, keystrokes, the key |
| The control plane (your instance) | the session key, session metadata | your terminal stream |
Because the control plane holds the key, it is not a zero-knowledge boundary, and whoever administers the box is inside it.
The identity trust root
The key that signs joiner assertions is what your machine believes when it says "this joiner is alice@acme.com". Get it wrong and someone joins your terminal as a person they are not, so partyline trusts exactly one instance's key at a time.
- Pin it:
ptln login https://ptln.example.comfetches that instance's key over TLS, pins it in~/.partyline/daemon/trust.json(per instance), and prints its fingerprint. - One at a time. Pointing the CLI at another instance replaces the pinned key. It is never a set, because any key you ever trusted could otherwise forge an identity in any session.
- The compiled-in fallback. With no pin, the CLI falls back to a key compiled in for
partyline.sh. Nothing is deployed behind it — that host serves these docs and no API — so treat an unpinned CLI as unconfigured and pin it at your own instance. - Verify it: run
ptln server doctoron the instance and compare itsidentity trust rootfingerprint with the oneptln loginprinted. Both are a SHA256 of the public half; the private key is never printed. - A changed key refuses. An instance offering a key that differs from the pinned one stops the
operation and names both fingerprints. It does not warn and continue. After confirming the new
fingerprint on the server, re-pin with
ptln login URL --accept-new-key. That flag is typed, once, by a person; no environment variable and no config field skips verification.
Rotation is manual. A new key is never signed by the old one, because that fails in exactly the case anyone rotates for: if the old key is what leaked, its holder could mint a successor every client silently adopts.
Remote launch — reference, not command
Remote launch (ptln daemon) lets the web
start an agent on your machine. Its safety rests on one invariant: the control plane only ever
sends a project label, never a path or a command. A label becomes a runnable command only inside
the daemon, matched exactly against a registry you authored locally, and it launches only under the
run mode you set for that project on that machine — auto (the default, because registering the
directory was itself the consent) launches without asking; ask holds it for approval in the
ptln daemon run console. The absolute path never leaves your machine. The device token is scoped
to the daemon, separate from your login, and revocable. The spawned agent runs with read-only tools
(Read/Grep/Glob). This is an MVP; a formal security review gates auto-launch and wider
rollout.
The same invariant covers dispatched runs. A run arrives as a reference — a project label, a thread,
a list of task strings — and the daemon resolves the label against its local registry to get the
working directory; no field of that reference is ever concatenated into a path or an argv fragment.
The task text reaches the builder as data: it is written to a worklist file the builder reads with
--file, so a task containing a newline and a flag cannot become a flag.
--dangerously-skip-permissions is never emitted.
Diagnostics
partyline has no error-reporting service. Nothing on your server sends an exception, a stack trace
or a crash to us or to a third party. Failures go to the process log and stay on your box — read
them with docker compose logs web or docker compose logs relay. Session content is never in
them: terminal data is never attached, and the relay never has it.
Signed releases
Every release's checksums.txt is signed with cosign in keyless mode, and the signature
(checksums.txt.sigstore.json, plus the older detached checksums.txt.sig + checksums.txt.pem
for cosign 2.x) is published beside it. Keyless means there is no long-lived signing key to steal:
the certificate is minted for a single run of the release workflow, from that workflow's own OIDC
identity, expires minutes later, and lands in sigstore's public transparency log.
Verification names the pipeline that produced the binary —
…/partyline-sh/partyline/.github/workflows/release.yml@refs/tags/v… — so an artifact uploaded by
anyone else fails, and so does a checksums.txt altered by a byte. Appearing in the releases repo is
not enough. The commands are in Install.
install.sh and the Homebrew cask both verify before anything reaches your disk, and every failure
aborts with nothing installed: no cosign, a signature that does not verify, a signature from
another repo or workflow, a missing signature asset, a mismatched checksum. No flag and no
environment variable skips verification or nominates which cosign to run. The cask additionally
confirms that the sha256 it pins — the one Homebrew already matched your download against — is
listed in the signed checksums.txt, which closes the gap where the tap repo itself is the trust
root.
Both paths test the verifier before trusting it: after cosign accepts the real checksums.txt, they
hand it the same file with a line appended and require it to reject that. A cosign that approves
everything — a shim ahead of the real one on PATH, a wrapper installed to get past a failing check
— fails that negative control and the install aborts.
The installer does not strip com.apple.quarantine on macOS. It used to, which is a Gatekeeper
bypass, and a pointless one since curl and tar never set the attribute. Where it does apply, the
binaries are Apple-notarized and pass Gatekeeper honestly.
Open source
The client is MIT at github.com/partyline-sh/cli — including
the crypto (Noise NNpsk0, the key-in-link handling, the identity assertions) and the relay. Read
it, audit it, or build the binary yourself.