Skip to main content

Overview

Moda ingests first-class telemetry from coding-agent runtimes. The primary signal is OTLP logs, with OTLP traces and metrics accepted on the same provider-prefixed route family. Customers can also POST JSON payloads from custom hook scripts for richer per-event signals. Together these signals power session, tool, skill, and cost analytics on top of Claude Code, Codex, and Cursor without modifying agent code or wrapping the runtime in a custom SDK.

Endpoints

EndpointPurpose
POST /v1/otel/claude/logsPrimary Claude Code runtime event path (OTLP logs).
POST /v1/otel/claude/tracesClaude Code OTLP trace spans.
POST /v1/otel/claude/metricsClaude Code OTLP metrics.
POST /v1/otel/claude/hooksClaude Code hook JSON payloads from installed hook scripts.
POST /v1/otel/codex/logsPrimary Codex runtime event path (OTLP logs).
POST /v1/otel/codex/tracesCodex OTLP trace spans.
POST /v1/otel/codex/metricsCodex OTLP metrics.
POST /v1/otel/codex/hooksCodex hook JSON payloads from installed hook scripts.
POST /v1/otel/cursor/logsPrimary Cursor runtime event path (OTLP logs).
POST /v1/otel/cursor/tracesCursor OTLP trace spans.
POST /v1/otel/cursor/metricsCursor OTLP metrics.
POST /v1/otel/cursor/hooksCursor hook JSON payloads from installed hook scripts.
All routes authenticate with your Moda API key via Authorization: Bearer <MODA_TOKEN>. OTLP routes accept JSON (Content-Type: application/json) or protobuf (Content-Type: application/x-protobuf) bodies. Hook routes accept JSON. Metric envelopes are accepted and persisted to events_raw (Layer 0) as raw payloads. Normalized rows are NOT yet written to coding_agent_events for metrics — only logs, traces, and hook events produce normalized rows in the first release. Token-usage metrics in particular are stored raw and not yet exposed in coding_agent_events-backed dashboards.

Claude Code setup

The Moda CLI configures Claude Code’s OTLP exporter for you. moda init (or moda hooks install on its own) writes the telemetry environment block — including the endpoints and your Authorization: Bearer header — into the env section of ~/.claude/settings.json, so every Claude Code session exports traces, logs, and metrics without any manual exports.
moda init   # authenticate, then wire hooks + the OTLP exporter env block
Because Claude Code reads telemetry config from its environment (the env block in settings.json), and not from ~/.moda/config.json, this is the step that makes telemetry flow. The CLI resolves your API key (env first, then ~/.moda/config.json) into the auth header and chmods settings.json to 0600 since it now holds that key. Re-running is idempotent and refreshes the key/endpoints in place; other env entries are left untouched.

Manual: export the OTEL variables yourself

For environments where the CLI doesn’t run the agent (CI, a service unit, a container entrypoint), export the standard OpenTelemetry variables in the shell that launches Claude Code:
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_METRICS_EXPORTER=otlp
export OTEL_LOGS_EXPORTER=otlp
export OTEL_TRACES_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
# Logs + metrics ingest as protobuf; the traces endpoint expects JSON.
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/json
export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://<moda-otel-endpoint>/v1/otel/claude/logs
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://<moda-otel-endpoint>/v1/otel/claude/traces
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=https://<moda-otel-endpoint>/v1/otel/claude/metrics
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer $MODA_TOKEN"
export OTEL_RESOURCE_ATTRIBUTES="provider=claude_code,company.id=...,project.id=...,user.email=...,repo=..."
# Required — without this Claude sends only prompt_length (or prompt="<REDACTED>")
export OTEL_LOG_USER_PROMPTS=1
These are the same variables moda init writes into ~/.claude/settings.json. An exported value in the launching shell wins over the settings.json env block, so an existing shell with stale exports keeps using them until you start a fresh session.
Claude Code redacts prompt text by default. If OTEL_LOG_USER_PROMPTS is not set in the shell that launches claude, the OTLP prompt attribute is empty or the literal placeholder <REDACTED> while prompt_length still reflects the real size. Moda does not store that placeholder as message text. When OTEL_LOG_USER_PROMPTS=1, Moda persists the real prompt attribute into coding_agent_events.prompt_text (after secret redaction). You do not need CODING_AGENT_CAPTURE_PROMPTS on the ingest worker for emitter-provided prompt text; that flag is for server-wide capture or hook payloads without a Claude-side opt-in. OTEL_RESOURCE_ATTRIBUTES is how Moda groups events by org, project, user, and repo. See Resource attributes Moda reads below for the full list. Because Claude Code defaults to OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf, the logs endpoint must accept protobuf bodies. To smoke-test the spec-mandated protobuf path directly (swap application/x-protobuf for application/json if your exporter is JSON-mode):
curl -X POST https://<moda-otel-endpoint>/v1/otel/claude/logs \
  -H "Authorization: Bearer $MODA_TOKEN" \
  -H "Content-Type: application/x-protobuf" \
  --data-binary @logs.binpb

