sessionpipe

sessionpipe protocol v1

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).

1. What this is

A coding-agent harness (Claude Code, Codex, Gemini CLI, Antigravity, Cursor, …) fires hooks as a session runs. sessionpipe turns those hooks into a small, uniform stream of events about the session — started, working on a tool, needs you, ended — filtered to a privacy tier the person chose per destination, and delivers them to any receiver over HTTP (or to a file, or to stdout). A receiver may send control messages back (answer a permission prompt, queue a message for the next turn).

The protocol carries facts and timestamps. It never carries a verdict: what "stale" means, when a session that never said goodbye is "done", and what a session is about are a receiver's readings, made from the facts.

This document defines the envelope, the session block, the event vocabulary and the tiers. HTTP.md is the wire binding, PRIVACY.md the tiers and rulesets in detail, CONTROL.md the channel back, ADAPTERS.md the per-harness mapping, VERSIONING.md how this changes.

2. Envelope

Every event is one JSON object with exactly these top-level members. The normative shape is schema/v1/event.json, generated from the reference implementation's types.

{
  "protocol": 1,
  "id": "01K6ABCXYZ0123456789ABCDEF",
  "type": "tool.ended",
  "time": "2026-09-28T01:03:11.204Z",
  "tier": 1,
  "harness": { "name": "claude-code", "version": "2.1.290", "event": "PostToolUse" },
  "session": {
    "id": "8c132906-8c3f-4814-870a-afc3d05e1d2e",
    "seq": 412,
    "machine": "misha-air", "cwd": "~/spacesheep",
    "repo": "github.com/micmmakarov/spacesheep", "branch": "main",
    "model": "claude-fable-5-1",
    "title": "Open-source session hooks", "title_source": "harness",
    "url": "https://claude.ai/code/session_01…", "account_id": "a1b2…",
    "parent_id": null
  },
  "privacy": { "rulesets": ["secrets@1"], "pii": false },
  "data": { "tool": "Bash", "call_id": "toolu_01…", "ms": 1830, "ok": true }
}
Field Type Req. Rule
protocol integer yes 1. Bumped only on a breaking change (VERSIONING.md).
id ULID yes Unique per event. The idempotency key for control acks.
type string yes Dotted, lowercase (^[a-z][a-z0-9]*(\.[a-z][a-z0-9_]*)+$), or the literal native. From §4. A receiver MUST accept and store an unknown type; it MUST NOT reject the batch for it.
time RFC 3339 yes The sender's clock, UTC, millisecond precision (…Z). Receivers MUST also stamp their own arrival time.
tier 0–3 yes The tier the event was filtered to. A receiver MUST answer 403 to an event above its max_tier and MUST NOT silently keep it.
harness object yes name from the registry in ADAPTERS.md (claude-code, codex, gemini-cli, antigravity, cursor, copilot-cli, droid, kiro, opencode; other [a-z0-9-] names allowed); version and event (the harness's own event name) optional.
session object yes §3. id and seq required, the rest optional.
privacy object yes rulesets: the redaction rulesets that ran (secrets@1 MUST be present above tier 0); pii: whether pii@1 ran.
data object yes Per type, §4. Extra keys are allowed; a receiver MUST keep them.

3. The session block

Every event carries the session as the sender knows it at that moment. A hook fired a second after start may know only the id and the folder; the worker learns the title, the model and the link later from the harness's own files. A receiver MUST merge with COALESCE semantics: a later event whose field is absent or empty never blanks an earlier full value.

Field Rule
id The harness's own session id; ≤ 128 chars of [A-Za-z0-9._:-].
seq Integer ≥ 0, monotonic per (harness, session) per sender. With harness.name and session.id it is the deduplication key. Order of arrival is not guaranteed; seq is the order.
machine A name for the sending machine: the hostname unless the person configured one. A sender MUST NOT guess it; an empty field beats a wrong one.
cwd The folder the session started in. The home directory is written ~.
repo The origin remote as a URL with any credentials stripped.
branch The checked-out branch; absent on a detached HEAD.
model The model id the harness reports.
title, title_source The session's name and where it came from: custom (the person named it), harness (the tool's own titler), first-ask (the first prompt, cut). A receiver SHOULD never replace a better source with a worse one.
url A link that opens the session (a Remote Control link, a web remote).
account_id An opaque id for the harness account, never an email.
parent_id For a subagent's own events: the parent session's id. null or absent otherwise.

4. Event vocabulary

Tier is the lowest tier that carries the type; a sink below it never sees it. Per-type data shapes are normative in schema/v1/event-data.json.

