Skip to content

docs(coding-agents): hook-to-OTLP foundation + Cursor application session tracing (TH-8229) - #912

Merged
nik13 merged 14 commits into
mainfrom
feat/th-8229-cursor-hook-to-otlp
Oct 29, 2026
Merged

nik13 merged 14 commits into
mainfrom
feat/th-8229-cursor-hook-to-otlp

Conversation

@nik13

@nik13 nik13 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Foundation PR for the TH-8103 coding-agent family plus the Cursor child (TH-8229). It replaces Rick's original cookbook recipe; his commit stays in the history and the new commits sit on top.

Shared emitter: examples/coding-agents/hook-to-otlp/

One stdlib-only Python 3.9+ hook command. It reads a hook payload on stdin and sends OTLP/HTTP spans to Future AGI with X-Api-Key, X-Secret-Key and project_name.

  • Trace shape: one session is one trace, with the trace id derived from the session id. Spans nest session → turn → tool / subagent / llm / event.
  • Durations: start and end hooks are paired through a locked per-session state file. When there is no start hook, the app's reported duration is used.
  • Apps without ids: these still get a tree. Keyless turns open turn-1, turn-2, ... Tool end hooks are paired with the oldest open start of the same tool (FIFO). A new turn closes a turn that never reported its end.
  • Privacy: content is off by default. FI_HOOK_CAPTURE_CONTENT=1 turns it on.
  • Host safety: every shipped hook uses the canonical guarded subshell (python3 -B <dir>/emit.py <app> <Event> 2>/dev/null || true), so a missing emitter, a missing python3 or a crash still exits 0 with empty stdout, and the payload reaches python3 whether the host pipes it to stdin or appends it as a trailing heredoc. The emitter itself exits 0 and prints the response the host expects; if its adapter cannot be loaded or it fails, it prints nothing. On macOS/Linux the network send runs in a detached child process; Windows has no fork, so the send is inline with a 3 s default timeout and state files are not locked (documented).
  • Adapters: one file per app in adapters/<app>.py, with the contract in adapters/base.py and Cursor as the reference. replay.py drives the real emitter with recorded payloads.

Cursor: examples/coding-agents/cursor/

  • hooks.json for project and user installs.
  • install.sh, which never overwrites an existing hooks.json.
  • run_e2e.sh.
  • Hooks are registered on observation hooks only (sessionStart, beforeSubmitPrompt, postToolUse, postToolUseFailure, subagentStop, afterAgentThought, afterAgentResponse, preCompact, stop, sessionEnd), never on permission hooks. If someone wires it to a permission hook by mistake it prints nothing (Cursor fails open on no output); it never answers allow.

Docs

  • New pages integrations/coding-agents/hook-to-otlp (shared) and integrations/coding-agents/cursor.
  • A new Integrations → Coding Agents nav group, with one // coding-agents:<app> marker per child.
  • The old cookbook page and its nav entry are removed.

Sources: the mapping follows https://cursor.com/docs/hooks (read 2026-10-14). The CLI and cloud-agent gaps come from the Cursor docs and staff posts on https://forum.cursor.com/t/cursor-cli-doesnt-send-all-events-defined-in-hooks/148316.

How to run

cd examples/coding-agents/hook-to-otlp && python3 -m unittest discover -s tests -t .   # 60 tests
examples/coding-agents/cursor/install.sh /path/to/project                            # real install
FI_API_KEY=... FI_SECRET_KEY=... FI_BASE_URL=http://localhost:14338 FI_PROJECT_NAME=cursor-e2e \
  examples/coding-agents/cursor/run_e2e.sh                                          # fixture replay through the real emitter
HOOK_TRANSPORT=heredoc FI_API_KEY=... FI_SECRET_KEY=... FI_BASE_URL=http://localhost:14338 \
  examples/coding-agents/cursor/run_e2e.sh                                          # same, through the installed hooks.json commands (stdin|heredoc)

E2E evidence (local Future AGI, 2026-10-14)

Real Cursor session

  • Setup: Cursor 3.24.18 (macOS arm64), signed in on the free Hobby plan. It ran with an isolated profile on a throwaway project. Hooks were installed by install.sh, and the keys came from ~/.config/fi-hook-to-otlp/env as the page describes. The Agent panel was driven through Cursor's own UI.
  • Hooks fire on Hobby. These agent hooks fired: beforeSubmitPrompt, afterAgentThought, postToolUse (Read, Grep, Shell), afterAgentResponse and stop. Every send returned HTTP 200.
  • Error marking: one turn failed in Cursor itself ("Named models unavailable" on the free plan). It came through as an ERROR turn.