Codex setup

moda init or moda hooks install --agent=codex writes the Moda hook config plus the provider-specific OTEL env block into ~/.codex/hooks.json. For managed environments where you configure Codex directly, add the following block to your Codex config:
[otel]
environment = "production"
log_user_prompt = false

[otel.exporter.otlp-http]
endpoint = "https://<moda-otel-endpoint>/v1/otel/codex/logs"
protocol = "binary"
headers = { "authorization" = "Bearer MODA_TOKEN" }
This config should live in your user-level or managed Codex config, not in a project-local file. Project-local OTEL config can leak between repositories and is harder to audit.

Hooks (optional)

Both Claude Code and Codex support custom hook scripts that fire on session, prompt, tool, and permission events. Moda exposes dedicated JSON endpoints so those hook scripts can POST structured payloads directly, without going through OTLP. This is useful when you want richer per-event signals than the runtime emits over OTLP, for example custom approval decisions, repo metadata, or sandbox details.
curl -X POST https://<moda-otel-endpoint>/v1/otel/claude/hooks \
  -H "Authorization: Bearer $MODA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hook": "PostToolUse",
    "session_id": "sess_123",
    "tool_name": "Bash",
    "tool_use_id": "tool_abc",
    "success": true,
    "duration_ms": 412,
    "cwd": "/home/user/repo",
    "repo": "acme/web",
    "branch": "main",
    "timestamp": "2025-01-15T12:34:56Z"
  }'
Swap /v1/otel/claude/hooks for /v1/otel/codex/hooks or /v1/otel/cursor/hooks when sending from a Codex or Cursor hook script.

Skill creation (Moda CLI hooks)

The moda CLI installs coding-agent hooks that capture a per-event shadow-repo snapshot (file state correlated to each prompt/tool call) and signal session ends. This powers Moda’s skill-creation pipeline, which turns repeated corrections into reusable SKILL.md files.
moda init            # authenticate, save the API key, and ask whether to ingest coding-agent traces
moda init --yes      # accept defaults, including trace hook install for all supported agents
moda init --no-coding-agent-traces
moda hooks install   # installs the snapshot + session-end hooks AND the OTLP exporter env block into ~/.claude/settings.json
moda hooks install --agent=codex
moda hooks install --agent=cursor
moda hooks install --agent=all
moda init prompts before enabling coding-agent trace ingestion. If accepted, it runs moda hooks install --agent=all so Claude Code, Codex, and Cursor all emit shadow snapshots and get provider-specific OTLP exporter env blocks. Use --no-coding-agent-traces to skip this in setup automation. moda hooks install remains backward-compatible and installs Claude Code hooks by default. Pass --agent=codex, --agent=cursor, or --agent=all to install additional hook adapters. Claude hooks and OTEL env are written to ~/.claude/settings.json; Codex hooks and OTEL env are written to ~/.codex/hooks.json; Cursor hooks and OTEL env are written to ~/.cursor/hooks.json. The CLI chmods these config files to 0600 because the OTLP header includes the resolved API key. Only MODA_API_KEY is needed locally — it is resolved from the environment first, then from ~/.moda/config.json written by moda init, so hooks work regardless of how the agent is launched (login shell, Infisical, etc.). The local machine never holds ClickHouse, model, or sandbox credentials. On session end/stop, the CLI posts a creds-free session_end signal with the final shadow workspace metadata. Skill creation itself runs server-side when you call moda skills gen: the skill harness processes unhandled coding-agent traces, triggers analytics clustering, distills candidate skills, and promotes candidates that pass the gate.

Pulling generated skills into your project

Promoted skills are served back to your coding agent with moda skills pull, which fetches your tenant’s skills (authenticated with the same API key) and writes each into the agent’s skill directory so it can use them:
moda skills pull                    # writes approved skills to .claude/skills/<id>/SKILL.md (+ .cursor/rules/<id>.mdc)
moda skills pull --status=proposed  # include not-yet-promoted candidates (default: approved)
moda init runs this automatically after detecting your coding agent, so existing skills are available immediately on setup. Re-running pull is idempotent: unchanged skills are skipped and updated ones are rewritten. The full SKILL.md text is stored durably server-side (in ClickHouse), so retrieval does not depend on the ephemeral skill-harness container filesystem.

