Repository navigation
feat(framework): extend the chain to the four remaining tools #653
Description
Activity
- added a parent issue
on Aug 14, 2026 The hook side, mapped across the five tools
This issue's table covers the export side. Building the run journal produced the hook side, and it is a different shape. Recorded here so it is not re-derived: every line below is either from the tool's own documentation, fetched 2026-08-20, or measured on a real session.
The framework stays Claude-oriented; the CLI adapts
Not a preference — it is already how the code works.
plugins/*/hooks/hooks.jsonis authored in Claude Code's shape, andcli/src/domain/formats/cursor-hooks.tslowercases the first character of each event key whilesubstitutePluginRootrewrites${CLAUDE_PLUGIN_ROOT}/. The CLI is a mechanical translator, and adding semantic per-tool knowledge to it would put the same concern in two places.Consequence, accepted deliberately: where a tool has a better event than the mechanical translation lands on, we do not take it. Cursor's
afterFileEditcarriesfile_pathdirectly, which is cleaner thanpostToolUse— and we still usepostToolUse, because reaching for it would make the CLI clever.The three concepts, and what each tool calls them
Meaning Claude Code Codex Cursor Copilot OpenCode session begins SessionStartSessionStartsessionStartsessionStartsession.createda turn ends StopStopstopnone session.idlea file was written PostToolUsePostToolUsepostToolUse(afterFileEditis better; not used)postToolUsefile.editedThe mechanical first-character transform gets Cursor all three for free.
The event name travels in argv, not in the payload
Measured across the recorded fixtures:
claude-code hook_event_name: "SessionStart" codex hook_event_name: "SessionStart" copilot hook_event_name: ABSENT cursor hook_event_name: "sessionStart"Copilot sends no event name at all, so a payload-only dispatch cannot work there. The hook command therefore carries our own stable name as an argument —
journal.js session-start|turn-end|file-written— which the CLI passes through untouched. Event keys stay each tool's; the argv is ours. Two vocabularies on purpose.Names describe intention, not mechanism:
turn-end, notstop;file-written, notpost-tool-use. Three of the five tools spell those mechanisms differently.Copilot has no turn-end event, and that is already handled
Its events are
sessionStart,sessionEnd,userPromptSubmitted,preToolUse,postToolUse,errorOccurred.ended_atwas specified as "the last observed turn, never a session-end event" — because Codex grants a session-end handler one second at most and does not fire it for subagents. Making that literally true fixes Copilot for free: any event reaching the hook advancesended_at, sofile-writtenbecomes the de-facto turn signal wherever turn-end does not exist. No per-tool logic.Extracting the written file path is per-tool work
This is the part that does not generalise, and the reason the journal ships a per-host extractor rather than one shape:
Tool Where the path is Claude Code tool_input.file_pathCursor afterFileEdit.file_path— direct, but unused, see aboveCopilot postToolUse, shape to confirmCodex no path field at all — it is inside the apply_patchcommand string, and must be parsed outOpenCode file.editedOpenCode is not a hooks-in-JSON tool
cli/.../tool-contracts.tsalready says so:hooks: { supported: false }, // opencode has no HasHooks capability.It exposes a JavaScript plugin API (
session.created,file.edited,tool.execute.after), not command hooks in a config file. So it is not a naming translation at all — it is a separate installation mode, and it deserves its own issue rather than a row in this one.One suspicion, probed and refuted
flattenCopilotHooksShapecopies the event key verbatim, so Copilot receivesSessionStartin PascalCase while its documentation listssessionStart. If Copilot rejected PascalCase,aidd-context's memory hook would be installed and silent there — the exact failure this layer exists to catch.Probed by wiring both spellings to the same event in one config. Both fired. No bug. Recorded so nobody re-opens it.
Sources: Claude Code · Codex · Cursor · Copilot · OpenCode
Needs re-scoping twice over.
#663 took the journal half. The run journal now serves Claude Code, Codex, Cursor and Copilot - each declaring how its session id and working directory are read. That part of "extend the chain to the four remaining tools" is delivered.
#684 took most of the export half. The decision there is that cost comes from the files a tool already writes, not from its OTLP export. Codex and OpenCode are readable locally (#685), so extending their export buys nothing they do not already have.
What honestly remains under this ticket:
- Copilot's export, which is the only route to a complete token figure for it - its local file gives
outputTokensper turn and nothing else. It needs the user to setCOPILOT_OTEL_ENABLED, which AIDD deliberately does not do. - Cursor, which has no route at all: no local token count, and an export gated behind an Enterprise team setting. This ticket should say that plainly rather than carry it as pending work.
Suggest re-titling around Copilot specifically, and moving Cursor's impossibility somewhere it is stated as a known limit rather than a backlog item.
- Copilot's export, which is the only route to a complete token figure for it - its local file gives
This ticket and the work now running are two different routes, and only one of them is in flight
A tool reaches a figure by two independent paths, and this ticket is about the second:
- The journal and the local read — the tool's hooks run, they see a session id, a step boundary is written, and something the tool already wrote on disk carries token counts. No export, no endpoint, no configuration.
- The export — the tool ships spans or logs to a collector, and the identifier in them joins the one the hook saw. That is the table above.
Everything in flight on
claude/aidd-telemetry-layer-e403ufis the first route. Plan:aidd_docs/tasks/2026_08/2026_08_22_telemetry-every-tool/. It matters here because the two routes share a prerequisite — a hook that fires and sees a session id — and that prerequisite is failing on three of the four tools for reasons this table does not mention.What the local route has established, per tool
Tool Hook fires Sees its session id Journals a step Local figure Codex yes, once trusted (#699) yes, CODEX_THREAD_IDyes yes, reconciles Copilot yes, all three events yes, session_idin the compat payload (#681)no — a skill call opens no step (#701) no per-request input in its own files Cursor no plugin-scope hook fired at all, twice (#680) unknown no writes no token count anywhere OpenCode no — its runtime is JS modules, a declarative hooks.jsonmeans nothing to it (#676)unknown no readable, unjoinable Two of these bear directly on this ticket's gates:
Codex's gate is not only the feature flag. It will not run a hook it has not been asked to trust, and says nothing when it declines — four consecutive sessions ran clean and journalled nothing. The table calls this "persisted hook trust"; what it does not say is that the failure is silent, which is what makes it expensive.
Cursor's gate may be prior to trust. The table assumes a hook fires and only the export is in question. Two headless probes fired no plugin-scope hook at all, while an earlier probe fired five of seven events from a project-scope file, and nothing in Cursor's configuration names the plugins sitting in its own plugin directory. If plugin hooks never run there, neither route reaches Cursor, and the Enterprise export setting is the second obstacle rather than the first. A probe is running now to settle it.
Not a proposal to change this ticket
The export route stands as written, including its two traps — Codex's
metrics_exporterdefaulting to a third party nobody chose, and OpenCode's 495 spans for a trivial session. This is so that whoever picks it up knows which gates have already been measured, and which of them turned out to fail quietly.- added a commit that references this issue
on Aug 24, 2026 Closing — the outcome arrived by the other route
This issue describes extending the chain through each tool's own OTLP export configuration. That route was deleted: on the machine that built it, it produced 0 of 34 stored records, it was the only thing opening a port and the only thing sending an address off the machine, and
receive/endpoint/endpoint clearare gone with it.The outcome it asked for is delivered anyway, by reading each tool's own files. All five tools carry a
telemetryLocalReaddeclaration incli/src/domain/tools/ai/—claude.ts,codex.ts,copilot.ts,opencode.ts,cursor.ts(the last asunsupported, with the measured reason). Coverage and its absence are both reported per tool, each naming what it can and cannot supply.Its one remaining condition — that the identifier join hold over the export — is not merely unbuilt, it is unbuildable: there is no export. The join it protects is re-checked on every pull request over the route that does exist, by
cli / Identifier join (Claude Code).Closed as delivered by another route rather than rewritten. The decision of record groups this issue with #655 and #662 as "describes a route that no longer exists"; that is imprecise for this one and worth correcting here — #653 describes no upload, and its result is already shipped.
- added a commit that references this issue
on Aug 31, 2026
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Projects
- StatusShow more project fieldsDone
Outcome
Each remaining tool joins the same chain, one at a time, with its own gate lifted and its coverage stated honestly.
Scope
Per tool: switch the export on, ship the hook, prove the identifier joins, and declare what is and is not reachable. Measured facts, not assumptions.
codex.sse_eventlog events[otel]inconfig.toml;metrics_exporterdefaults tostatsig, notnoneinvoke_agent/chatspansCOPILOT_OTEL_ENABLEDor an OTLP endpointcursor.api.requestlogsai.streamTextspansexperimental.openTelemetryinopencode.jsonTwo traps to carry into the work
Codex ships metrics to a third party by default.
otel.metrics_exporterdefaults tostatsig. Enabling telemetry without setting that key sends data somewhere nobody chose.OpenCode is loud. One trivial session produced 495 spans across 348 KB, since everything down to file reads is instrumented. Sampling is not optional there.
Done When
Relations