fi_verify.py --project th8103-coding-cursor --expect-type tool --expect-type llm --expect-type agent passed. The default-privacy chat produced one trace with three turns:

trace 8042e760-e501-59fc-9555-64512d76d3eb (13 spans)
- [agent] Cursor session model=grok-4.6
  - [chain] Cursor turn status=ERROR:agent loop ended with error
  - [chain] Cursor turn (9074ms)
    - [tool] Shell (1390ms) / [tool] Read (46ms) / [llm] Agent thought x2
    - [llm] Agent response tok=38068/220
  - [chain] Cursor turn (8071ms)
    - [tool] Grep (218ms) / [llm] Agent thought x2
    - [llm] Agent response tok=39102/64
trace 6fe1fbc1-905d-3d58-1d76-17450e52aac4 (8 spans, content on): turn with Grep, Read x2, thoughts, response tok=38807/293
PASS

Real-payload findings, now handled in the Cursor adapter and covered by a sanitized fixture

  • tool_use_id arrives as two newline-joined ids; the first is the call id, the second is kept as cursor.tool.provider_call_id.
  • afterAgentResponse and stop carry input_tokens, output_tokens, cache_read_tokens and cache_write_tokens. Cursor's docs don't list these. The counts are recorded once, on the Agent response span.
  • afterAgentThought fires twice per thinking block, once with a suffixed generation_id. The suffix is stripped and the pair collapses to one span.
  • sessionStart did not fire for IDE chats.
  • sessionEnd fires for empty chats, so empty sessions are now never exported.
  • sessionEnd fails on window close ("MainThreadShellExec not initialized").

Fixture replay (after the review fixes, head 6513a26, 2026-10-15)

run_e2e.sh replays a fixture through the real emitter (one emit.py process per hook) with fresh UUID ids. The fixture's /Users/fixture was rewritten to the tester's real home directory, and the failed tool's error text to ENOENT: open '<home>/secret-proj/.env'. All 38 sends returned HTTP 200 and fi_verify --project th8103-coding-cursor --expect-type tool passed:

Fixture Default Content on
Real Cursor 3.24.18 turn (Read, Grep, thoughts, response with tokens) 3b2f51f1-7cea-4fe6-509b-96248625f4aa (7 spans) 91ad5701-b930-6e94-7e67-046998041257 (7 spans)
Documented shape (Shell timeout ERROR, subagent, compaction, sessionEnd) ab779b0a-3b2e-0845-8a43-0924b19540f4 (9 spans) ff65f3de-0656-6a79-8a55-c59e0373e09f (9 spans)
Same, hooks 0.1 s apart (tool durations reach back before the exported root) e99709d2-d0be-0d9e-f224-f1d9e83d86af (9 spans)

In ClickHouse every trace has one root, no orphans, and no span starting before its root. The fast replay exercised the clamp: root 3405 ms over turn 3034 ms. The reviewed trace 7b7a4dcb (root 5827 ms under a turn of 7475 ms) is superseded by these traces.

trace ff65f3de-0656-6a79-8a55-c59e0373e09f (9 spans)
- [agent] Cursor session model=claude-opus-4-7 (13111ms)
  - [chain] Cursor turn model=claude-opus-4-7 in=64B out=48B (10520ms)
    - [llm] Agent thought / [tool] Shell status=ERROR:ENOENT: open '/Users/<user>/secret-proj/.env' ... (5000ms)
    - [tool] Shell (420ms) / [agent] Subagent: explore (4000ms) / [tool] Read (35ms)
    - [chain] Context compaction / [llm] Agent response
trace ab779b0a-... (default): same tree, failed Shell status=ERROR:timeout, in=0B out=0B everywhere
PASS

Privacy on the OTLP bodies actually sent (FI_HOOK_DUMP_FILE), occurrences:

Check Default (2 replays) Content on (3 replays)
Prompt/tool/response markers 0 30
input.value / output.value / exception.message 0 / 0 / 0 13 / 20 / 2
Tool error text (ERRPATH_*, secret-proj) 0 exported, path as /Users/<user>/secret-proj/.env
Tester's real username or /Users/<username> 0 0
/Users/<user>/ (scrubbed home paths in tool input/output/errors) 0 7
User email, transcript path, workspace roots 0 0