type tier data Fires when
session.started 0 source: startup · resume · clear · compact · fork · unknown The harness's session-start hook; for Antigravity, the first PreInvocation of a conversation.
session.heartbeat 0 — At most once per 60 s while a turn is running, for sinks below tier 1 (which get no tool events). Lets a tier-0 receiver show "working".
session.ended 0 reason: clear · logout · exit · other The harness's session-end hook. Four harnesses never fire one (ADAPTERS.md); a receiver infers "done" from silence and MUST present that as its own reading.
session.backfill 0 started_at, last_at, turns? One per historical session found in local files at install. seq is 0 and the session block carries what the files say.
session.forgotten 0 — The person ran sessionpipe forget. A receiver MUST delete everything it holds for the session and answer as for any accepted event.
turn.started 0 turn_id?; tier 1 adds prompt_chars A prompt was submitted.
turn.ended 0 turn_id?, reason: stop · interrupt · error, ms? The Stop / AfterAgent / agentStop hook.
turn.transcript 2 turn (integer), user, assistant (each ≤ 20 000 chars, redacted), at Read from the harness's transcript file by the worker after a turn ends; one event per completed exchange, in order.
tool.started 1 tool, call_id?, turn_id?, agent_id? (set when a subagent made the call); tier 3 adds input Pre-tool hook. OPTIONAL for senders whose harness has none.
tool.ended 1 tool, call_id?, turn_id?, agent_id?, ms?, ok, error?; tier 3 adds input, output (each ≤ 64 KB) Post-tool hook. A tool name is [A-Za-z0-9_.:-]{1,120}; MCP tools keep the harness's own spelling.
files.changed 1 paths[] (repo-relative or ~-relative) File-edit hooks (Cursor, Kiro, Goose), or paths lifted from a write tool's input.
attention.needed 0 attention_id, kind: permission · question · elicitation · idle · error; tier 1 adds tool, message (redacted, ≤ 200) PermissionRequest, Notification, permission.asked. Tier-0 receivers learn "needs you" without the words.
attention.cleared 0 attention_id, how: answered · cancelled · timeout · unknown The next event that proves the prompt was resolved (a tool ran, a turn ended), or a control answer.
subagent.started / subagent.ended 1 agent_id, agent_type? Subagent hooks; the subagent's own events carry session.parent_id.
context.compacted 1 trigger: manual · auto Compaction hooks.
native 3 the raw hook JSON Only on the native lane (HTTP.md §6); never emitted by the client.

Rules for all types:

5. Tiers

What each tier carries. The full rules, the rulesets and guidance are in PRIVACY.md.

What 0 1 2 3
Session id, state events, timing ✓ ✓ ✓ ✓
Machine, cwd, repo, branch, model, title, url, account_id ✓ ✓ ✓ ✓
Attention kind ✓ ✓ ✓ ✓
Tool names, durations, ok/error — ✓ ✓ ✓
File paths touched — ✓ ✓ ✓
Attention message, prompt length, subagents, compactions — ✓ ✓ ✓
Prompts and assistant text (turn.transcript) — — ✓ ✓
Tool input and output, thinking, raw lines — — — ✓
Secrets ruleset n/a always always always
PII ruleset opt-in opt-in opt-in opt-in

Filtering is a pure function. filter(event, tier, pii) returns the event with fields removed and strings redacted. It removes by allowlist per type, never by denylist, so a field the spec does not name at a tier is not sent at that tier. The same function runs in the conformance runner, so a receiver can prove what a tier-N event may contain.

Tier per sink, capped by the receiver. A sink is configured at a tier; the receiver's well-known file declares max_tier; the sender uses the lower.

6. Invariants a sender keeps

  1. Never lose, never duplicate. The outbox is append-only; a sink's cursor advances only on a 2xx; every event carries seq; a receiver deduplicates on (harness.name, session.id, session.seq). An outage costs delay, never an event.
  2. Secrets never leave. secrets@1 runs on every string above tier 0 before an event reaches the outbox, and cannot be turned off per sink.
  3. The hook path is invisible. The process a harness spawns reads stdin, writes one job file, spawns a detached worker and exits 0; it writes to stdout only what the harness requires. Nothing in this protocol may block or alter the harness.
  4. No telemetry. A sender reports to the sinks the person configured and to nobody else.

7. Conformance

A sender or receiver is conformant when it passes the fixtures under conformance/: adapter fixtures (real hook payloads → expected events), redaction vectors, and delivery scenarios (HTTP.md §7). @sessionpipe/conformance runs them.