Type: RFC Status: Accepted Systems: Gateway, Sources, Config, Plugins, Privacy, Observability Author: Phil / Claude Date: 2026-08-17 Related: LLP 0231, LLP 0242, LLP 0243, LLP 0244, LLP 0027, LLP 0030, LLP 0032, LLP 0049, LLP 0085, LLP 0103, LLP 0012, LLP 0015; formerly numbered 0245, ceded to PR #815 (2026-08-17) Tracker: #798 (implementation spec) Spawns: LLP 0252, LLP 0253, LLP 0254, LLP 0255, LLP 0256, LLP 0258 (decisions), LLP 0257 (spec). All accepted with this RFC, 2026-08-17.
Claude Code now ships a sanctioned, documented export path for everything the proxy intercepts: OTEL telemetry events plus raw API body files (
OTEL_LOG_RAW_API_BODIES=file:<dir>). Attaching via that path needs no CA in the keychain, noHTTPS_PROXY, no launchd env, and no terminal restart: oneenvblock written into~/.claude/settings.jsonreaches every session, however it is launched. This RFC proposes replacing proxy attach for theclaudeclient with an OTEL attach, while the gateway proxy remains for every other client.
Proxy-mode capture (LLP 0231, default since LLP 0242 to 0244) exists because
repointing ANTHROPIC_BASE_URL broke Remote Control. It works, but it carries
standing costs that are intrinsic to TLS interception, not bugs to fix:
- A machine-local CA trusted in the login keychain (one dialog at attach, a
detach --purgeobligation at the end of life). HTTPS_PROXYplusNODE_USE_SYSTEM_CAin the launchd env, which never reaches new windows of an already-running terminal app; a full quit and reopen is required and is undetectable from our side (LLP 0231 run G finding).- The daemon sits on the wire for every request: if it is down or wedged, Claude Code's traffic is affected, not just our capture.
- The capture depends on Claude Code not changing its proxy and CA handling, which is behavior we consume but Anthropic does not promise us.
Meanwhile Claude Code's telemetry system (docs: monitoring-usage.md) grew the
missing piece: raw request and response bodies, exportable untruncated to local
files, alongside an event stream that carries identity, cost, and behavioral
data the wire never shows.
Sequencing (settled 2026-08-17): PR #794 shipped the proxy default as a
stopgap. If this RFC succeeds, OTEL attach is the successor migration for the
claude client in a later release; LLP 0244's migration machinery gets an
Extended-by: forward ref on acceptance, not a revert.
Spike run 2026-08-17 on Claude Code 2.1.233, one real session captured simultaneously by the proxy (control) and by a scratch OTLP http/json listener plus body-file spool (candidate):
- Events observed:
user_prompt,assistant_response,api_request,api_request_body,api_response_body,tool_decision,tool_result,permission_mode_changed,mcp_server_connection,plugin_loaded, and (beyond the docs)hook_registered,hook_execution_start,hook_execution_complete. - Every event carried
session.id,prompt.id,user.email,organization.id,user.account_uuid,terminal.type,app.version,app.entrypoint; content events carried full prompt text, full response text, and fulltool_inputJSON (withOTEL_LOG_USER_PROMPTS,OTEL_LOG_ASSISTANT_RESPONSES,OTEL_LOG_TOOL_DETAILSset). - Body files held the complete request JSON: 4 system blocks, 12 tool
definitions, full message history, and
metadata.user_idembedding session id, account uuid, and device id. About 145 KB per request for a trivial session (system prompt and tool definitions dominate). - Thinking parity: the response body carries thinking blocks as
"thinking":"<REDACTED>"with the signature kept. The proxy control capture of the same session stored its 2 reasoning parts with empty text. The wire no longer carries thinking either (checked across August: 7201 of 7201 captured Claude reasoning parts are empty). Neither path loses anything the other has. workspace.host_pathsdid NOT appear on any event, so cwd and git identity do not come from event attributes on a plain local session.
- R1 Field parity. Every
ai_gateway_messagescolumn populated by the proxy path today is populated by the OTEL path, from events, body files, or the retained SessionStart hook. See the parity table below. - R2 More when useful. Net-new data with clear report or graph value is captured, not discarded: tool accept/reject decisions with their source, permission mode changes, active time, per-request USD cost, lines of code, commit and PR counts, user email and org id, terminal type, hook execution, MCP server health, refusals.
- R3 Storage must not explode. Steady-state Iceberg growth stays at or below the proxy path's (the local all-history table is 182 MB today; August is 45,200 rows). The transient body-file spool is bounded by a hard cap with oldest-first eviction; eviction degrades to transcript backfill, never to unbounded disk.
- R4 Privacy seams hold.
.hypignoreand the machine-local list (LLP 0049, 0103), local-only withholding,hyp purge, and per-session ignore all keep working. The policy check runs inline at ingest with cwd in hand; the fail-open window LLP 0085 patches must not reappear. Settled 2026-08-17: transient spool presence of a to-be-dropped session's bodies is acceptable (the same content already sits in~/.claude/projectstranscripts), with three duties: the spool lives under hyp-home with owner-only permissions, ingest DELETES (never merely skips) the bodies of ignored or policy-dropped sessions, andhyp purgeand detach both sweep the spool. - R5 Attach is one reversible write.
hyp attach claudemerges keys into theenvblock of~/.claude/settings.json; detach removes exactly those keys. No PATH shim, no corporate launcher, no keychain, no launchd env, no terminal restart. - R6 Remote Control untouched. The base URL is never repointed and no proxy is set, so the first-party predicate holds trivially. The override keys LLP 0231 documents are not needed.
Attach writes into ~/.claude/settings.json:
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_METRICS_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:<listener-port>",
"OTEL_LOG_USER_PROMPTS": "1",
"OTEL_LOG_ASSISTANT_RESPONSES": "1",
"OTEL_LOG_TOOL_DETAILS": "1",
"OTEL_LOG_RAW_API_BODIES": "file:<hyp-home>/spool/claude-bodies",
...
}
Settings env overrides the shell environment at startup and reaches every
session regardless of launch path (terminal, desktop-spawned, SDK, background
service), which is strictly broader than the PATH-shim idea and does not need
the corporate launcher (CLAUDE_CODE_PROCESS_WRAPPER); that mechanism wraps
processes, which we do not need for env injection. Fleets can deliver the same
block via managed settings, with the documented approval dialog.
A new listener source (home: the @hypaware/claude plugin, since the payload
shapes are Claude Code's, with the OTLP endpoint itself possibly shared
kernel machinery) receives the event stream and tails the body spool:
- Events first. The event stream is naturally incremental: each piece of
content is emitted exactly once (
user_promptonce,assistant_responseonce,tool_resultonce), so it arrives pre-deduplicated. - Bodies for what events lack.
system_text, thetoolslist, message ordering, and untruncated tool args come from the body files referenced byapi_request_body.body_ref. A body file is projected and then deleted. - Projection unchanged. The source yields the same
ai_gateway.projected_exchangevalues backfill providers yield today; the dataset,part_iddedupe, partitioning (LLP 0030), and repo identity columns (LLP 0032) are untouched. OTEL is a third producer, not a new table. - Behavioral events (
tool_decision,permission_mode_changed, hook execution, MCP health, and the metrics) land in their own dataset(s), not crammed intoai_gateway_messages. Shape is an open question below.
Native message.uuid on events kills the identity race the flush-time
settlement enricher exists for (LLP 0027). The .hypignore check moves inline
to ingest with cwd known (R4), retiring the late-drop machinery of LLP 0085 on
this path. What survives: transcript backfill as the recovery path, and the
SessionStart hook as the source of cwd and git identity (git_remote,
head_sha, git_branch), since events do not carry them (spike finding) and
deriving them from body system text is parseable but fragile.
- Iceberg: identical mechanics to today. Request bodies repeat the full
history each turn, exactly as the proxied wire does, and the same
part_iddedupe stores each part once. Events reduce pressure further because they never repeat content at all. - Spool: bodies are large transiently (about 145 KB per request measured, so a heavy day can pass gigabytes through the directory) but are deleted on projection. The daemon enforces a byte cap with oldest-first eviction for the daemon-down window; evicted bodies are recovered later from transcript backfill. The cap is a config value with a sane default (proposal: 512 MB).
hyp attach claude on a proxy-attached machine removes the proxy env keys,
unwinds launchd env, offers detach --purge for the CA trust, and writes the
OTEL env block. Sessions started before the flip keep proxying until restart;
capture overlap is harmless because both producers dedupe into the same rows.
The gateway proxy remains fully supported for codex, claude-desktop, openclaw,
hermes, and raw SDK traffic: this RFC narrows the proxy's client list, it does
not retire the gateway.
| Column(s) | OTEL source |
|---|---|
session_id, message_id, provider_uuid, request_id, prompt_id |
event attributes (session.id, message.uuid, request_id, prompt.id) |
model, role, content_text, part_* |
events + body messages |
system_text, tools |
body files |
tool_name, tool_call_id, tool_args, tool_result_for |
tool_result events + body blocks |
usage tokens, attributes.usage |
api_request events (plus body usage) |
cwd, git_branch, git_remote, head_sha, repo_root |
SessionStart hook (unchanged) |
client_version, entrypoint |
app.version, app.entrypoint (today: enrichment) |
user_id |
user.account_uuid / body metadata.user_id |
is_sidechain, agent_id |
query_source, agent.name (today: transcript inference) |
parent_uuid, logical_parent_uuid, user_type, permission_mode |
transcript join only, if retained (open question 3) |
thinking_signature |
body thinking blocks (text redacted both paths) |
raw_frame |
body excerpt at projection, same policy as today |
Net-new (R2): everything listed in R2, none of it visible on the wire.
- Keep proxy attach, add OTEL alongside. Captures the same content twice and keeps every proxy cost. The behavioral event stream is worth ingesting regardless, but content capture does not need two producers.
- PATH shim plus corporate launcher. The docs' own pattern for process
wrapping, but strictly more moving parts than the settings
envblock for env injection, with worse coverage (shim misses GUI launches; launcher misses plain terminal sessions by design). - Events only, no body files. Cleanest storage story, but loses
system_text, thetoolslist, and untruncated tool args (tool_inputin events clips values at 512 chars, about 4 KB total). Revisit ifCLAUDE_CODE_OTEL_CONTENT_MAX_LENGTHplus future event coverage closes those gaps.
- Delivery guarantees: resolved 2026-08-17. Best-effort delivery is
accepted. Content loss has a recovery path (spool survives a down daemon;
transcript backfill covers the rest, as it already does for pre-attach
history); behavioral-event loss during daemon downtime is tolerated.
Duty:
hyp statusgrows a capture-health line (last event seen vs last transcript activity) so silent gaps are visible instead of discovered at report time. - Behavioral dataset shape: resolved 2026-08-17. A new
claude_telemetry_eventsdataset owned by@hypaware/claude: one row per event, typed columns for the hot fields (event name, session_id, tool_name, decision, source, cost), attributes JSON for the rest. Not routed through@hypaware/otel's generic logs/metrics datasets, and not widened intoai_gateway_messages. - Parent chains: resolved 2026-08-17. Code survey found no consumer of
parent_uuid/logical_parent_uuidoutside their producers, and no graph plugin readsis_sidechain. The columns stay in the schema and read null on the OTEL path;query_sourceandagent.nameare the attribution source. No transcript join is kept for live capture. - Session ignore: resolved 2026-08-17. The claude listener hosts the
same session-ignore control route the gateway proxy hosts, and
hyp session ignore/unignoreposts to both. Reuses the existing in-memory mechanism; no new on-disk contract. (Settled in the implementation spec, #798.) - Flag stability (position).
OTEL_LOG_RAW_API_BODIESand friends are documented but young. Detection is two-layered: thehyp statuscapture-health line (open question 1's duty) catches silent field drift in production, and a hermetic smoke asserts the event and body shapes against the installed Claude Code on every release. - Version floor: resolved 2026-08-17. Below the floor (>= 2.1.193 for
the event set,
tool_sourcedetail at >= 2.1.214),hyp attach claudeREFUSES to switch modes: it leaves any existing attach untouched and prints an upgrade hint (claude update). No proxy fallback for theclaudeclient: one attach mode per client keeps the test matrix single, and Claude Code self-updates aggressively enough that stale clients are transient.
This RFC stays the deliberation record and spawns six narrow decisions, one per
settled choice: injection mechanism (LLP 0258), events-plus-bodies split
(LLP 0252), spool cap policy (LLP 0253), settlement retirement scope
(LLP 0254), behavioral dataset shape (LLP 0255), and session-ignore transport
(LLP 0256, the choice open question 4 above resolves). It also spawns a spec
for the listener source (LLP 0257). The proxy-attach docs it displaces for the
claude client (parts of LLP 0231 to 0244) get Extended-by: /
Superseded-by: forward refs at that point, not before.