Not tested: Cursor CLI

Cursor CLI 2026.10.14-941492e was installed with the official installer into an isolated HOME. It needs its own agent login or CURSOR_API_KEY, so no CLI session was run. The page now says the CLI was not run, documents it as not at parity, and no longer claims a CLI session produces a trace. Payload transport differs by surface: the IDE pipes the payload to stdin, while the headless agent host bundled with Cursor 3.24.18 (cursor-agent-host/dist/agent-host-daemon) builds CliHooksExecutor without options, so it defaults to argv_heredoc and runs <command> <<'CURSOR_HOOK_EOF' with stdin ignored. The subshell commands handle both (round-2 E2E below).

Privacy proof (live Cursor chats)

Each chat had its own marker in the prompt.

Check Default chat Content-on chat
Marker in the OTLP bodies actually sent (FI_HOOK_DUMP_FILE) 0 3
input.value / output.value sent 0 / 0 4 / 9
Marker in ClickHouse input/output/attributes 0 2 spans
User email or transcript path, sent or stored 0 0

A unit test points transcript_path at an unreadable canary file and spies on open, io.open and os.open: it is never opened.

Review fixes (2026-10-15, commit 6513a26)

  • Guarded hook commands (cross-cutting rule): python3 -B ... 2>/dev/null || true in hooks.json and hooks.user.json and on the page (superseded by the subshell form in round 2 below). tests/test_cursor_hooks_config.py runs every shipped command through /bin/sh, zsh and bash with the emitter missing, with python3 missing, and installed: exit 0, empty or expected stdout, no __pycache__. Cursor 3.24.18 runs hooks with $SHELL -c (fallback /bin/sh), checked in its bundled extension host.
  • Paths with content on: the privacy table claimed paths are never exported. They are never exported as metadata. With content on, paths inside prompts and tool input/output (Read file_path, Shell working_directory) and error text are exported, and the emitter now replaces the user name in home-directory paths with <user> (/Users/, /home/, C:\Users\). Both pages now say so.
  • Error text is content: with content off only the vendor enum (failure_type) or a fixed message is exported. A test puts /Users/alice/... in tool and session error text and asserts it is absent by default and scrubbed with content on.
  • Tool without generation_id (PRD R-02 deviation): R-02 says to attach such a tool to the session and mark the turn unknown. The emitter attaches it to the turn that is open at the time, because the core's "no turn id means the open turn" rule is what the keyless-turn apps (Kiro, Antigravity, Claude Code) rely on. The span is marked coding_agent.turn.inferred=true, and if no turn is open it goes under the session. Both pages' troubleshooting now describe this instead of claiming session parenting.
  • Root start: once the root has been exported its start is fixed, so children are clamped to it and can no longer start before their parent.
  • Subagent span id now includes the turn and duration, so the same task in two turns no longer overwrites the earlier span.
  • State file with content on keeps content only for spans still open and for turns closed as incomplete (needed to re-send them complete). A duplicate end of an already-complete turn is ignored. Documented on the page and in the README.
  • Windows: documented that background send and locking are macOS/Linux only; the inline send defaults to a 3 s timeout. Native Windows is not tested.
  • Misregistered permission hook prints nothing instead of {"permission": "allow"}. Per Cursor's docs, {} is schema-invalid and blocks, while no output is a failed hook that fails open.
  • Nits: cache tokens removed from "not available"; cursor.tool.provider_call_id explained; the root model comes only from session, turn and LLM events (the live trace 8042e760 above predates this and shows grok-4.6 on the root); the adapter docstring is corrected; replay.py rewrites suffixed ids and run_e2e.sh gives any fixture fresh UUIDs (an exact-match replace had left the suffixed thought ids unrewritten, which produced orphan thought spans in an earlier replay).
  • Compatibility: the core changes were run against every child adapter's own tests (Kiro IDE/CLI, Augment, Antigravity, Devin, Devin Desktop, Grok Build, Copilot, Gemini CLI, Hermes, OpenClaw). The pass/fail set is identical with the old and new emit.py; the existing failures are in those children's work in progress.

