KeiSeiKit-1.0/hooks/task-timer.sh
Parfii-bot 4afc85ca30 fix(hooks): post-audit hook chain hardening + 4 new defensive hooks
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>
2026-05-02 21:38:47 +08:00

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