[DOCS] PostToolUse hook input schema for Agent/Task tools: reproducible evidence of telemetry extraction failure mode

Resolved 💬 3 comments Opened May 14, 2026 by pepo1275 Closed May 18, 2026

Summary

External user-built telemetry hook (PostToolUse, matcher=Agent|Task) accumulated 24h+ of null metrics entries (0 of 13 captures with real total_tokens/tool_uses/duration_ms) because the spike that designed it assumed those fields would be in tool_response.usage.* of the stdin payload. They are not. This issue documents reproducible evidence to help close the documentation gap referenced in #52607 and #52047.

Reproducible scenario

  1. Configure ~/.claude/settings.json:

``json
{"hooks": {"PostToolUse": [{"matcher": "Agent|Task", "hooks": [
{"type": "command", "command": "node /path/to/extractor.cjs"}
]}]}}
``

  1. Hook script reads stdin JSON and attempts to extract input.tool_response.usage.total_tokens (the path many open-source examples assume).
  2. Invoke any Agent (Agent({subagent_type: \"X\", ...})) → hook fires.
  3. Observed: extracted value is null for every invocation.
  4. Hook continues silently producing null-only JSONL entries.

We accumulated 13 entries in ~/.claude/agent-usage.jsonl like:

{\"subagent_type\":\"silent-hunter\",\"total_tokens\":null,\"tool_uses\":null,\"duration_ms\":null,\"agentId\":\"a4d2...\",\"repo\":\"RPVEA\",\"...\":\"...\"}

The fields that ARE populated correctly via stdin: tool_name, tool_input.subagent_type, tool_input.description, tool_response.agentId, session_id. The metrics fields the user-facing <usage> block shows during a session — total_tokens, tool_uses, duration_ms — appear nowhere in stdin.

Root cause confirmed empirically

Subagent transcripts at ~/.claude/projects/<project>/<session_id>/subagents/agent-<agentId>.jsonl contain per-type=assistant message:

{
  \"type\": \"assistant\",
  \"agentId\": \"...\",
  \"timestamp\": \"...\",
  \"message\": {
    \"usage\": {
      \"input_tokens\": 36,
      \"output_tokens\": 3108,
      \"cache_creation_input_tokens\": 156014,
      \"cache_read_input_tokens\": 342058,
      \"cache_creation\": {\"ephemeral_5m_input_tokens\": ..., \"ephemeral_1h_input_tokens\": ...},
      \"service_tier\": \"standard\",
      \"inference_geo\": \"not_available\"
    },
    \"content\": [{\"type\": \"tool_use\", \"...\": \"...\"}, ...]
  }
}

So the data exists, in a per-message format inside the subagent transcript file — accessible via transcript_path (BaseHookInput common field).

The schema shown in the docs (per #52607's quote) confirms there is no aggregated usage/duration_ms in tool_response:

type PostToolUseHookInput = BaseHookInput & {
  hook_event_name: \"PostToolUse\";
  tool_name: string;
  tool_input: unknown;
  tool_response: unknown;     // ← actually opaque depending on tool
  tool_use_id: string;
};

Why this matters

  1. Silent failure mode: a hook that \"runs successfully\" (exit 0, JSONL grows) but accumulates null metrics passes superficial review. Detection requires explicit cross-validation against transcript files. This is exactly the failure class #52047 anticipated.
  2. Existing community examples mislead: several public examples of telemetry hooks assume tool_response.usage exists. They produce null silently.
  3. Documentation alone could prevent re-occurrence: explicit note in the PostToolUse reference saying \"metrics live in transcript file, not in tool_response\" + example of how to read them via input.transcript_path would close the loop.

Constructive request

Could the hooks reference include:

  1. For PostToolUse on Agent/Task tools: explicit mention that aggregate metrics (total_tokens, tool_uses, duration_ms) live in the subagent transcript file, NOT in tool_response. Point to input.transcript_path as the access path.
  1. Schema example of tool_response for the Agent/Task case (currently \"unknown\" type in the TypeScript definition). Even a partial example showing agentId, worktreePath, etc. would help.
  1. Cross-link to the JSONL transcript format docs (already an OPEN concern in #55240).

Cross-refs

  • #52607 (OPEN) — same gap: hook input references omit duration_ms for PostToolUse
  • #52047 (CLOSED) — feature request for duration_ms in PostToolUse (we hit exactly this need)
  • #55240 (OPEN) — JSONL session format wait-vs-execute distinction (related transcript format concern)
  • #19170 (CLOSED) — similar gap for SubagentStart schema documentation
  • #27755 (CLOSED) — SubagentStart/SubagentStop unreliable for Task tool dispatches (rules out a possible workaround)

What we built as workaround

We're building v0.2 of our hook that reads input.transcript_path, parses the subagent transcript, and aggregates per-type=assistant message.usage. Happy to share the extractor pattern if useful for docs.

Environment

  • Claude Code CLI
  • macOS Darwin 24.6.0
  • 24h+ of accumulated null entries reproduced consistently
  • Subagent transcripts always present at expected path after invocation

Thanks for considering — the platform is excellent, just need a small docs clarification to prevent the next external user from hitting this same silent failure.

View original on GitHub ↗

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