Send your agent's own telemetry to sigiro
Point Claude Code, Codex, Hermes Agent, or Pi at sigiro to inspect sessions, model latency, tokens, tools, errors, and retries.
This guide records what your coding agent does, rather than what the application it edits does. The resulting spans, logs, and metrics let you find slow model calls, repeated tools, failed prerequisites, excess context, and serialized work.
This is different from Instrument your code with an agent, which points your application at sigiro. Use both when you need both views.
Install the sigiro skill
Install the public skill before you start the agent:
npx skills add Arnav12114/skills
The debug-with-sigiro skill teaches the
agent how to inspect telemetry in sigiro. It does not change the telemetry
settings in this guide.
Before you start
Start sigiro and verify its API:
sigiro serve
curl http://127.0.0.1:9999/health
The local receiver accepts OTLP/HTTP on 4318 and OTLP/gRPC on 4317. It has no
request authentication, so do not expose it to an untrusted network. For the
the hosted service, run sigiro signup --name "Your Name" --email you@example.com
for a new account (or sigiro auth login for an existing account), and follow Send telemetry to hosted
sigiro instead.
Telemetry can contain prompts, responses, file contents, commands, user identifiers, and environment details. The examples below choose conservative capture settings. Review every exporter before enabling richer payloads.
Claude Code
Claude Code has built-in OpenTelemetry export. Add these keys to the env object
in ~/.claude/settings.json. Then every session sends telemetry, including
sessions that an IDE starts:
{
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA": "1",
"OTEL_TRACES_EXPORTER": "otlp",
"OTEL_METRICS_EXPORTER": "otlp",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:4318",
"OTEL_METRIC_EXPORT_INTERVAL": "10000"
}
}
If the file already has an env object, merge the keys into it. The same
variables also work as export statements in the shell that starts claude.
Tracing is beta in Claude Code. CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 and
OTEL_TRACES_EXPORTER=otlp turn it on. Do not remove them: /v1/services and the
gen_ai and operation_summaries sections of a diagnosis come from spans, so a
setup without traces looks empty. See
Claude Code monitoring for the
version you run.
Start a new Claude Code session, run one prompt, wait ten seconds, then use the verification steps below.
What Claude Code sends by default
Every metric, log, and span has user.email, user.account_uuid,
user.account_id, user.id, organization.id, session.id, and
terminal.type. Think about these identity attributes before you send telemetry
to a shared or hosted server.
Without the settings in the next section, telemetry names each tool but not its arguments:
- Tool calls: tool name, success, duration, input and result size in bytes, and the permission decision.
- Bash commands: only the first program, from a fixed list (
bash_argv0, for examplegit, orother), and its category (bash_command_class). - Tool failures: only a category, such as
ShellErrororTelemetrySafeError. - Prompts:
<REDACTED>and the prompt length. - Model requests: model, tokens, time to first token, and
cost_usdonclaude_code.api_requestevents.
Capture more detail
Each setting below exports more content, such as commands, file contents, and conversation text. Enable only the settings whose exposure you accept, then start a new session.
| Setting | What it adds |
|---|---|
OTEL_LOG_TOOL_DETAILS=1 |
full_command, file_path, skill_name, and subagent_type on tool spans. tool_input and tool_parameters on tool events. Full error text. MCP server names and connection errors. |
OTEL_LOG_TOOL_CONTENT=1 |
A tool.output span event with the input and output of each tool. Requires traces. |
OTEL_LOG_USER_PROMPTS=1 |
Prompt text on claude_code.user_prompt events and claude_code.interaction spans. |
OTEL_LOG_ASSISTANT_RESPONSES=1 |
Response text on claude_code.assistant_response events. If you do not set it, it follows OTEL_LOG_USER_PROMPTS. Set it to 0 to keep responses redacted. |
OTEL_LOG_RAW_API_BODIES=1 |
The full API request and response JSON as claude_code.api_request_body and claude_code.api_response_body events. Request bodies include the conversation history. |
OTEL_METRICS_INCLUDE_VERSION=true |
app.version on metrics and events. |
OTEL_METRICS_INCLUDE_ENTRYPOINT=true |
app.entrypoint on metrics and events. |
A failed tool shows the difference. With the default settings, a failed Read
reports TelemetrySafeError and a failed Bash command reports ShellError. With
OTEL_LOG_TOOL_DETAILS=1, the same failures report
File does not exist. Note: your current working directory is … and
Shell command failed, with the command or path that failed.
Claude Code truncates content attributes at
CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH, which is 61,440 characters by default. A
request body for a long conversation arrives cut off at that length. Sigiro
stores values of that size. Larger limits are not tested.
Detailed beta tracing
Detailed beta tracing also records what the model received and returned at each
step. Add these keys to the same env object:
"ENABLE_BETA_TRACING_DETAILED": "1",
"BETA_TRACING_ENDPOINT": "http://127.0.0.1:4318"
With these keys, Claude Code sends logs and traces to BETA_TRACING_ENDPOINT
instead of OTEL_EXPORTER_OTLP_ENDPOINT. Metrics still go to
OTEL_EXPORTER_OTLP_ENDPOINT. Use the same sigiro base URL for both.
Detailed beta tracing adds:
- a
claude_code.hookspan for each hook run, with the hook configuration ifOTEL_LOG_TOOL_DETAILS=1; new_contexton interaction, model request, and tool spans: the text that Claude Code added to the model context at that step;response.model_output,query_source,system_prompt_preview,system_prompt_length,system_prompt_hash,system_reminders, andtoolson model request spans;claude_code.system_promptlog events.
system_reminders can be tens of kilobytes on each model request. Claude Code
does not include these attributes in its stable span schema, so their names can
change. Interactive sessions get them only if your organization is on Anthropic’s
allowlist. claude -p and Agent SDK sessions do not need the allowlist. Claude
Code ignores both keys in project and local settings files.
Traces from a subprocess
Claude Code gives each Bash command a TRACEPARENT variable. A claude -p run
that a Claude Code Bash tool starts joins the trace of the parent session. Its
claude_code.interaction span has parent.source: env, and
/v1/traces/{trace_id} returns the spans of both sessions. Filter on
session.id to separate them.
Read Claude Code telemetry
claude_code.interaction one user prompt
├── claude_code.llm_request one model request
├── claude_code.tool permission wait and execution
│ ├── claude_code.tool.blocked_on_user permission wait
│ │ └── claude_code.llm_request auto mode permission check
│ └── claude_code.tool.execution execution
└── claude_code.hook detailed beta tracing only
- The duration of
claude_code.toolincludes the permission wait. Useclaude_code.tool.executionfor run time andclaude_code.tool.blocked_on_userfor wait time. - In auto mode, Claude Code checks some tool calls with a separate model request.
That request is a
claude_code.llm_requestspan underclaude_code.tool.blocked_on_user, withquery_source_safe = 'auto_mode'. In Claude Code 2.1.273, each check added about 1.4 seconds. The span has token counts, but Claude Code sends noclaude_code.api_requestevent and noclaude_code.cost.usagepoint for it. Claude Code cost totals do not include these checks. - A failed tool sets span status and
success = falseon itsclaude_code.tool_resultevent. Claude Code sends no ERROR-severity log for it. claude_code.api_requestevents and theclaude_code.cost.usagemetric report the same total cost.
Replace the timestamps in these queries with literal UTC bounds around your sessions. Cost by day and model:
curl -sS -X POST http://127.0.0.1:9999/v1/query \
-H 'content-type: text/plain' \
--data "SELECT strftime(timestamp, '%Y-%m-%d') AS usage_date,
json_extract_string(log_attributes, 'model') AS model,
count(*) AS requests,
round(sum(CAST(json_extract_string(log_attributes, 'cost_usd') AS DOUBLE)), 2) AS cost_usd
FROM sigiro_logs
WHERE service_name = 'claude-code'
AND body = 'claude_code.api_request'
AND timestamp >= TIMESTAMP '2026-09-10 00:00:00'
AND timestamp < TIMESTAMP '2026-09-11 00:00:00'
GROUP BY usage_date, model
ORDER BY usage_date, cost_usd DESC"
Calls, failures, and p95 duration for each tool:
curl -sS -X POST http://127.0.0.1:9999/v1/query \
-H 'content-type: text/plain' \
--data "SELECT json_extract_string(log_attributes, 'tool_name') AS tool,
count(*) AS calls,
count(*) FILTER (WHERE json_extract_string(log_attributes, 'success') = 'false') AS failures,
quantile_cont(CAST(json_extract_string(log_attributes, 'duration_ms') AS DOUBLE), 0.95) AS p95_ms
FROM sigiro_logs
WHERE service_name = 'claude-code'
AND body = 'claude_code.tool_result'
AND timestamp >= TIMESTAMP '2026-09-10 00:00:00'
AND timestamp < TIMESTAMP '2026-09-11 00:00:00'
GROUP BY tool
ORDER BY calls DESC"
Permission wait compared with execution time:
curl -sS -X POST http://127.0.0.1:9999/v1/query \
-H 'content-type: text/plain' \
--data "SELECT span_name,
count(*) AS spans,
round(sum(duration) / 60000000.0, 1) AS total_minutes,
median(duration) / 1000 AS p50_ms
FROM sigiro_spans
WHERE service_name = 'claude-code'
AND span_name IN ('claude_code.tool.blocked_on_user', 'claude_code.tool.execution')
AND timestamp >= TIMESTAMP '2026-09-10 00:00:00'
AND timestamp < TIMESTAMP '2026-09-11 00:00:00'
GROUP BY span_name"
Auto mode permission checks, which are not in Claude Code cost totals:
curl -sS -X POST http://127.0.0.1:9999/v1/query \
-H 'content-type: text/plain' \
--data "SELECT json_extract_string(span_attributes, 'model') AS model,
count(*) AS checks,
round(sum(duration) / 60000000.0, 1) AS total_minutes,
sum(CAST(json_extract_string(span_attributes, 'cache_read_tokens') AS BIGINT)) AS cache_read_tokens,
sum(CAST(json_extract_string(span_attributes, 'output_tokens') AS BIGINT)) AS output_tokens
FROM sigiro_spans
WHERE service_name = 'claude-code'
AND span_name = 'claude_code.llm_request'
AND json_extract_string(span_attributes, 'query_source_safe') = 'auto_mode'
AND timestamp >= TIMESTAMP '2026-09-10 00:00:00'
AND timestamp < TIMESTAMP '2026-09-11 00:00:00'
GROUP BY model"
Codex
Codex reads telemetry settings from ~/.codex/config.toml:
[otel]
environment = "dev"
log_user_prompt = false
[otel.trace_exporter.otlp-grpc]
endpoint = "http://127.0.0.1:4317"
[otel.metrics_exporter.otlp-grpc]
endpoint = "http://127.0.0.1:4317"
[otel.exporter.otlp-grpc]
endpoint = "http://127.0.0.1:4317"
exporter is the log exporter; it does not replace trace_exporter or
metrics_exporter. Codex reports itself as codex_exec, so look for that
service name during verification.
Hermes Agent
Hermes uses the community
briancaffey/hermes-otel plugin:
hermes plugins install briancaffey/hermes-otel/hermes_otel
Install the plugin requirements into the same Python environment that runs
Hermes. For a source checkout whose virtual environment is
~/git/hermes-agent/venv:
~/git/hermes-agent/venv/bin/pip install \
-r ~/.hermes/plugins/hermes_otel/requirements.txt
Use the equivalent pip path for your installation; installing these packages
into an unrelated system Python does not make them visible to Hermes.
Create ~/.hermes/hermes_otel.yaml:
backends:
- type: otlp
name: sigiro
endpoint: http://127.0.0.1:4318/v1/traces
metrics: true
logs: true
capture_logs: true
capture_previews: false
capture_sender_id: false
capture_conversation_history: false
# Optional host-level evidence for local performance work.
host_metrics: true
host_metrics_gpu: auto
host_metrics_interval_ms: 1000
Restart Hermes after installing or changing the plugin. The generic OTLP backend
derives /v1/metrics and /v1/logs from the trace endpoint. It emits nested
session, model, API, tool, skill, approval, and subagent spans plus token,
duration, cost, retry, and resource metrics.
The plugin defaults capture_previews to true; the explicit false above
prevents prompt, response, tool argument, and tool result previews from being
exported. capture_logs: true can still export Python records from imported
libraries. Use the plugin’s log_attach_logger setting when you need a narrower
logger boundary.
Pi coding agent
Pi’s official @earendil-works/pi-telemetry package defines telemetry types but
currently provides no OTLP exporter. The following setup therefore uses the
community-maintained
@devkade/pi-opentelemetry
extension; review it before adopting it:
pi install npm:@devkade/pi-opentelemetry
export PI_OTEL_ENABLE=1
export OTEL_SERVICE_NAME=pi-coding-agent
export OTEL_TRACES_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://127.0.0.1:4318/v1/traces
export OTEL_METRICS_EXPORTER=otlp
export OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://127.0.0.1:4318/v1/metrics
export OTEL_METRIC_EXPORT_INTERVAL=10000
export PI_OTEL_PRIVACY_PROFILE=strict
export PI_OTEL_PAYLOAD_MAX_BYTES=32768
pi
Inside Pi, /otel-status reports exporter state and /otel-open-trace opens the
current trace. The extension documents traces and metrics, not log export. Its
strict privacy profile exposes less than the default
detailed-with-redaction profile.
Confirm that telemetry arrived
Run one real prompt in the configured agent, then discover the service name:
curl http://127.0.0.1:9999/v1/services
Do not guess the emitted vocabulary. Inspect span and metric names first. Replace the two timestamps with literal UTC bounds around your test session:
curl -sS -X POST http://127.0.0.1:9999/v1/query \
-H 'content-type: text/plain' \
--data "SELECT service_name, span_name, count(*) AS occurrences
FROM sigiro_spans
WHERE timestamp >= TIMESTAMP '2026-09-10 12:00:00'
AND timestamp < TIMESTAMP '2026-09-10 13:00:00'
GROUP BY service_name, span_name
ORDER BY occurrences DESC"
Metrics can use several OpenTelemetry table types, so discover all of them:
for table in gauge sum histogram exp_histogram; do
curl -sS -X POST http://127.0.0.1:9999/v1/query \
-H 'content-type: text/plain' \
--data "SELECT metric_name, count(*) AS points
FROM sigiro_metrics_${table}
WHERE timestamp >= TIMESTAMP '2026-09-10 12:00:00'
AND timestamp < TIMESTAMP '2026-09-10 13:00:00'
GROUP BY metric_name ORDER BY points DESC"
done
Literal TIMESTAMP bounds let sigiro prune Parquet files. A predicate built from
now() reads every file and becomes progressively slower as local history grows.