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:
- A sender MUST emit at least
session.started(orsession.backfill) before any other event of a session, and SHOULD emitturn.started/turn.endedfor every prompt its harness reports. - A sender MUST NOT emit a type at a tier below the type's tier (a
tool.endedin a tier-0 event is a protocol error; a receiver MAY reject it with reasontier_type). attention_id,turn_id,call_id,agent_idare opaque strings the sender chooses; when the harness provides one (tool_use_id,prompt_id) it SHOULD be used as is.
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
- 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. - Secrets never leave.
secrets@1runs on every string above tier 0 before an event reaches the outbox, and cannot be turned off per sink. - 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.
- 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.