Round-2 fixes (2026-10-15, commit 9b77d51)

  • MAJOR, heredoc transport: with cmd 2>/dev/null || true, a trailing heredoc binds to true, so Cursor's headless agent host (argv_heredoc) would hand the emitter an empty stdin and nothing would be exported. Every shipped command (hooks.json, hooks.user.json), both pages and the README now use (python3 -B <path>/emit.py cursor <Event> 2>/dev/null || true). tests/test_cursor_hooks_config.py runs every shipped command (project and user installs) through /bin/sh, zsh, bash and dash, with the payload on stdin and as a trailing quoted heredoc (stdin closed, as Cursor does). With the emitter missing and with python3 missing it gets exit 0 and empty stdout. Installed, it gets exit 0 and the expected stdout, and the emitter receives the payload verbatim (a prompt containing $HOME, backticks and quotes is not expanded). A regression test shows the unparenthesized form exports nothing under heredoc.
  • Cursor page CLI claim: it no longer says "A CLI session still produces a trace with tool spans". It now says the CLI and cloud agents were not run, keeps the forum-reported hook gaps as reports, and explains the stdin and heredoc transports.
  • Nit, {} on a broken install: emit.py now prints nothing, instead of {}, when the adapter cannot be loaded, when respond() raises, or when run() fails. A test copies the emitter without adapters/cursor.py and runs preToolUse, beforeSubmitPrompt and stop: exit 0, empty stdout, empty stderr. The shared page states this, plus the guard's role when Python cannot start the script at all.
  • Nit, spaced user names: the home-path scrub now covers names with spaces when a path separator follows them (C:\Users\John Smith\x, /Users/Hank Smith/notes.txt), ~name/ paths, and this machine's own home directory anywhere in the text, including at the end of a string. A prefix-only match (/Users/al vs /Users/alice) is not rewritten. (Superseded in round 3: letting a name run across spaces swallowed command text.)
  • Nit, troubleshooting: "A tool span sits directly under the session" (no turn open, no marker) is now its own entry on both pages, separate from coding_agent.turn.inferred=true.
  • replay.py --hooks-json/--cwd/--transport and run_e2e.sh HOOK_TRANSPORT=stdin|heredoc run the installed hooks.json commands through $SHELL -c, the way Cursor does.
  • Children: every child adapter's own tests (Kiro IDE/CLI, Augment, Antigravity, Devin Desktop, Grok Build) give the same pass/fail set with their emit.py and with this one. Children that ship hook commands must switch to the subshell form; that is FOUNDATION.md's canonical command.

Round-2 E2E (local Future AGI, project th8103-coding-cursor). run_e2e.sh with HOOK_TRANSPORT installed the hooks into a temporary project and ran the shipped commands through zsh, giving fresh UUID ids and a unique prompt marker per run. The fixture's /Users/fixture was rewritten to the tester's real home directory, or to /Users/Zq7 Spaced for the spaced-name check, and the failed tool's error text to ENOENT: open '<home>/secret-proj/.env'. All 34 sends returned HTTP 200. fi_verify --expect-type tool --expect-type llm --expect-type agent passed, with one root per trace:

Run Transport Content Trace
Real Cursor 3.24.18 turn (Read, Grep, thoughts, response tok=38779/306) heredoc off 104fd234-58b3-21e7-52e1-1a7c9a9ed9b1 (7 spans)
Same heredoc on b44aa3a8-f858-0580-6ef5-1e21fe904b51 (7 spans)
Documented shape (Shell timeout ERROR, subagent, compaction, sessionEnd) stdin off f2850b32-55f2-055c-b219-aa3f7d30d42b (9 spans)
Same, home /Users/Zq7 Spaced heredoc on 4ef79059-fe4f-6cf4-0f54-4189a46d5592 (9 spans)
trace 4ef79059-fe4f-6cf4-0f54-4189a46d5592 (9 spans)
- [agent] Cursor session model=claude-opus-4-7 (11152ms)
  - [chain] Cursor turn model=claude-opus-4-7 in=62B out=46B (9461ms)
    - [tool] Shell status=ERROR:ENOENT: open '/Users/<user>/secret-proj/.env' ... (5000ms)
    - [llm] Agent thought / [agent] Subagent: explore (4000ms) / [tool] Shell (420ms) / [tool] Read (35ms)
    - [chain] Context compaction / [llm] Agent response
PASS

Privacy, counted in the OTLP bodies as sent (FI_HOOK_DUMP_FILE) and in all ClickHouse columns (input, output, attributes, status message, name):

