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>
This commit is contained in:
Parfii-bot 2026-05-02 21:38:47 +08:00
parent 897d010802
commit 85a61d7253
14 changed files with 401 additions and 63 deletions

View file

@ -4,7 +4,7 @@
# THREE-TIME REPEAT BUG: exp6, exp24-28, basecaller — all forgot alignment.
INPUT=$(cat)
PROMPT=$(printf '%s' "$INPUT" | jq -r '.user_prompt // empty' 2>/dev/null)
PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty' 2>/dev/null)
[ -z "$PROMPT" ] && exit 0
# Detect comparison/experiment keywords

View file

@ -1,8 +1,10 @@
#!/bin/bash
# Block dangerous commands that could cause irreversible damage
command -v jq >/dev/null 2>&1 || exit 0
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
# Block patterns
if echo "$COMMAND" | grep -qE 'rm\s+-rf\s+(/|~|\$HOME|/Users)'; then

77
hooks/chat-numeric-postflag.sh Executable file
View file

@ -0,0 +1,77 @@
#!/bin/sh
# chat-numeric-postflag.sh — Stop warn (RULE 0.18 chat-output)
#
# Reads the session transcript, extracts the last assistant message,
# and scans it for naked numeric claims that lack a RULE 0.18 evidence
# marker within 100 characters of the number.
#
# Severity: warn — always exits 0, emits stderr on violation.
# Never blocks; this is a post-session audit hook.
#
# Bypass: set RULE_018_CHAT_BYPASS=1 in the calling environment.
set -u
if [ "${RULE_018_CHAT_BYPASS:-0}" = "1" ]; then
exit 0
fi
if ! command -v jq > /dev/null 2>&1; then
exit 0
fi
INPUT=$(cat)
TRANSCRIPT_PATH=$(printf '%s' "$INPUT" \
| jq -r '.transcript_path // empty' 2>/dev/null)
[ -z "$TRANSCRIPT_PATH" ] && exit 0
[ ! -f "$TRANSCRIPT_PATH" ] && exit 0
# Extract last assistant message text from the JSONL transcript.
# Each line is a JSON object; assistant messages have role="assistant".
# We want the last one.
LAST_MSG=$(grep '"role":"assistant"' "$TRANSCRIPT_PATH" 2>/dev/null \
| tail -1 \
| jq -r '.content // empty' 2>/dev/null)
[ -z "$LAST_MSG" ] && exit 0
# Numeric claim pattern: optional ~ + digits + unit
# Units: min, hour, day, week, MB, GB, LOC, tests, crates, atomars, %, $N,
# минут, часов, дней, недель (Russian time units)
NUMERIC_RE='~?[0-9]+[[:space:]]*(min|minute|hour|hr|day|week|month|MB|GB|KB|LOC|test|crate|atomar|%|минут|часов|дней|недел)'
# Evidence marker pattern
MARKER_RE='\[REAL:|\[FROM-JOURNAL:|\[ESTIMATE-HTC:'
# Quick check: does the message contain any numeric claim at all?
if ! printf '%s' "$LAST_MSG" | grep -iqE "$NUMERIC_RE"; then
exit 0
fi
# Quick check: does the message contain at least one marker?
# If it does, we assume the author was compliant (shallow check).
# A deeper per-match proximity check would require awk/perl.
if printf '%s' "$LAST_MSG" | grep -qE "$MARKER_RE"; then
exit 0
fi
# No marker found anywhere in the message — extract a short excerpt for context
EXCERPT=$(printf '%s' "$LAST_MSG" \
| grep -ioE "$NUMERIC_RE" \
| head -3 \
| tr '\n' ' ')
COUNT=$(printf '%s' "$LAST_MSG" \
| grep -ioE "$NUMERIC_RE" \
| wc -l \
| tr -d ' ')
cat >&2 <<EOF
[chat-numeric-postflag] WARN — assistant emitted ${COUNT} naked numeric claim(s) without RULE 0.18 marker.
First example(s): ${EXCERPT}
Required markers: [REAL: ...] [FROM-JOURNAL: ...] [ESTIMATE-HTC: ...]
See: ~/.claude/rules/chat-numeric-pre-output.md
EOF
exit 0

