Docs

    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

    VendorOne tree per turnTool callsTool outputPrompt and replySubagentsTokens and cost
    claude-codeyesyes--capture full--capture fullyesboth
    codexyeswhen Codex emits them--capture full--capture full, reply read from disk (below)noboth
    gemini-cliyesyesno--capture fullyestokens only
    cursoryesyes--capture full--capture fullyesno
    windsurfyesyes--capture full--capture fullnono
    opencodeyesyes--capture full--capture fullyes, as child sessionswhen OpenCode reports them
    • Claude Code, including in VS Code and JetBrains, sends its own spans. With --no-traces it falls back to flat log events. Subagents nest under the Agent tool call. Tokens are on the span, and cost comes from the api_request event.
    • Codex sends a turn span plus about 150 internal plumbing spans, and the receiver drops the plumbing. Tokens come from sse_event records and cost from turn_cost.
    • Gemini CLI sends agent_call, llm_call and tool_call spans, and a subagent is an agent_call span.
    • Cursor subagents come from its subagentStart and subagentStop events, 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.

    bash
    # 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-run

    What 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:

    • codex gets an [otel] table between # evalkit-managed-begin/end markers 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 merges Stop and SessionEnd hooks into ~/.codex/hooks.json (or .codex/hooks.json). Uninstall removes the block and those hooks.
    • gemini-cli gets a telemetry block in ~/.gemini/settings.json (otlpProtocol: http, traces: true unless --no-traces, logPrompts false unless --capture full) and the OTEL_EXPORTER_OTLP_HEADERS line between # evalkit-begin/end markers in ~/.gemini/.env. Uninstall restores the previous telemetry values and removes the marker block.
    • cursor gets 12 hook entries (evalkit coding hook --vendor cursor --event …, with a 5-second timeout) in ~/.cursor/hooks.json or .cursor/hooks.json, and other hooks are left alone. Uninstall removes only entries whose command contains coding hook --vendor cursor, and a reinstall drops entries for events no longer used.
    • windsurf gets 6 hook entries in ~/.codeium/windsurf/hooks.json, or .windsurf/hooks.json in project scope. Uninstall follows the same rule as for Cursor.
    • opencode gets the plugin ~/.config/opencode/plugins/evalkit.ts and a "plugin" entry in ~/.config/opencode/opencode.json. Project scope writes .opencode/plugins/evalkit.ts and opencode.json in the current directory. The plugin holds no key and reads ~/.config/evalkit/config.env when 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.

    EvalKit is built by SyntropyLabs. Published on PyPI and npm.