[FEATURE] Emit OTel events for hook execution lifecycle
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
Claude Code's OTel instrumentation covers the tool pipeline comprehensively — tool_decision (PreToolUse), tool_result (PostToolUse), user_prompt, api_request, api_error — but the hook execution pipeline is completely invisible to telemetry.
When a hook fires, Claude Code:
- Matches the event type to hook entries in settings.json
- Spawns the process, pipes the payload via stdin
- Waits for exit (sync) or fires and forgets (async)
- Checks exit code — 0 = pass, 2 = block, else = failure
All of this information exists internally and is recorded in session transcripts. But none of it is exported to OTel. The only crack in this opacity is PostToolUseFailure / StopFailure, which fire on hook errors — but there is no corresponding event for successful execution.
This means operators cannot answer basic questions from their observability stack:
- Which hook is adding latency to my tool calls? (no duration data)
- How often does each hook fire? (no per-hook invocation count)
- Is my async notification hook silently failing on half the events? (success only visible via absence of failure)
- Which hooks fire for which tools? (no hook-to-tool correlation)
- What's the hook overhead across my plugin stack? (related to #35078's performance concerns, but from an observability angle)
The data exists in session transcripts, but transcripts are flat files per session — not queryable, not aggregatable, not in your dashboard.
Proposed Solution
Emit a new OTel log event for each hook execution:
Event name: hook_execution
Attributes:
| Attribute | Type | Description |
|---|---|---|
| hook_event_type | string | PreToolUse, PostToolUse, Stop, SessionStart, etc. |
| hook_command | string | The command string from settings.json (opt-in via OTEL_LOG_TOOL_DETAILS) |
| hook_index | int | Position in the hooks array (disambiguates multiple hooks per matcher) |
| hook_matcher | string | The matcher pattern (e.g., Edit\|Write, empty string for catch-all) |
| hook_type | string | command, prompt, agent, http |
| hook_async | bool | Whether the hook ran async |
| duration_ms | int | Wall-clock execution time |
| exit_code | int | Process exit code (0 = success, 2 = block) |
| success | bool | true if exit code 0 or 2 |
| tool_name | string | The tool that triggered this hook (for Pre/PostToolUse) |
| session_id | string | (already on all events) |
Gated by: OTEL_LOG_TOOL_DETAILS=1 (reuses existing detail-level opt-in, since hook commands may contain sensitive paths).
This is consistent with how tool_result already works — it reports tool name, duration, success, and parameters. Hooks are user-provided code executed by Claude Code's runtime, analogous to middleware in a web framework. The framework should report middleware execution telemetry.
Why Not Just Use Failure Events?
PostToolUseFailure and StopFailure exist and are useful, but:
- No positive signal — "no failure event" ≠ "hook ran successfully." The hook might not have fired at all (matcher didn't match), or the failure event itself might have failed to deliver.
- No duration — Failure events don't report how long the hook ran before failing. For timeout issues, this is the critical data point.
- No success telemetry — You can't calculate success rate without both numerator and denominator.
- No per-hook identity — Failure events don't always identify which hook in a multi-hook matcher group failed.
Alternative Solutions
Wrapper scripts — Users can wrap each hook command with a timing/reporting wrapper that sends OTLP logs. This works but adds ~50ms Python startup overhead per hook invocation and requires modifying every hook entry in settings.json. It's the user instrumenting their own code because the runtime won't.
Infer from tool_result duration — For sync hooks, the tool_result.duration_ms includes hook execution time. But this conflates tool execution with hook overhead and doesn't work for async hooks at all.
Priority
Medium - Would be very helpful
Feature Category
Monitoring and telemetry
Additional Context
- Related: #35078 (hook performance/parallel dispatch — observability data would directly inform that work)
- Related: #42281 (native OTLP trace export — hook spans would fit naturally into trace trees)
- Related: #35319 (skill invocation tracking — skills are invoked via the Skill tool, but the hook that gates them is invisible)
- The existing
PostToolUseFailure/StopFailureevent types show the team already recognizes hook observability matters — this request extends that to the full lifecycle
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