48
hooks/chat-numeric-prewarn.sh Executable file
View file

@ -0,0 +1,48 @@
#!/bin/sh
# chat-numeric-prewarn.sh — UserPromptSubmit remind (RULE 0.18 chat-output)
#
# Detects time/cost/effort keywords in the user's prompt and injects an
# additionalContext reminder asking the assistant to attach RULE 0.18
# evidence markers before emitting any numeric claim in its response.
#
# Severity: remind — always exits 0, never blocks.
#
# Bypass: set RULE_018_CHAT_BYPASS=1 in the calling environment.
set -u
if [ "${RULE_018_CHAT_BYPASS:-0}" = "1" ]; then
exit 0
fi
if ! command -v jq > /dev/null 2>&1; then
exit 0
fi
INPUT=$(cat)
PROMPT=$(printf '%s' "$INPUT" | jq -r '.prompt // empty' 2>/dev/null)
[ -z "$PROMPT" ] && exit 0
PROMPT_LC=$(printf '%s' "$PROMPT" | tr '[:upper:]' '[:lower:]')
# Keywords that imply the user is asking for a time/cost/effort estimate
MATCH=0
if printf '%s' "$PROMPT_LC" | grep -qE \
'сколько|как долго|estimate|how long|how much|duration|time|effort|займёт|сколько стоит|cost|стоимость|за сколько|за (сколько|это)'; then
MATCH=1
fi
[ "$MATCH" -eq 0 ] && exit 0
# Emit additionalContext JSON to stdout (Claude Code hook protocol)
cat <<'EOF'
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "<rule-018-chat-prewarn>\nRULE 0.18 REMINDER — user prompt contains time/cost/effort keywords.\n\nBefore emitting ANY duration, count, cost, size, or percentage claim in your response, attach one of these evidence markers inline:\n\n [REAL: <source — file:line, commit SHA, or timestamp>]\n [FROM-JOURNAL: ~/.claude/memory/time-metrics/<file>.jsonl#<id>]\n [ESTIMATE-HTC: <one sentence: why this cannot be measured precisely>]\n\nNaked numbers are forbidden by RULE 0.18 (lock 2026-04-29).\nIf you do not have a journal entry for the task, use [ESTIMATE-HTC:] and state the reason.\nDo NOT fabricate a number from latent space — refusal to estimate is preferred over a false estimate.\n\nSee: ~/.claude/rules/chat-numeric-pre-output.md\n</rule-018-chat-prewarn>"
}
}
EOF
exit 0

View file