Privacy & data capture

Prompt text and tool output snippets are off by default. Moda stores event-level metadata (tool names, durations, token counts, success/failure, error types) but does not persist user prompts or command output unless you explicitly opt in.

Capture policy

The ingest worker exposes three env flags that, together, determine whether a given prompt or output snippet is persisted. The effective rule applied to every event is:
capture = serverCapture OR (allowEventLevelOptIn AND eventLevelOptIn)
Env varDefaultPurpose
CODING_AGENT_CAPTURE_PROMPTSfalseServer-side switch for prompt text. When true, every event’s prompt text is persisted (after redaction + truncation).
CODING_AGENT_CAPTURE_OUTPUT_SNIPPETSfalseServer-side switch for tool output snippets. When true, every event’s output snippet is persisted (after redaction + truncation).
CODING_AGENT_ALLOW_EVENT_LEVEL_OPT_INfalseMaster gate for honoring per-event moda.capture_prompts=true / moda.capture_outputs=true attributes. When false, those attributes are ignored. When true, individual events can upgrade themselves into capture even when the server-level flags are off.
All three flags accept "true" or "1" as truthy; every other value (including empty, "yes", "on") is treated as false.

Prompt and output capture

The three flags combine to give three useful deployment shapes:
# Default: nothing is captured. Only event-level metadata reaches Moda.
# (no env vars set — this is the recommended baseline.)

# Server-on: capture everything globally, regardless of per-event attrs.
export CODING_AGENT_CAPTURE_PROMPTS=true
export CODING_AGENT_CAPTURE_OUTPUT_SNIPPETS=true

# Event-opt-in enabled: server defaults to off, but events that carry
# `moda.capture_prompts=true` or `moda.capture_outputs=true` are honored.
export CODING_AGENT_ALLOW_EVENT_LEVEL_OPT_IN=true
When an event’s text is suppressed by this policy, Moda still writes the row but sets a prompt_text_redacted=true (or output_snippet_redacted=true) marker on attributes so downstream consumers can distinguish “no text emitted” from “text emitted but suppressed at ingest”.
Secret redaction is applied to commands, error messages, and any captured output snippets before insert. Patterns such as Authorization, Bearer, api_key, token, password, secret, ANTHROPIC_API_KEY, OPENAI_API_KEY, MODA_TOKEN, and COOKIE are stripped.

Provider-specific notes

Claude Code: skill activation is first-class. Claude Code emits first-class skill activation events (claude_code.skill_activated). Moda treats these as canonical skill_activated rows with skill_name and skill_scope populated, so skill usage analytics work without any extra instrumentation.
Claude child processes do not inherit OTEL env vars automatically. If Claude Code spawns subprocesses (for example via Bash tool calls that themselves invoke claude), those child processes will not export telemetry unless you re-export the same OTEL environment variables in the child’s shell. Plan child telemetry setup separately.
Codex: skill usage is not first-class today. Codex does not currently expose canonical skill telemetry; skill usage is inferred from tool-call attributes or hook payloads. Moda surfaces these signals where present but does not synthesize first-class skill_activated rows for Codex.

Resource attributes Moda reads

The Moda normalizer recognizes the following attribute names on OTLP resource, log record, or span scopes. Set these via OTEL_RESOURCE_ATTRIBUTES (Claude Code) or your hook payloads (Codex) to route events to the right org, project, and user in Moda.
  • Org: org_id, org.id, company.id, organization.id
  • Project: project_id, project.id, moda.project_id
  • User ID: user.id, enduser.id, moda.user_id
  • User email: user.email, enduser.email
  • Repo: repo
  • Branch: branch
tenant_id is always resolved from your Moda API key and cannot be overridden via body attributes.

Troubleshooting

Events not appearing? Query the Layer 0 raw store to see whether requests are reaching the ingest worker:
SELECT timestamp, source, http_status, raw_event
FROM moda.events_raw
WHERE source = 'otlp'
   OR source = 'coding_agent_hook'
ORDER BY timestamp DESC
LIMIT 50;
  • source = 'otlp' covers all /v1/otel/{claude,codex,cursor}/{logs,traces,metrics} requests.
  • source = 'coding_agent_hook' covers /v1/otel/{claude,codex,cursor}/hooks requests.
If rows are missing entirely, check that your Authorization header is set and that the exporter endpoint matches the table in Endpoints. If rows are present but no normalized data appears, inspect raw_event to confirm the payload shape and event names.
For more on the underlying OTLP trace pipeline and other ingestion providers, see the Ingestion overview.