#!/usr/bin/env node // post-bash-stats.mjs — PostToolUse hook (CC v2.1.97+) // // Captures duration_ms from PostToolUse payload for Bash tool calls and // appends a structured stats line to ${CLAUDE_PLUGIN_DATA}/trekexecute-stats.jsonl // when the running session is an trekexecute session. // // Detection: only fires when the tool input matches the verify/checkpoint // pattern of an trekexecute step (i.e., the command was issued from inside // /trekexecute). We err on the side of "log everything in plugin // scope" — duration data is cheap and the alternative is missing real // per-step timings. // // Fail-open invariant: any error → exit 0, no output, no log line. import { stdin } from 'node:process'; import { appendFileSync, mkdirSync } from 'node:fs'; import { dirname, join } from 'node:path'; async function readStdin() { let data = ''; for await (const chunk of stdin) data += chunk; return data; } (async () => { try { const raw = await readStdin(); if (!raw.trim()) return; const payload = JSON.parse(raw); if (payload.tool_name !== 'Bash') return; const duration = payload.duration_ms; if (typeof duration !== 'number') return; const dataDir = process.env.CLAUDE_PLUGIN_DATA; if (!dataDir) return; const cmd = payload.tool_input?.command || ''; if (!cmd) return; const line = JSON.stringify({ ts: new Date().toISOString(), session_id: payload.session_id || null, command_excerpt: cmd.slice(0, 120), duration_ms: duration, success: payload.tool_response?.success !== false, }); const target = join(dataDir, 'trekexecute-stats.jsonl'); try { mkdirSync(dirname(target), { recursive: true }); } catch {} appendFileSync(target, line + '\n'); } catch { // fail open } })();