Get started
Markdownllms.txt

Privacy: tiers and rulesets

On this page

This document is part of the sessionpipe protocol and is licensed under CC BY 4.0. The words MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are used as in RFC 2119. Status: Draft (see VERSIONING.md).

Sessions are personal. A prompt names colleagues, a path names a client, a pasted config carries a key. The protocol's answer is data minimisation by construction: a sink gets a tier, the tier is an allowlist, and two rulesets scrub what remains.

1. Tiers

Tier Name Carries
0 presence Session id, state events (session.*, turn.started, turn.ended, attention.* with only attention_id and kind, delegation.* with who asked whom and how it went: ask_id, via, harness, to, ok, ms, error, relayed), timing, machine, cwd, repo, branch, model, title, url, account_id, attended, launched_by, delegated_by. Enough for "working / needs you / done" and a link to open the session. turn.started carries submitted_by and injected: who started the turn, and whether its prompt was the harness's own block — never any of the prompt's words, but injected is one bit read off the prompt's first characters, where the prompt's bare length (prompt_chars) is tier 1. submitted_by is a bare word (PROTOCOL.md §4) because tier 0 is not scrubbed by secrets@1.
1 actions + tool names, call_id, durations, ok/error text, files.changed paths, attention tool, message (redacted, ≤ 200 chars) and agent_id, prompt_chars, subagents, compactions, the account's usage limits (limits.observed: percentages, reset times, plan), what an ask asked (delegation.started.text, redacted, ≤ 200 chars). Enough for an activity feed.
2 conversation + turn.transcript: the person's prompts and the assistant's text, redacted, ≤ 20 000 chars a side, turn.progress, the same for a turn still running, and what an asked agent answered (delegation.ended.reply, redacted, ≤ 20 000 chars). Enough for memory, search, recaps.
3 full + tool input and output (≤ 64 KB each), thinking where the harness exposes it, the native lane's raw lines. Full replay; meant for a receiver you own.

Rules:

  • Allowlist per type. filter(event, tier, pii) keeps, for each type, only the fields the table in PROTOCOL.md §4 names at that tier or below, and drops the whole event when its type's tier is higher. It never works by removing named fields from an unknown shape. A field this spec does not mention at a tier is not sent at that tier.
  • Per sink. A person may send tier 0 to a team board and tier 2 to their own memory. The tier is a property of the sink, not of the machine.
  • Capped by the receiver. max_tier in the well-known file wins when lower. A receiver MUST 403 an event above its cap; it MUST NOT quietly keep it.
  • Default is 0. A sink added by hand without --tier is tier 0. The installer asks once per sink, shows what each tier sends, and remembers the answer.
  • Every event says what ran. privacy.rulesets and privacy.pii are on every event, so a receiver can tell an unredacted native-lane row from a client row.

2. Rulesets

secrets@1 (always, above tier 0)

Runs on every string an event carries above tier 0, before the event reaches the outbox. It cannot be disabled per sink. It removes the value and keeps the sentence: password: [redacted]. What it matches:

Class Examples
Vendor token shapes ghp_…, github_pat_…, glpat-…, xox[abposre]-…, Slack and Discord webhook URLs, sk-… (OpenAI, Anthropic, DeepSeek, OpenRouter), sk_live_ / rk_test_ (Stripe), whsec_…, AIza…, ya29.…, GOCSPX-…, AWS access key ids, xai-, gsk_, pplx-, hf_, pcsk_, npm_, pypi-, SendGrid, Shopify, DigitalOcean, Linear, Notion, Supabase, Figma, Doppler, Granola, spacesheep ss_, Telegram bot tokens, JWTs, PEM private-key blocks (including one cut off mid-paste).
URL credentials scheme://user:password@host → the password goes, user and host stay.
Authorization headers Bearer, Basic, Token followed by a credential.
Assignments NAME=value, NAME: value, NAME => value where NAME names a secret (DB_PASSWORD, apiKey, client-secret, *_KEY, *token* …) — but not password_hash, tokenizer, max_tokens: 4096, and not a bare pass: (a count after "Verification pass:" is prose, not a credential). Only a secret name may consume a value, so https: never swallows a ?token= behind it.
Prose "password is Hunter2!", "the token was …", "set the password to …" — only when the value looks generated (letters and digits, or three character classes), so "the password is wrong" stays.

