Coding agents
What each vendor exports
Which coding agents send tool output, prompts, replies, subagents, tokens and cost, and which files the installer writes for each one.
Each coding agent sends a different part of the turn, which explains a tool row with no result or a turn with no dollars.
What you get
| Vendor | One tree per turn | Tool calls | Tool output | Prompt and reply | Subagents | Tokens and cost |
|---|---|---|---|---|---|---|
claude-code | yes | yes | --capture full | --capture full | yes | both |
codex | yes | when Codex emits them | --capture full | --capture full, reply read from disk (below) | no | both |
gemini-cli | yes | yes | no | --capture full | yes | tokens only |
cursor | yes | yes | --capture full | --capture full | yes | no |
windsurf | yes | yes | --capture full | --capture full | no | no |
opencode | yes | yes | --capture full | --capture full | yes, as child sessions | when OpenCode reports them |
- Claude Code, including in VS Code and JetBrains, sends its own spans. With
--no-tracesit falls back to flat log events. Subagents nest under theAgenttool call. Tokens are on the span, and cost comes from theapi_requestevent. - Codex sends a turn span plus about 150 internal plumbing spans, and the receiver drops the plumbing. Tokens come from
sse_eventrecords and cost fromturn_cost. - Gemini CLI sends
agent_call,llm_callandtool_callspans, and a subagent is anagent_callspan. - Cursor subagents come from its
subagentStartandsubagentStopevents, and Windsurf's Cascade has no subagent events. Cursor's hooks carry no tokens or cost. - OpenCode child sessions nest under their parent turn. Tokens and cost come from the assistant message, when OpenCode reports them there.
Codex replies come from its session files
Codex's OpenTelemetry export carries the prompt, tool calls, tokens, cost and errors, but never the assistant's message. Its stream records hold only an event kind, an error and usage, and no setting changes this. Codex does write every message to a session file under ~/.codex/sessions/<year>/<month>/<day>/.
With --capture full, the Python CLI reads each reply from there and sends it as a record the receiver attaches to its turn. Each file is read from a saved offset, so nothing is sent twice. Each turn carries the trace id Codex used, so a reply joins its own turn even when it is sent hours later. The TypeScript CLI has neither the reply hooks nor backfill.
# new turns: installed with the hooks by --capture full
evalkit coding install --vendor codex --capture full
# turns you already ran: the newest session, the last five, or everything
evalkit coding backfill --vendor codex
evalkit coding backfill --vendor codex --last 5
evalkit coding backfill --vendor codex --all --dry-runWhat the installer writes
For claude-code, the installer writes env keys into ~/.claude/settings.json, or .claude/settings.json with --scope project. They turn telemetry on, send OTLP over http/protobuf to the endpoint with the Authorization=Bearer header, and set the resource attribute evalkit.vendor=claude-code. Span mode is on by default (CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1, OTEL_TRACES_EXPORTER=otlp, OTEL_TRACES_EXPORT_INTERVAL=1000).
Unless you pass --no-subagents, it also adds two hooks, PostToolUse and SubagentStop, that run evalkit coding hook. Uninstall restores the previous values of those keys and removes only the hooks whose command contains coding hook --vendor claude-code. For the other vendors:
codexgets an[otel]table between# evalkit-managed-begin/endmarkers in~/.codex/config.toml(or.codex/config.toml), with log, trace and metrics exporters pointing at<endpoint>/v1/{logs,traces,metrics}. The installer refuses to run if an[otel]table already exists outside the markers. With--capture full, the Python CLI also mergesStopandSessionEndhooks into~/.codex/hooks.json(or.codex/hooks.json). Uninstall removes the block and those hooks.gemini-cligets atelemetryblock in~/.gemini/settings.json(otlpProtocol: http,traces: trueunless--no-traces,logPromptsfalse unless--capture full) and theOTEL_EXPORTER_OTLP_HEADERSline between# evalkit-begin/endmarkers in~/.gemini/.env. Uninstall restores the previoustelemetryvalues and removes the marker block.cursorgets 12 hook entries (evalkit coding hook --vendor cursor --event …, with a 5-second timeout) in~/.cursor/hooks.jsonor.cursor/hooks.json, and other hooks are left alone. Uninstall removes only entries whose command containscoding hook --vendor cursor, and a reinstall drops entries for events no longer used.windsurfgets 6 hook entries in~/.codeium/windsurf/hooks.json, or.windsurf/hooks.jsonin project scope. Uninstall follows the same rule as for Cursor.opencodegets the plugin~/.config/opencode/plugins/evalkit.tsand a"plugin"entry in~/.config/opencode/opencode.json. Project scope writes.opencode/plugins/evalkit.tsandopencode.jsonin the current directory. The plugin holds no key and reads~/.config/evalkit/config.envwhen it runs. Uninstall removes the plugin file and only our entry in"plugin".
The spans Cursor, Windsurf and OpenCode build themselves
These vendors export nothing, so the hook or plugin posts OTLP spans directly. Every id is a SHA-256 of payload fields, so processes that never talk to each other still agree on the tree.
For Cursor, the trace id is sha256(conversation_id + generation_id)[:32], the turn root sha256(generation_id)[:16], a tool span sha256(tool_use_id)[:16] and a subagent sha256(subagent_id)[:16]. Windsurf uses trajectory_id and execution_id. OpenCode uses sessionID and messageID, and a child session reuses its parent's trace.
The post hook writes each tool span with start = now − duration, so the waterfall shows the real interval. A turn root arrives with start == end when the prompt is submitted. An enrichment record completes it later with the real end time and, with --capture full, the completion. Without full capture the turn root is named turn, not the first 80 characters of the prompt.