Skip to content

claudio listen receives OpenTelemetry logs - #106

Merged
ctoth merged 3 commits into
masterfrom
feat/otlp
Oct 20, 2026
Merged

ctoth merged 3 commits into
masterfrom
feat/otlp

Conversation

@ctoth

@ctoth ctoth commented Oct 10, 2026

Copy link
Copy Markdown
Owner

Why

The unit claudio plays is an event: a timestamped name with a few attributes. That is also what an OpenTelemetry log record is. Hooks are one way to get events and need claudio installed where the agent runs; telemetry is another and needs only a few environment variables there. Accepting OTLP logs lets claudio hear anything that already exports telemetry.

What

POST /v1/logs on the existing listener is an OTLP/HTTP logs receiver for the JSON encoding (gzip accepted, protobuf refused with 415 and a hint). It sits behind the same token, staleness drop and limit of eight sounds as /events. No new dependencies.

  • Claude Code's records map onto the events its hooks produce, so one soundpack serves both: user_prompt, tool_decision, tool_result, compaction, subagent_completed, api_retries_exhausted. Its other records are silent. One session start sends dozens of hook_registered and plugin_loaded records.
  • Any other service's record maps by name and outcome: eventName, the event.name attribute or an identifier body becomes the hint; success or an error severity picks the category. A free-text log line is skipped.
  • duration_ms and error_type ride along in the event's attributes. Nothing reads them yet.

Evidence

  • The fixture is a real export from Claude Code 2.1.296, scrubbed of identity fields; internal/listen/testdata/SOURCE.md says how it was captured and what was changed. It settled things the docs do not say: the event name is the record body plus an event.name attribute, timeUnixNano is a string, and success and duration_ms are strings.
  • Live run: a built listener received a real claude -p session's telemetry and logged prompt, two tool starts, one tool success and one tool error as accepted, with the startup records silent and no playback error.
  • go build ./..., go vet ./..., go test ./... -count=1 pass locally on Windows.

Known limits

  • The mappings for a rejected tool_decision, compaction, subagent_completed and api_retries_exhausted come from the documented attribute lists. None of those records was in the capture.
  • Telemetry says less than a hook: a shell command is only "Bash" without tool-detail logging, and nothing reports a turn ending or the agent waiting for permission.
  • Records arrive in batches, five seconds apart by default.
  • Hooks and telemetry enabled for the same session play every sound twice. Nothing dedupes.
  • Not tested from a cloud session or with Codex.

ctoth added 3 commits October 10, 2026 16:15
- POST /v1/logs is an OTLP/HTTP logs receiver for the JSON encoding, with
  optional gzip, behind the same token, staleness drop and play limit as
  /events; protobuf is refused with 415 and a hint
- Claude Code's records map onto the events its hooks produce (prompt,
  tool start, tool success and failure, compaction, subagent finished),
  so one soundpack serves both; its other records are silent
- Any other service's record maps by name and outcome: eventName, the
  event.name attribute or an identifier body is the hint, and success or
  an error severity picks the category; a free-text log line is skipped
- duration_ms and error_type ride along in the event's attributes
- The fixture is a real export from Claude Code 2.1.296, scrubbed;
  testdata/SOURCE.md says how it was captured and what was changed
- cli-reference: the /v1/logs receiver, the variables that point Claude
  Code at it, how its records and any other service's map to sounds, what
  telemetry lacks next to hooks, and the status codes
- CLAUDE.md: the third way events arrive
@ctoth
ctoth merged commit 7e96e43 into master Oct 20, 2026
9 checks passed
tahodev pushed a commit to tahodev/claudio that referenced this pull request Oct 20, 2026
LoadJSONSoundpackFromBytes silently dereferenced arbitrary absolute
paths and any '..' segments from untrusted soundpack JSON, allowing a
malicious soundpack to map every event to /etc/shadow, ~/.ssh/id_rsa,
or similar.

Split the loader API into untrusted (default; rejects absolute paths,
'..' segments, and resolutions outside baseDir) and trusted-embedded
(for go:embed platform JSONs that ship with the binary and use system
paths like /System/Library/Sounds/Purr.aiff). The trusted variant
skips path-syntax checks; the untrusted variant validates every
mapping value through validateMappingValue.

LoadJSONSoundpackFromBytes signature gains a baseDir parameter so it
can resolve relative values consistently with LoadJSONSoundpack
(closes review finding ctoth#15).

Also bounds mappings count at MaxSoundpackMappings (10,000) before
the existence-check loop runs, preventing a pathological JSON from
pinning the inode cache during validation. The cap is enforced on
both trust tiers — it is a DoS guard, not a trust check.

internal/cli/cli.go switched its one call site from
LoadJSONSoundpackFromBytes (now untrusted, needs baseDir) to the new
LoadEmbeddedPlatformSoundpack so the shipped wsl.json/darwin.json/
windows.json continue loading with their absolute system paths.

Existing soundpack_test.go tests that used absolute paths in mapping
values were rewritten to use relative paths under temp directories,
matching the new contract.

Resolves review findings ctoth#15, ctoth#28, and ctoth#106.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant