Hook chain repairs (Group A):
- alignment-check.sh: read .prompt (was .user_prompt) — hook was dead
- block-dangerous.sh: jq instead of inline interpreter (RULE 0.2 + fail-open fix)
- destructive-guard.sh: explicit INPUT=cat + jq guard + exit 0 — was silent no-op
- numeric-claims-guard.sh: exit 1 -> exit 2 (Claude Code spec — was non-blocking)
comments updated 0.17 -> 0.18 (env var name kept)
- no-downgrade.sh: removed (?i) PCRE syntax — POSIX ERE matched literal text
- task-timer.sh: jq -nc instead of bare printf — JSON injection on quotes/backslashes
in description was corrupting RULE 0.18 evidence journal
- check-error-patterns.sh: replaced with no-op stub — had hardcoded /Users/denis/...
PATH LEAK in public kit, plus inline interpreter use
- post-commit-audit.sh: added trailing exit 0 — grep return code was hook exit code
- citation-verify.sh: ALLOW_REGEX accepts HOOK-BYPASS marker — bypass was documented
but never matched
- settings-snippet.json: agent-stub-scan moved PreToolUse:Agent -> PostToolUse:Agent
(RULE 0.16 enforcement was firing before transcript existed)
- check-error-patterns hook removed from settings-snippet.json
New defensive hooks (Group H):
- no-github-push.sh: PreToolUse:Bash hard deny on github.com push/create/sync/remote-add
(RULE 0.1 — patent IP protection; was missing from public kit)
- secrets-pre-guard.sh: PreToolUse:Edit|Write — token-pattern scan with allowlist (RULE 0.8)
- chat-numeric-prewarn.sh: UserPromptSubmit reminder when prompt mentions time/cost
(RULE 0.18 chat extension)
- chat-numeric-postflag.sh: Stop event scans last assistant message for naked numerics
without REAL/FROM-JOURNAL/ESTIMATE-HTC markers
Source: full Sonnet test-retest audit 2026-05-02 (3 parallel waves of 6 agents each)
identified hook chain bugs as HIGH severity in all 3 runs independently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
84 lines
3.5 KiB
Bash
Executable file
84 lines
3.5 KiB
Bash
Executable file
#!/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))
|
|
jq -nc --arg id "$SESSION_ID" --arg ts "$NOW_ISO" \
|
|
--argjson start "$START" --argjson end "$NOW_EPOCH" --argjson dur "$DURATION" \
|
|
'{"kind":"session","id":$id,"start_epoch":$start,"end_epoch":$end,"duration_s":$dur,"ts":$ts}' \
|
|
>> "$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"
|
|
jq -nc --arg id "$AGENT_ID" --arg desc "$DESC" --arg type "$AGENT_TYPE" \
|
|
--argjson start "$NOW_EPOCH" \
|
|
'{"id":$id,"desc":$desc,"type":$type,"start_epoch":$start}' \
|
|
> "$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))
|
|
jq -nc --arg id "$AGENT_ID" --arg desc "$DESC" --arg type "$AGENT_TYPE" \
|
|
--arg ts "$NOW_ISO" \
|
|
--argjson start "$START_EPOCH" --argjson end "$NOW_EPOCH" --argjson dur "$DURATION" \
|
|
'{"kind":"task","id":$id,"desc":$desc,"type":$type,"start_epoch":$start,"end_epoch":$end,"duration_s":$dur,"ts":$ts}' \
|
|
>> "$JOURNAL_DIR/tasks.jsonl"
|
|
rm -f "$TASK_START"
|
|
fi
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
# Always exit 0 — this hook is observability, never blocks.
|
|
exit 0
|