From 85a61d7253e66ec130158151ddc73465a01cf9fb Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Sat, 2 May 2026 21:38:47 +0800 Subject: [PATCH] fix(hooks): post-audit hook chain hardening + 4 new defensive hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- hooks/alignment-check.sh | 2 +- hooks/block-dangerous.sh | 4 +- hooks/chat-numeric-postflag.sh | 77 ++++++++++++++++++++ hooks/chat-numeric-prewarn.sh | 48 ++++++++++++ hooks/check-error-patterns.sh | 43 ++--------- hooks/citation-verify.sh | 2 +- hooks/destructive-guard.sh | 7 +- hooks/no-downgrade.sh | 4 +- hooks/no-github-push.sh | 86 ++++++++++++++++++++++ hooks/numeric-claims-guard.sh | 8 +- hooks/post-commit-audit.sh | 2 + hooks/secrets-pre-guard.sh | 129 +++++++++++++++++++++++++++++++++ hooks/task-timer.sh | 17 +++-- settings-snippet.json | 35 ++++++--- 14 files changed, 401 insertions(+), 63 deletions(-) create mode 100755 hooks/chat-numeric-postflag.sh create mode 100755 hooks/chat-numeric-prewarn.sh create mode 100755 hooks/no-github-push.sh create mode 100755 hooks/secrets-pre-guard.sh diff --git a/hooks/alignment-check.sh b/hooks/alignment-check.sh index 941dff5..9d08782 100755 --- a/hooks/alignment-check.sh +++ b/hooks/alignment-check.sh @@ -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 diff --git a/hooks/block-dangerous.sh b/hooks/block-dangerous.sh index 1ffc66b..4595b86 100755 --- a/hooks/block-dangerous.sh +++ b/hooks/block-dangerous.sh @@ -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 diff --git a/hooks/chat-numeric-postflag.sh b/hooks/chat-numeric-postflag.sh new file mode 100755 index 0000000..fc18969 --- /dev/null +++ b/hooks/chat-numeric-postflag.sh @@ -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 < /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": "\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: ]\n [FROM-JOURNAL: ~/.claude/memory/time-metrics/.jsonl#]\n [ESTIMATE-HTC: ]\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" + } +} +EOF + +exit 0 diff --git a/hooks/check-error-patterns.sh b/hooks/check-error-patterns.sh index 14d0c80..c125710 100755 --- a/hooks/check-error-patterns.sh +++ b/hooks/check-error-patterns.sh @@ -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 diff --git a/hooks/citation-verify.sh b/hooks/citation-verify.sh index 171e7cc..7f473e9 100755 --- a/hooks/citation-verify.sh +++ b/hooks/citation-verify.sh @@ -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 diff --git a/hooks/destructive-guard.sh b/hooks/destructive-guard.sh index 1315f4f..9d3665c 100755 --- a/hooks/destructive-guard.sh +++ b/hooks/destructive-guard.sh @@ -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 diff --git a/hooks/no-downgrade.sh b/hooks/no-downgrade.sh index 0501d1a..8bcf491 100755 --- a/hooks/no-downgrade.sh +++ b/hooks/no-downgrade.sh @@ -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) diff --git a/hooks/no-github-push.sh b/hooks/no-github-push.sh new file mode 100755 index 0000000..725845c --- /dev/null +++ b/hooks/no-github-push.sh @@ -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@//.git + git push origin + +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 diff --git a/hooks/numeric-claims-guard.sh b/hooks/numeric-claims-guard.sh index cf92339..28570de 100755 --- a/hooks/numeric-claims-guard.sh +++ b/hooks/numeric-claims-guard.sh @@ -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 < /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||\[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 < + + 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 diff --git a/hooks/task-timer.sh b/hooks/task-timer.sh index 54e6d17..8d35a06 100755 --- a/hooks/task-timer.sh +++ b/hooks/task-timer.sh @@ -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 diff --git a/settings-snippet.json b/settings-snippet.json index f2a70a0..ea4e3c9 100644 --- a/settings-snippet.json +++ b/settings-snippet.json @@ -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)..." } ] }