Placeholders are left alone: <your-key>, ${VAR}, process.env.X, string, [redacted], a member path, a call. The rules are linear (bounded repetition, no nested quantifiers); the adversarial vectors pin that a 50 000-space run or password_ × 5000 returns in milliseconds. The ruleset is idempotent.

Vectors: conformance/redaction/secrets.json. The reference implementation is packages/core/src/privacy/secrets.ts; every other copy (a server-side re-check, a CLI) MUST import it or run its vectors.

pii@1 (opt-in, any tier)

A second pass, on by --pii per sink. It reduces identifiers, it does not promise anonymity:

Rule Effect
Email addresses → [email]
Phone numbers (E.164 and common national forms) → [phone]
Home directory paths → ~ (already the rule for session.cwd; here applied inside every string)
session.machine → the first 8 hex characters of its SHA-256
session.account_id, and limits.observed's account_id dropped

Vectors: conformance/redaction/pii.json.

Tiers and rulesets govern events. The control lane is not a sink: it talks only to a receiver the person paired with by approving it with their passkey, and its pairing request and hellos may name the machine's harness accounts with their emails, or an API endpoint's host for a home with no login (accounts, CONTROL.md §2), so that receiver's board can label session.account_id. No event carries an email or a host for that.

3. What a sender never reads

An adapter uses a harness's documented hooks API and reads files the person's own harness wrote on the person's own machine (its transcript, its session registry, its config's account id, or the API endpoint its settings name — that one value, never the token beside it). It never reads a credential file, never calls a vendor API, and never sends a token of the harness's anywhere. ADAPTERS.md states this per harness with the documentation URL.

One exception is the person's own to make, and off until they do: with remote_titles on (sessionpipe titles on), the Claude Code adapter reads Claude Code's sign-in on this machine to ask claude.ai for a session's name, the one fact claude.ai never writes to the machine. The token goes to api.anthropic.com alone, for that read; it is never stored, logged, put in an event, or sent to a sink. Only the name comes back, as session.title.

The other exception is the account pool, and only after the person turns it on (CONTROL.md §6 pool, ADAPTERS.md Pool). A person with several accounts for one harness may run sessionpipe pool on and pool add at the machine's own terminal. From then on, and only then, the machine's daemon holds the credentials of the accounts the person added (copied from the harness's own login with pool add --import, or signed in with pool add --login), in sessionpipe's own secrets store, and sends each one only to that vendor's own endpoints, only for the person's own requests (the harness's requests, which it routes, and the vendor's usage and token-refresh calls that keep those accounts usable). Nothing is read, called or sent before pool on or pool add; pool off removes what pool on wrote; pool remove deletes the credential. No credential, no token and no email reaches an event, an ack, a log or a receiver: a receiver learns the accounts' ids, labels and emails only in the hello, as it does the harness's own accounts above, and their usage only as limits.observed. Nothing here extends to any other harness, to any other vendor, to any other endpoint, or to anything install or connect does.

4. For receivers

This section is guidance, not legal advice.

Session events can be personal data: names in prompts, an account id, paths that name a client or a colleague. If you operate a receiver for anyone but yourself, you are the controller of what it stores.

  • The tiers are your data-minimisation tool. Declare the lowest max_tier your purpose needs. A receiver serving more than its operator SHOULD declare max_tier ≤ 1.
  • session.forgotten is the deletion path. Honour it fully (events, derived data, backups on their schedule) and say in your documentation how long that takes.
  • Document retention. Say how long events are kept and delete on schedule.
  • Access requests can be answered from the per-session file: the protocol's storage unit is one session's events, in order.
  • Secrets can still arrive. secrets@1 is good, not perfect; treat every stored string as though it might contain one. Never log event bodies.
  • The native lane carries unredacted data. Do not enable it on a receiver other people send to.
  • launched_by can name a session you never saw. The launcher and the session it started may report to different sinks. The id is opaque: it says the two are related and nothing else.

5. What sessionpipe itself never does

  • No telemetry: the client reports to the sinks the person configured and to no one else. The website has no analytics.
  • No hosted receiver: the project operates no server that receives anyone's data.
  • The optional daily version check is a plain read of the npm registry, off with SESSIONPIPE_NO_UPDATE_CHECK=1.