concepts
Backend contract
Pheebs holds a base URL and a token.
Pheebs is backend-agnostic. The client holds a base URL and a per-developer token, and nothing else. Anything that answers the four required routes is a Pheebs backend, and openapi.yaml at the repo root is the normative schema.
This page covers what a schema cannot express: why each route behaves the way it does, and the failure semantics a correct implementation has to honour.
Point a client at it
pheebs config set base-url https://pheebs.acme.dev
pheebs config set token <token>Both, or nothing is sent. config set token blocks on /validate-token, so that route has to answer before a developer can finish setup. Everything after that runs on the hook path and never blocks the agent.
The routes
| Route | Auth | Blocking? |
|---|---|---|
| POST /ingest | Bearer <token> | No, fire and forget |
| POST /validate-token | none | Yes, at token-set time |
| POST /classify-prompt | Bearer <token> | Yes, roughly a 2.5s budget |
| POST /otel/v1/{traces,metrics,logs} | Bearer <token> | No |
| GET /insights | Bearer <token> | Optional |
POST /ingest
One event envelope per request. Seven fields are always present, and everything else is per-event and open-ended.
| Field | Type | Notes |
|---|---|---|
| timestamp | string | RFC 3339 date-time |
| source | string | Always hook |
| event | string | One of 17 names, from session_started through artifact_found |
| codebase | string | PHEEBS_CODEBASE_ID if set, else org/repo from the git remote, else local/<folder>, else the literal unknown |
| ai_tool | string | claude_code, cursor, or codex |
| session_id | string | The agent's own session id. See the warning below |
| version | string | The Pheebs client version that produced the row |
Status codes that mean something
Only two matter to the client:
401and403: the token was rejected. The client writes~/.pheebs/.last-transport-error, whichpheebs doctorsurfaces to tell the developer to re-set their token.422: unprocessable envelope. The client writes one line to stderr and moves on.
Everything else, including 5xx, is swallowed. A connection-level failure prints one stderr line and is otherwise ignored. The client does not retry and does not queue: the local JSONL is the durable copy, and a hook must never block the agent. Do not design around client-side retry, because there is none.
POST /validate-token
The one unauthenticated route, because the token being validated travels in the body: { "token": "..." }. Return allowed plus, when allowed, id, developer, tenant, opt_out, and prompt_collection. On rejection, return a reason.
{
"allowed": true,
"id": "dev_01H8X",
"developer": "octocat",
"tenant": "acme",
"opt_out": false,
"prompt_collection": true
}Three important notes:
Consent flags
opt_out is advisory in the current client. It changes a message printed by pheebs config set token and nothing else. It does not suppress ingest and it does not gate classification. If you mean to stop capture for a developer, enforce it server-side.
prompt_collection is the consent gate for the next route. It is cached locally when setting up the token and never refreshed automatically, because nothing on the hook path revalidates it. Revoking consent server-side does not reach a machine until its developer re-runs pheebs config set token, so enforce consent again on every /classify-prompt call. That is the only gate that actually revokes.
POST /classify-prompt
Takes { "prompt": "..." } and returns a verdict plus a single intent label:
{
"allowed": true,
"result": {
"label": "pushback",
"requests_verification": true,
"classifier_version": "2026-09-01"
}
}label is the only required field inside result. It lands on the event as prompt_intent. The client copies label, requests_verification, and classifier_version, and drops everything else.
This is the only route that receives raw developer content, and it carries four obligations the others do not:
- Answer within 2500 ms. The client aborts at that point and degrades.
- Enforce consent again, server-side. The client checks the token's
prompt_collectionbefore sending, but a client check is not a control. Returnallowed: falseand the client recordsunclassified.
Every degraded path lands identically, with no error surfaced and nothing blocked: a non-2xx, an allowed: false, a missing result.label, or a timeout all record prompt_intent: "unclassified".
result.task_scope
Optional and additive: mechanical, bounded, cross_cutting, open_ended, or n/a. It sizes the work a prompt asks for, and only task-shaped and repair-shaped prompts get sized. A backend that does not size tasks omits the field and stays conformant.
POST /otel/v1/{signal}
An OTLP/HTTP passthrough, where {signal} is traces, metrics, or logs. The agent's native OpenTelemetry exporter points here with the Pheebs token as the bearer, and the proxy swaps in the real upstream credential before forwarding.
GET /insights (optional)
What one developer can see about their own work. The response is a map of named sections which follow the AI Proficiency Model: repertoire is Layer 1, judgement_signals is Layer 2.
Identity comes from the token, as everywhere else. The route returns the caller's own data and never anyone else's.
Build one
Start from examples/backend-node/server.js. It implements all four required routes in around 150 lines with no dependencies, answers /insights with enabled: false, appends to JSONL, and runs with node server.js:
node examples/backend-node/server.js
pheebs config set base-url http://localhost:8787
pheebs config set token pheebs_localThe full prose rationale lives in docs/backend-contract.md, and the schema that governs every field is openapi.yaml.