@ -1,39 +1,8 @@
#!/bin/bash
# Pre-deploy hook: check error-patterns.json for recurring/critical issues
# Exit 0 = allow, Exit 2 = block
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
# Only check deploy-related commands
if echo "$COMMAND" | grep -qiE 'docker.*(build|up|push)|deploy|rsync.*server|ssh.*docker'; then
PATTERNS_FILE="/Users/denis/projects/ai machine learning/error-patterns.json"
if [ -f "$PATTERNS_FILE" ]; then
# Find critical/recurring patterns
WARNINGS=$(python3 -c "
import json
try:
with open('$PATTERNS_FILE') as f:
patterns = json.load(f)
critical = [p for p in patterns if p.get('severity') == 'critical' or p.get('frequency') == 'recurring']
if critical:
print('PRE-DEPLOY WARNING - Check these known patterns:')
for p in critical[:5]:
print(f\" [{p.get('severity','?')}] {p.get('id','?')}: {p.get('name','?')}\")
print(f\" Trigger: {p.get('trigger','?')}\")
print()
print('Verify these do not apply to current deploy.')
except:
pass
" 2>/dev/null)
if [ -n "$WARNINGS" ]; then
echo "$WARNINGS" >&2
# Warn but don't block (exit 0)
exit 0
fi
fi
fi
# DELETED — 2026-05-02
# Reasons:
# 1. Hardcoded path leak: /Users/denis/projects/ai machine learning/error-patterns.json
# 2. RULE 0.2 violation: used python3 for JSON parsing
# 3. No-op on every machine except original author's
# Removed from settings-snippet.json PostToolUse matcher "*" block.
exit 0

View file

@ -50,7 +50,7 @@ ALL_HITS=$(printf '%s\n%s\n%s' "$HITS_A" "$HITS_B" "$HITS_C" | grep -v '^$' || t
[ -z "$ALL_HITS" ] && exit 0
# Allowlist: explicit verification or retraction context
ALLOW_REGEX='\[VERIFIED:|\[UNVERIFIED\]|\[FABRICATED|\[RETRACTED|\[MISATTRIBUTED|FABRICATED|RETRACTED 2026|MISATTRIBUTED|NOT FOUND|unverifiable|misattributed|does NOT exist|do NOT EXIST|are fabricated|were fabricated'
ALLOW_REGEX='\[VERIFIED:|\[UNVERIFIED\]|\[FABRICATED|\[RETRACTED|\[MISATTRIBUTED|FABRICATED|RETRACTED 2026|MISATTRIBUTED|NOT FOUND|unverifiable|misattributed|does NOT exist|do NOT EXIST|are fabricated|were fabricated|\[HOOK-BYPASS:[[:space:]]*citation-verify'
if printf '%s' "$CONTENT" | grep -qE "$ALLOW_REGEX"; then
exit 0
fi

View file

@ -2,9 +2,14 @@
# Guard against destructive actions that could damage running experiments.
# Returns JSON with block decision if destructive command detected.
CMD=$(jq -r '.tool_input.command // empty')
command -v jq >/dev/null 2>&1 || exit 0
INPUT=$(cat 2>/dev/null || true)
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
# Check if command contains destructive patterns
if echo "$CMD" | grep -qEi '(^|\s|sudo\s+)(pkill|kill|killall)\b|rm\s+-rf?\b|reboot|shutdown|systemctl\s+(stop|restart)|docker\s+(rm|stop|kill)|drop\s+table|truncate|git\s+reset\s+--hard|git\s+clean\s+-f'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"⚠️ Destructive action detected. Verify this will not damage a running experiment or data collection."}}'
fi
exit 0

View file

@ -36,11 +36,11 @@ esac
# Downgrade triggers (case-insensitive, word-boundary where possible)
# derived: incident catalog from 2026-04-14 chatlogs + 2026-04-24 live session
TRIGGERS='(?i)\b(failed|refuted|doesn.?t work|downgrade|accept as limitation|не работает|не сработало|провалился|не удалось|tautolog(y|ical)|rejected?|dismiss|give up|отказываемся|отступаем|неудача|провал|это (всё\s+)?что мы)\b'
TRIGGERS='\b(failed|refuted|doesn.?t work|downgrade|accept as limitation|не работает|не сработало|провалился|не удалось|tautolog(y|ical)|rejected?|dismiss|give up|отказываемся|отступаем|неудача|провал|это (всё\s+)?что мы)\b'
# Constructive rescue markers — if ANY of these present, downgrade is OK
# because the agent provided solution paths (RULE -1 compliance).
RESCUE='(?i)(three paths|3 paths|variant A|option A|вариант[аы]?\s+решения|solution paths?|constructive|recommend [AB]|три пути|можем попробовать|proposed fix|root cause.*fix|альтернативный путь|next step|решения\s*:)'
RESCUE='(three paths|3 paths|variant A|option A|вариант[аы]?\s+решения|solution paths?|constructive|recommend [AB]|три пути|можем попробовать|proposed fix|root cause.*fix|альтернативный путь|next step|решения\s*:)'
HAS_TRIGGER=$(echo "$CONTENT" | grep -ciE "$TRIGGERS" || true)
HAS_RESCUE=$(echo "$CONTENT" | grep -ciE "$RESCUE" || true)

86
hooks/no-github-push.sh Executable file
View file

@ -0,0 +1,86 @@
#!/bin/sh
# no-github-push.sh — PreToolUse:Bash hard deny (RULE 0.1 NO GITHUB PUSH)
#
# Blocks any Bash command that would push code to github.com.
# KeiTech portfolio contains unfiled patent IP — a public push destroys
# priority date and trade secrets. Irrecoverable.
#
# Exit codes:
# 0 = pass (command is safe)
# 2 = block (Claude Code aborts the tool call)
#
# Bypass: set KEI_NO_GITHUB_PUSH_BYPASS=1 in the calling environment.
# Even with bypass, the rule is logged to stderr.
set -u
# Bypass check (must be explicit env, not embedded in command string)
if [ "${KEI_NO_GITHUB_PUSH_BYPASS:-0}" = "1" ]; then
printf '[no-github-push] BYPASS active (KEI_NO_GITHUB_PUSH_BYPASS=1). Proceeding.\n' >&2
exit 0
fi
# jq is required to parse the Claude Code hook input
if ! command -v jq > /dev/null 2>&1; then
exit 0
fi
INPUT=$(cat)
COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
[ -z "$COMMAND" ] && exit 0
# --- Pattern matching -------------------------------------------------------
# Match any of the forbidden surfaces (case-sensitive; github URLs are
# always lowercase in practice, but we anchor on the protocol/domain).
BLOCKED=0
# git push to github.com (HTTPS or SSH)
if printf '%s' "$COMMAND" | grep -qE 'git[[:space:]]+push[^|&;]*github\.com'; then
BLOCKED=1
fi
# git push to SSH shorthand git@github.com
if [ "$BLOCKED" -eq 0 ] && \
printf '%s' "$COMMAND" | grep -qE 'git[[:space:]]+push[^|&;]*git@github\.com'; then
BLOCKED=1
fi
# gh repo create (any visibility — creating a public repo leaks IP by default)
if [ "$BLOCKED" -eq 0 ] && \
printf '%s' "$COMMAND" | grep -qE 'gh[[:space:]]+repo[[:space:]]+create'; then
BLOCKED=1
fi
# gh repo sync (pushes local state to remote)
if [ "$BLOCKED" -eq 0 ] && \
printf '%s' "$COMMAND" | grep -qE 'gh[[:space:]]+repo[[:space:]]+sync'; then
BLOCKED=1
fi
# git remote add/set-url pointing at github.com
if [ "$BLOCKED" -eq 0 ] && \
printf '%s' "$COMMAND" | grep -qE 'git[[:space:]]+remote[[:space:]]+(add|set-url)[^|&;]*github\.com'; then
BLOCKED=1
fi
[ "$BLOCKED" -eq 0 ] && exit 0
# --- Block ------------------------------------------------------------------
cat >&2 <<'EOF'
[no-github-push] BLOCK — RULE 0.1 NO GITHUB PUSH
KeiTech portfolio contains unfiled patent IP. Public push destroys
priority date + trade secrets. Irrecoverable.
Use a private remote instead (Forgejo, Gitea, self-hosted):
git remote set-url origin ssh://git@<private-host>/<user>/<repo>.git
git push origin <branch>
Bypass (visible, per-call):
Set env KEI_NO_GITHUB_PUSH_BYPASS=1 before the command.
You must also add confirmation phrase: "yes, push patent code to github"
+ "confirm publication" in the session turn.
EOF
exit 2

View file

@ -1,6 +1,6 @@
#!/usr/bin/env bash
# RULE 0.17 enforcement — block Edit/Write of numeric claims without
# evidence marker. Bypass: RULE_017_BYPASS=1 prefix.
# RULE 0.18 — Numeric claim enforcement — block Edit/Write of numeric claims
# without evidence marker. Bypass: RULE_017_BYPASS=1 prefix (kept for compat).
#
# Reads tool-call JSON on stdin (Claude Code hook protocol).
@ -46,7 +46,7 @@ MATCHED="$(echo "$NEW_CONTENT" | grep -iEo "$NUMERIC_PATTERN" | head -3 | tr '\n
cat >&2 <<EOF
═══════════════════════════════════════════════════════════════════
RULE 0.17 — Numeric claim without evidence marker.
RULE 0.18 — Numeric claim without evidence marker.
═══════════════════════════════════════════════════════════════════
Found in Edit/Write content:
@ -70,4 +70,4 @@ See: ~/.claude/rules/numeric-claims-evidence.md
═══════════════════════════════════════════════════════════════════
EOF
exit 1
exit 2

View file

@ -13,3 +13,5 @@ if echo "$CMD" | grep -qE 'git\s+commit'; then
echo "═══════════════════════════════════════════════════"
echo ""
fi
exit 0

129
hooks/secrets-pre-guard.sh Executable file
View file

@ -0,0 +1,129 @@
#!/bin/sh
# secrets-pre-guard.sh — PreToolUse:Edit|Write hard deny (RULE 0.8 SECRETS)
#
# Scans the content being written for hardcoded secret tokens.
# If a live secret pattern is detected, exits 2 (block) and instructs
# the author to move the value to ~/.claude/secrets/.env.
#
# Exit codes:
# 0 = pass
# 2 = block (Claude Code aborts the tool call)
#
# Bypass: set KEI_SECRETS_GUARD_BYPASS=1 in the calling environment.
set -u
if [ "${KEI_SECRETS_GUARD_BYPASS:-0}" = "1" ]; then
exit 0
fi
if ! command -v jq > /dev/null 2>&1; then
exit 0
fi
INPUT=$(cat)
# Extract the file path being written/edited
FILE_PATH=$(printf '%s' "$INPUT" | jq -r \
'.tool_input.path // .tool_input.file_path // empty' 2>/dev/null)
# --- Allowlisted paths (secrets live here intentionally) -------------------
case "$FILE_PATH" in
*/secrets/*.env|*/secrets/.env|*.env.example|*.env.template)
exit 0
;;
esac
# Extract the content being written
CONTENT=$(printf '%s' "$INPUT" | jq -r \
'.tool_input.new_string // .tool_input.content // empty' 2>/dev/null)
[ -z "$CONTENT" ] && exit 0
# --- Allowlist: placeholder or documentation patterns ----------------------
# If the content indicates example/placeholder values, skip.
if printf '%s' "$CONTENT" | grep -qiE \
'YOUR_TOKEN_HERE|<redacted>|\[VERIFY:|placeholder|xxx+|_TOKEN_NAME_HERE|_KEY_HERE|_SECRET_HERE|example[_-]?(key|token|secret)'; then
exit 0
fi
# --- Secret detection patterns -------------------------------------------
# Each pattern is checked individually so we can name the type in the error.
DETECTED=""
# Anthropic/OpenAI legacy key
if printf '%s' "$CONTENT" | grep -qE 'sk-[A-Za-z0-9]{20,}'; then
DETECTED="Anthropic/OpenAI legacy key (sk-...)"
fi
# Anthropic current key
if [ -z "$DETECTED" ] && \
printf '%s' "$CONTENT" | grep -qE 'sk-ant-[A-Za-z0-9_-]{40,}'; then
DETECTED="Anthropic current key (sk-ant-...)"
fi
# GitHub classic PAT
if [ -z "$DETECTED" ] && \
printf '%s' "$CONTENT" | grep -qE 'ghp_[A-Za-z0-9]{36}'; then
DETECTED="GitHub classic PAT (ghp_...)"
fi
# GitHub fine-grained PAT
if [ -z "$DETECTED" ] && \
printf '%s' "$CONTENT" | grep -qE 'github_pat_[A-Za-z0-9_]{82}'; then
DETECTED="GitHub fine-grained PAT (github_pat_...)"
fi
# Slack bot token
if [ -z "$DETECTED" ] && \
printf '%s' "$CONTENT" | grep -qE 'xoxb-[0-9]+-[0-9]+-[A-Za-z0-9]+'; then
DETECTED="Slack bot token (xoxb-...)"
fi
# Telegram bot token
if [ -z "$DETECTED" ] && \
printf '%s' "$CONTENT" | grep -qE '[0-9]{8,10}:[A-Za-z0-9_-]{35}'; then
DETECTED="Telegram bot token (NNNNNNNNN:...)"
fi
# AWS access key
if [ -z "$DETECTED" ] && \
printf '%s' "$CONTENT" | grep -qE 'AKIA[A-Z0-9]{16}'; then
DETECTED="AWS access key (AKIA...)"
fi
# PEM private key block
if [ -z "$DETECTED" ] && \
printf '%s' "$CONTENT" | grep -qE '-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----'; then
DETECTED="PEM private key (-----BEGIN ... PRIVATE KEY-----)"
fi
[ -z "$DETECTED" ] && exit 0
# --- Block ------------------------------------------------------------------
cat >&2 <<EOF
[secrets-pre-guard] BLOCK — RULE 0.8 SECRETS SINGLE SOURCE
Detected hardcoded secret in content being written.
Type: $DETECTED
Hardcoding credentials in source files is forbidden (RULE 0.8).
Even .gitignored files expand the leak surface and resist rotation.
REMEDIATION:
1. Add the value to ~/.claude/secrets/.env (chmod 600):
VARIABLE_NAME=<value>
2. Reference it in code by env var name only:
Shell: source ~/.claude/secrets/.env && use \$VARIABLE_NAME
Python: os.environ["VARIABLE_NAME"]
Rust: std::env::var("VARIABLE_NAME")
3. Never paste the literal value in chat, commits, or docs.
Bypass (per-call, visible):
Set env KEI_SECRETS_GUARD_BYPASS=1 before the tool call.
Log the reason in your session chatlog.
EOF
exit 2

View file

@ -31,8 +31,9 @@ case "$EVENT" in
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" \
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
@ -49,8 +50,10 @@ case "$EVENT" in
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"
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
;;
@ -66,8 +69,10 @@ case "$EVENT" in
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" \
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

View file

@ -48,6 +48,11 @@
"type": "command",
"command": "~/.claude/hooks/agent-fork-done.sh",
"statusMessage": "agent-fork-done — close ledger lifecycle..."
},
{
"type": "command",
"command": "~/.claude/hooks/agent-stub-scan.sh",
"statusMessage": "STATUS-TRUTH marker scan (RULE 0.16)..."
}
]
},
@ -59,11 +64,6 @@
"command": "~/.claude/hooks/error-spike-detector.sh",
"statusMessage": "error-spike rolling window (RULE 0.14)..."
},
{
"type": "command",
"command": "~/.claude/hooks/check-error-patterns.sh",
"statusMessage": "error-pattern check..."
},
{
"type": "command",
"command": "~/.claude/hooks/agent-heartbeat-tick.sh"
@ -108,6 +108,11 @@
"type": "command",
"command": "~/.claude/hooks/no-python-without-approval.sh",
"statusMessage": "rust-first python gate (RULE 0.2)..."
},
{
"type": "command",
"command": "~/.claude/hooks/no-github-push.sh",
"statusMessage": "no-github-push guard (RULE 0.1)..."
}
]
},
@ -132,6 +137,11 @@
"type": "command",
"command": "~/.claude/hooks/citation-verify.sh",
"statusMessage": "citation-verify (RULE 0.4)..."
},
{
"type": "command",
"command": "~/.claude/hooks/secrets-pre-guard.sh",
"statusMessage": "secrets-pre-guard (RULE 0.8)..."
}
]
},
@ -163,11 +173,6 @@
"command": "~/.claude/hooks/orchestrator-branch-check.sh",
"statusMessage": "orchestrator branch ownership (RULE 0.13)..."
},
{
"type": "command",
"command": "~/.claude/hooks/agent-stub-scan.sh",
"statusMessage": "STATUS-TRUTH marker scan (RULE 0.16)..."
},
{
"type": "command",
"command": "~/.claude/hooks/task-timer.sh"
@ -197,6 +202,11 @@
{
"type": "command",
"command": "~/.claude/hooks/alignment-check.sh"
},
{
"type": "command",
"command": "~/.claude/hooks/chat-numeric-prewarn.sh",
"statusMessage": "chat-numeric-prewarn (RULE 0.18)..."
}
]
}
@ -223,6 +233,11 @@
"type": "command",
"command": "~/.claude/hooks/extract-task-durations.sh",
"statusMessage": "extract-task-durations — pull async durations from notifications..."
},
{
"type": "command",
"command": "~/.claude/hooks/chat-numeric-postflag.sh",
"statusMessage": "chat-numeric-postflag (RULE 0.18)..."
}
]
}