#!/usr/bin/env bash # RULE 0.18 — task/session time tracker. Appends durations to JSONL # journals so future estimates have real data to cite. # # Three modes selected by hook_event_name from JSON stdin: # - "Stop" → write session-end record to sessions.jsonl # - "PreToolUse" → record agent-spawn start (Agent tool only) # - "PostToolUse" → record agent-spawn end + duration # # Modern Claude Code passes hook info via JSON stdin: # {"hook_event_name":"...","tool_name":"...","tool_input":{...}, # "tool_use_id":"...","session_id":"..."} # Older env-var protocol (CLAUDE_HOOK_EVENT) is kept as fallback. set -uo pipefail JOURNAL_DIR="$HOME/.claude/memory/time-metrics" mkdir -p "$JOURNAL_DIR" INPUT="$(cat 2>/dev/null || true)" EVENT="$(printf '%s' "$INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null)" [[ -z "$EVENT" ]] && EVENT="${CLAUDE_HOOK_EVENT:-unknown}" SESSION_ID="$(printf '%s' "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)" [[ -z "$SESSION_ID" ]] && SESSION_ID="${CLAUDE_SESSION_ID:-unknown}" NOW_EPOCH="$(date +%s)" NOW_ISO="$(date -u +%Y-%m-%dT%H:%M:%SZ)" case "$EVENT" in Stop) START_FILE="$JOURNAL_DIR/.session-${SESSION_ID}.start" if [[ -f "$START_FILE" ]]; then START="$(cat "$START_FILE")" DURATION=$((NOW_EPOCH - START)) printf '{"kind":"session","id":"%s","start_epoch":%s,"end_epoch":%s,"duration_s":%s,"ts":"%s"}\n' \ "$SESSION_ID" "$START" "$NOW_EPOCH" "$DURATION" "$NOW_ISO" \ >> "$JOURNAL_DIR/sessions.jsonl" rm -f "$START_FILE" fi ;; PreToolUse) TOOL_NAME="$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)" if [[ "$TOOL_NAME" = "Agent" ]]; then START_FILE="$JOURNAL_DIR/.session-${SESSION_ID}.start" [[ -f "$START_FILE" ]] || echo "$NOW_EPOCH" > "$START_FILE" AGENT_ID="$(printf '%s' "$INPUT" | jq -r '.tool_use_id // empty' 2>/dev/null)" DESC="$(printf '%s' "$INPUT" | jq -r '.tool_input.description // empty' 2>/dev/null)" AGENT_TYPE="$(printf '%s' "$INPUT" | jq -r '.tool_input.subagent_type // "fork"' 2>/dev/null)" if [[ -n "$AGENT_ID" ]]; then TASK_START="$JOURNAL_DIR/.task-${AGENT_ID}.start" printf '{"id":"%s","desc":"%s","type":"%s","start_epoch":%s}' \ "$AGENT_ID" "$DESC" "$AGENT_TYPE" "$NOW_EPOCH" > "$TASK_START" fi fi ;; PostToolUse) TOOL_NAME="$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)" if [[ "$TOOL_NAME" = "Agent" ]]; then AGENT_ID="$(printf '%s' "$INPUT" | jq -r '.tool_use_id // empty' 2>/dev/null)" TASK_START="$JOURNAL_DIR/.task-${AGENT_ID}.start" if [[ -f "$TASK_START" ]]; then START_RAW="$(cat "$TASK_START")" START_EPOCH="$(echo "$START_RAW" | jq -r '.start_epoch')" DESC="$(echo "$START_RAW" | jq -r '.desc')" AGENT_TYPE="$(echo "$START_RAW" | jq -r '.type')" DURATION=$((NOW_EPOCH - START_EPOCH)) printf '{"kind":"task","id":"%s","desc":"%s","type":"%s","start_epoch":%s,"end_epoch":%s,"duration_s":%s,"ts":"%s"}\n' \ "$AGENT_ID" "$DESC" "$AGENT_TYPE" "$START_EPOCH" "$NOW_EPOCH" "$DURATION" "$NOW_ISO" \ >> "$JOURNAL_DIR/tasks.jsonl" rm -f "$TASK_START" fi fi ;; esac # Always exit 0 — this hook is observability, never blocks. exit 0