Check Default (2 runs) Content on (2 runs)
Prompt/tool/response marker, sent / stored 0 / 0 19 / 12 spans
input.value / output.value / exception.message sent 0 / 0 / 0 8 / 14 / 1
Tester's username, home directory or hostname, sent or stored 0 0
Zq7 / Spaced (spaced home name) 0 0 (scrubbed to /Users/<user>/)
User email, transcript path, workspace roots 0 0

Docs: npm run build passes, and npm run audit-links reports 0 broken nav or content links (the 29 orphan pages are pre-existing). Tests: 58 pass on Python 3.14.4, macOS /usr/bin/python3 3.9.6, and Homebrew 3.9.22 and 3.10.17.

Round-3 fixes (2026-10-15, commit 9c6acd6)

  • Regression, scrub swallowed command text: round 2 let a home-path user name run across spaces up to the next slash, so with content on cd /Users/alice && ls src/foo became cd /Users/<user>/foo. The generic scrub now replaces only the one directory component after /Users/, /home/ or C:\Users\ (raw, JSON-escaped or c:/Users/). The name stops at whitespace, quotes and punctuation no macOS, Linux or Windows account name uses, and does not end with a dot, so cd /Users/<user> && ls src/foo, grep ... /home/<user> --include=*.py && cat docs/readme.md, ls /Users/<user>;echo hi and (see /Users/<user>), then /home/<user>. keep the rest of the text.
  • Spaced names via this machine's home: the basename of os.path.expanduser("~") is matched literally after any home prefix, with a boundary check (/Users/al does not match inside /Users/alice). This machine's spaced name is replaced in full at the end of a string and before . or , (round-2 recheck nit: failed in /Users/Jo Ann. used to leak Ann), in POSIX and Windows forms.
  • Documented limitation: another account's spaced name (for example in a log copied from another machine) keeps the part after the first space (/Users/<user> Smith/notes.txt). Stated in the README and on the shared page; a test pins it.
  • Shared page wording: "Registering the hook" no longer says every app page uses the guarded command. Hook-based apps register it; apps that export OpenTelemetry natively have no hook command. The README says "every app that uses this emitter". Both pages now note that fish reads the parentheses as command substitution (round-2 recheck nit; documentation only, fish not run).
  • Tests: 60 pass on Python 3.14.4 and macOS /usr/bin/python3 3.9.6. The three new scrub tests fail against the round-2 emit.py. Every child adapter's own tests (Kiro IDE/CLI, Augment, Antigravity, Devin, Devin Desktop, Grok Build, Copilot, Gemini CLI, Hermes, OpenClaw) give the same pass/fail set (all pass) with their emit.py and with this one.
  • Dump-only replay through the installed hooks.json commands (zsh, heredoc transport, content on, HOME=/Users/Zq7 Spaced, collector unreachable so nothing was sent): 9 OTLP bodies with 0 occurrences of Zq7, Spaced or bob. The Shell span's input.value is ls -la ... && cd /home/<user> && ls src/foo with working_directory /Users/<user>/project, and its error text ends at /Users/<user>.
  • Docs: npm run build passes; npm run audit-links reports 0 broken nav or content links (29 pre-existing orphan pages).

Limitations / unsupported

  • No per-request detail: tokens are per turn, not per model request. There is no cost and no system prompt.
  • Cursor CLI: fires fewer hooks (forum reports) and was not run (needs agent login). The heredoc transport used by Cursor's headless agent host is covered by the shipped commands; checked with replays through those commands, not with a CLI session.
  • Login shell: Cursor runs hooks with $SHELL -c, so the commands need a POSIX login shell (zsh, bash, sh). fish is not supported (it reads the parentheses as command substitution).
  • Other accounts' spaced user names (content on): only the part before the first space is replaced; this machine's own name is replaced in full.
  • Cloud agents: no sessionStart or sessionEnd.
  • AskQuestion: fires no hooks, per forum reports.
  • Window close: the session-end reason is lost.
  • Native Windows: not tested; the guarded commands and installer need a POSIX shell.
  • Out of scope: Tab hooks and Enterprise OTel export, per decision N-03.

🤖 Generated with Claude Code

nik13 and others added 3 commits October 14, 2026 16:25
approved: Nikhil 2026-10-03 blanket
…8229)

Rework the Cursor hook recipe into the coding-agent family foundation:

