[DOCS] PostToolUse hook input schema for Agent/Task tools: reproducible evidence of telemetry extraction failure mode
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
- Configure
~/.claude/settings.json:
``json``
{"hooks": {"PostToolUse": [{"matcher": "Agent|Task", "hooks": [
{"type": "command", "command": "node /path/to/extractor.cjs"}
]}]}}
- Hook script reads stdin JSON and attempts to extract
input.tool_response.usage.total_tokens(the path many open-source examples assume). - Invoke any Agent (
Agent({subagent_type: \"X\", ...})) → hook fires. - Observed: extracted value is
nullfor every invocation. - 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
- 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.
- Existing community examples mislead: several public examples of telemetry hooks assume
tool_response.usageexists. They produce null silently. - 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_pathwould close the loop.
Constructive request
Could the hooks reference include:
- For PostToolUse on Agent/Task tools: explicit mention that aggregate metrics (
total_tokens,tool_uses,duration_ms) live in the subagent transcript file, NOT intool_response. Point toinput.transcript_pathas the access path.
- Schema example of
tool_responsefor the Agent/Task case (currently \"unknown\" type in the TypeScript definition). Even a partial example showingagentId,worktreePath, etc. would help.
- 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_msfor PostToolUse - #52047 (CLOSED) — feature request for
duration_msin PostToolUse (we hit exactly this need) - #55240 (OPEN) — JSONL session format wait-vs-execute distinction (related transcript format concern)
- #19170 (CLOSED) — similar gap for
SubagentStartschema 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.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