What is logged
The exact fields kept per call, where a no-key caller's address lives and for how long, and what never reaches the server.
An agent sends this server your questions, so the honest question is what survives the request. The list below is the whole of it, taken from apps/mcp/src/telemetry.ts and the hourly job that folds the log.
Per call
| Field | Example | Why it exists |
|---|---|---|
at | 2026-09-08T14:02:11.913Z | Groups the fold by day. |
client | claude-code | The name the client announces in the MCP handshake. |
clientVersion | 2.0.14 | Same handshake. Tells us which builds are in use. |
tool | recommend | Which of the eight tools was called. |
scope | anonymous or org | Whether the caller had a key. Not which key. |
charsOut | 742 | Characters written back, counted as they stream. |
ms | 38 | How long the request took. |
An entry is written only for initialize and tools/call. Notifications and anything else are dropped before the log.
Entries go to a Redis list capped at 100,000. Once an hour a job folds them into one Postgres row per day, client, client version, tool, and scope kind, holding the call count, the sum of characters returned and their median. The organization id does not survive that fold; scope in the stored row is the word org, not an identifier.
Never stored
Prompts, file contents, and code are not stored, and the reason is stronger than a policy: the tools never receive them. recommend takes a task description you wrote, traps takes a package name and a version, design takes a screen description. Nothing in any argument schema is a file, no argument value is written to the log or the database, and no tool reads from disk. The Claude Code hook that prints traps before a dependency edit parses the package names out of the command or the manifest on your machine and sends only those names in a query string; the diff and the file stay local.
No-key callers
Counting 60 calls a day per IP needs the address. It lives in one Redis counter key, ww:cap:ip:<address>:<date>, which is set to expire at midnight UTC on the day it belongs to. A second key records the addresses inside a /24 that reached the cap, so one noisy network cannot be split across a thousand free allowances; it expires on the same schedule.
Nothing writes the address to Postgres. The hourly rollup replaces it with sha256(salt + day + address), where the salt is a server secret and the day is part of the input, so two rows for the same address on different days cannot be matched to each other.
Keyed callers are counted by organization id and never by address.
Submissions
A submit_trap call is different by design. The library, version, symptom, fix and evidence URL you send are stored with the id of the key that sent them, so a reviewer can come back to you about it. If the trap is published you can choose to be credited by name.
Errors and analytics
An unhandled error is reported to our own Bugsink instance with the exception type, the message and the stack, plus the service name. Request bodies and tool arguments are not attached. The website uses Cloudflare Web Analytics, which sets no cookies.
Retention
Usage rows are kept for 13 months, which covers a year of billing plus the month you might ask about it. Accounts are deleted on request within 30 days. Servers are in the European Union behind Cloudflare, and backups are encrypted in Cloudflare R2. The full policy is on the privacy page; questions go to support@wellworn.dev.