- examples/coding-agents/hook-to-otlp: stdlib-only (Python 3.9+) hook
  command. One session = one trace (trace id derived from the session id),
  session -> turn -> tool/subagent/llm/event spans, start/end pairing via a
  locked per-session state file, vendor durations when no start hook,
  content off unless FI_HOOK_CAPTURE_CONTENT=1, always exits 0 with the
  response the host expects, network send in a detached child.
- Pluggable adapters (adapters/<app>.py, contract in adapters/base.py);
  Cursor adapter mapped from https://cursor.com/docs/hooks, observation
  hooks only (no permission hooks).
- replay.py for driving the emitter with recorded payloads; unit, CLI and
  Cursor contract tests.
- Integrations > Coding Agents nav group with the shared page, the Cursor
  page, and one marker comment per remaining child. The cookbook page is
  replaced by the integration pages.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ssions

A real Cursor 3.24.18 run showed sessionStart/sessionEnd firing for chats
that were opened and closed without a message (sessionEnd with
duration_ms 0, final_status "none"). Exporting the root at session start
would create an empty trace per such chat.

The root session span now rides along with every batch that exports a
child span (same id and start time, so the latest export wins) and is sent
at session end only when the session had activity. Adds the captured
Cursor 3.24.18 sessionEnd payload as a contract fixture.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ng (TH-8229)

- Cursor page: Cursor reports model "default" when no model is selected;
  chats opened and closed without a message are not sent.
- run_e2e.sh: space hooks 1s apart so the fixture's reported tool
  durations fit inside the turn, as in a real session.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@nik13 nik13 changed the title docs(agents): Cursor application session tracing via hooks (TH-8229) docs(coding-agents): hook-to-OTLP foundation + Cursor application session tracing (TH-8229) Oct 15, 2026
nik13 and others added 2 commits October 14, 2026 19:41
…er session

Additive core changes requested by the Kiro/Antigravity/Devin Desktop
children; existing adapters (Cursor) are unaffected.

- A TURN START with an empty key opens turn-1, turn-2, ... per session;
  a keyless TURN END/UPDATE applies to the open turn (ignored if none).
- A keyless END of a tool/subagent/llm/event span pairs with the oldest
  open keyless START of the same scope and name (FIFO), for pre/post hooks
  without call ids.
- A TURN START closes any other open turn of the session (and its
  children) as incomplete; a late END for that turn re-sends it complete
  with its original start.
- respond(): an adapter returning "" prints nothing (Kiro and Claude Code
  add hook stdout to the model context); None still means "{}".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…e (TH-8229)

A real IDE agent session on the free Hobby plan showed payload details
the hooks reference does not document:

- afterAgentResponse and stop carry input/output/cache-read/cache-write
  tokens for the turn: recorded once on the "Agent response" LLM span as
  llm.token_count.*, and on the turn as cursor.usage.*.
- afterAgentThought fires twice per thinking block, once with a
  generation_id suffixed "-<n>-<4 chars>": the suffix is stripped (no
  phantom turns) and the pair maps to one span id.
- sessionStart does not fire for IDE chats; sessionEnd fails on window
  close. The Cursor page now says so, reports that agent hooks run on the
  Hobby plan, and covers the free-plan "named model" error turn.

Adds the sanitized real turn as a contract fixture. Core: the default
FI_HOOK_TIMEOUT is now 10 s (the send runs in a detached child, so the
host never waits; 3 s timed out against a loaded collector).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
… id (TH-8229)

Real Cursor 3.24.18 postToolUse payloads send tool_use_id as
"call-<uuid>-<n>\nfc_<id>". The first line becomes the span key and
gen_ai.tool.call.id; the provider function-call id is kept as
cursor.tool.provider_call_id. Covered by the real-turn fixture test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
nik13 and others added 2 commits October 15, 2026 05:51
…renting, root start (TH-8229)

Hooks never block the host:
- Cursor hooks.json / hooks.user.json use the canonical guarded command
  `python3 -B <emit.py> cursor <Event> 2>/dev/null || true`; new
  test_cursor_hooks_config runs every shipped command through sh/zsh/bash
  with the emitter missing, python3 missing, and installed (exit 0, empty
  or expected stdout, no __pycache__).
- A misregistered permission hook now prints nothing (Cursor fails open on
  no output) instead of answering {"permission": "allow"}.

Privacy and doc truth:
- Opted-in content keeps paths but replaces the user name in home paths with
  <user>; pages now say paths inside prompts/tool input/output are exported
  with content on, never as metadata. Test: error text with a path and user
  name is absent by default and scrubbed with content on.
