[FEATURE] Emit OTel events for hook execution lifecycle

Resolved 💬 3 comments Opened Apr 8, 2026 by m-ghalib Closed Apr 11, 2026

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:

  1. Matches the event type to hook entries in settings.json
  2. Spawns the process, pipes the payload via stdin
  3. Waits for exit (sync) or fires and forgets (async)
  4. 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:

  1. 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.
  2. No duration — Failure events don't report how long the hook ran before failing. For timeout issues, this is the critical data point.
  3. No success telemetry — You can't calculate success rate without both numerator and denominator.
  4. 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/StopFailure event types show the team already recognizes hook observability matters — this request extends that to the full lifecycle

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