- State file keeps content only for open spans and turns closed as
  incomplete; a duplicate end of a completed turn is ignored.
- Windows: inline send uses a 3 s default timeout; pages say background send
  and state locking are macOS/Linux only.
- Cursor page: cache tokens are captured (removed from "not available").

Tree:
- Tool/subagent/llm/event spans without a turn id go under the open turn and
  get coding_agent.turn.inferred=true (pages corrected; PRD R-02 deviation).
- Children never start before the root once its start is fixed.
- Subagent span key includes the turn and duration (no cross-turn overwrite).
- Root model comes from session/turn/llm events only.
- transcript_path test uses an unreadable canary and spies on os.open too.

E2E tooling: replay --replace rewrites substrings (suffixed generation ids),
run_e2e.sh gives any fixture fresh UUID ids.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ilures (TH-8229)

Cursor's bundled headless agent host passes the hook payload as a trailing
heredoc (`<command> <<'CURSOR_HOOK_EOF'`, commandHookPayloadTransport
argv_heredoc). With `python3 ... 2>/dev/null || true` the heredoc binds to
`true` and the emitter reads nothing. Every shipped command, the shared page,
the Cursor page and the README now use the subshell form
`(python3 -B <path>/emit.py cursor <Event> 2>/dev/null || true)`, which works
for stdin and heredoc and still exits 0 with no output when the emitter or
python3 is missing.

- tests/test_cursor_hooks_config.py runs every shipped command (project and
  user hooks) through sh, zsh, bash and dash with the payload on stdin and as
  a trailing heredoc: emitter missing, python3 missing, and installed (exit 0,
  expected stdout, payload received verbatim, no __pycache__). A regression
  test shows the unparenthesized form loses a heredoc payload.
- emit.py prints nothing (instead of "{}") when the adapter cannot be loaded,
  respond() raises, or run() fails, so a misregistered permission hook never
  gets schema-invalid "{}".
- Home-path scrub handles user names with spaces (when a separator follows),
  ~name paths, and this machine's own home directory anywhere in the text.
- replay.py --hooks-json/--cwd/--transport and run_e2e.sh HOOK_TRANSPORT run
  the installed hooks.json commands the way Cursor does.
- cursor.mdx: CLI section no longer claims a CLI session was traced (it was
  not run); troubleshooting covers tools directly under the session (no
  marker) separately from coding_agent.turn.inferred.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
nik13 added a commit that referenced this pull request Oct 15, 2026
nik13 added a commit that referenced this pull request Oct 15, 2026
The previous round let a home-path user name run across spaces up to the
next slash, so opted-in Shell command text such as
"cd /Users/alice && ls src/foo" lost "&& ls src". The generic scrub now
replaces only the one directory component after /Users/, /home/ or
C:\Users\: it stops at whitespace, quotes and punctuation no account name
uses, and does not end with a dot.

Names with spaces are handled through this machine's own home directory:
the basename of os.path.expanduser("~") is matched literally after any
home prefix (raw, JSON-escaped or forward-slash Windows form), with a
boundary check so /Users/al does not match inside /Users/alice. This also
fixes "/Users/Jo Ann." and "/Users/Jo Ann," leaking "Ann". Another
account's spaced name keeps the part after the first space; README and
the shared page document that.

Docs: the shared page no longer says every app page uses the guarded
command (hook-based apps do; native-OTel apps have no hook command), and
both pages note that fish reads the subshell parentheses as command
substitution.

Tests: command-text regression cases, own spaced home (POSIX and
Windows forms, end of string, punctuation, al/alice), and the documented
limitation for other accounts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Keep CLI, cloud agents and other platforms outside supported setup; preserve transport and privacy limitations.

Co-Authored-By: Claude Code <[email protected]>
nik13 and others added 3 commits October 28, 2026 22:18
Preserve the generated integration catalog and the coding-agent navigation entries.

Co-Authored-By: Claude Code <[email protected]>
Register Cursor, the shared hook emitter, LangSmith and the Assistants retirement notice in the existing discovery source. Remove the stale Alpha label without changing the unsupported status. Keep all catalog regression checks intact.

Co-Authored-By: Claude Code <[email protected]>
@nik13
nik13 merged commit 7f7f1ec into main Oct 29, 2026
2 checks passed
@nik13
nik13 deleted the feat/th-8229-cursor-hook-to-otlp branch October 29, 2026 05:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant