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:
parent
897d010802
commit
85a61d7253
14 changed files with 401 additions and 63 deletions
|
|
@ -4,7 +4,7 @@
|
||||||
# THREE-TIME REPEAT BUG: exp6, exp24-28, basecaller — all forgot alignment.
|
# THREE-TIME REPEAT BUG: exp6, exp24-28, basecaller — all forgot alignment.
|
||||||
|
|
||||||
INPUT=$(cat)
|
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
|
[ -z "$PROMPT" ] && exit 0
|
||||||
|
|
||||||
# Detect comparison/experiment keywords
|
# Detect comparison/experiment keywords
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Block dangerous commands that could cause irreversible damage
|
# Block dangerous commands that could cause irreversible damage
|
||||||
|
|
||||||
|
command -v jq >/dev/null 2>&1 || exit 0
|
||||||
|
|
||||||
INPUT=$(cat)
|
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
|
# Block patterns
|
||||||
if echo "$COMMAND" | grep -qE 'rm\s+-rf\s+(/|~|\$HOME|/Users)'; then
|
if echo "$COMMAND" | grep -qE 'rm\s+-rf\s+(/|~|\$HOME|/Users)'; then
|
||||||
|
|
|
||||||
77
hooks/chat-numeric-postflag.sh
Executable file
77
hooks/chat-numeric-postflag.sh
Executable 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
48
hooks/chat-numeric-prewarn.sh
Executable 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
|
||||||
|
|
@ -1,39 +1,8 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Pre-deploy hook: check error-patterns.json for recurring/critical issues
|
# DELETED — 2026-05-02
|
||||||
# Exit 0 = allow, Exit 2 = block
|
# Reasons:
|
||||||
|
# 1. Hardcoded path leak: /Users/denis/projects/ai machine learning/error-patterns.json
|
||||||
INPUT=$(cat)
|
# 2. RULE 0.2 violation: used python3 for JSON parsing
|
||||||
COMMAND=$(echo "$INPUT" | python3 -c "import json,sys; print(json.load(sys.stdin).get('tool_input',{}).get('command',''))" 2>/dev/null)
|
# 3. No-op on every machine except original author's
|
||||||
|
# Removed from settings-snippet.json PostToolUse matcher "*" block.
|
||||||
# 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
|
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|
|
||||||
|
|
@ -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
|
[ -z "$ALL_HITS" ] && exit 0
|
||||||
|
|
||||||
# Allowlist: explicit verification or retraction context
|
# 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
|
if printf '%s' "$CONTENT" | grep -qE "$ALLOW_REGEX"; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,14 @@
|
||||||
# Guard against destructive actions that could damage running experiments.
|
# Guard against destructive actions that could damage running experiments.
|
||||||
# Returns JSON with block decision if destructive command detected.
|
# 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
|
# 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
|
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."}}'
|
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"⚠️ Destructive action detected. Verify this will not damage a running experiment or data collection."}}'
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
|
|
||||||
|
|
@ -36,11 +36,11 @@ esac
|
||||||
|
|
||||||
# Downgrade triggers (case-insensitive, word-boundary where possible)
|
# Downgrade triggers (case-insensitive, word-boundary where possible)
|
||||||
# derived: incident catalog from 2026-04-14 chatlogs + 2026-04-24 live session
|
# 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
|
# Constructive rescue markers — if ANY of these present, downgrade is OK
|
||||||
# because the agent provided solution paths (RULE -1 compliance).
|
# 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_TRIGGER=$(echo "$CONTENT" | grep -ciE "$TRIGGERS" || true)
|
||||||
HAS_RESCUE=$(echo "$CONTENT" | grep -ciE "$RESCUE" || true)
|
HAS_RESCUE=$(echo "$CONTENT" | grep -ciE "$RESCUE" || true)
|
||||||
|
|
|
||||||
86
hooks/no-github-push.sh
Executable file
86
hooks/no-github-push.sh
Executable 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
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# RULE 0.17 enforcement — block Edit/Write of numeric claims without
|
# RULE 0.18 — Numeric claim enforcement — block Edit/Write of numeric claims
|
||||||
# evidence marker. Bypass: RULE_017_BYPASS=1 prefix.
|
# without evidence marker. Bypass: RULE_017_BYPASS=1 prefix (kept for compat).
|
||||||
#
|
#
|
||||||
# Reads tool-call JSON on stdin (Claude Code hook protocol).
|
# 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
|
cat >&2 <<EOF
|
||||||
═══════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════
|
||||||
RULE 0.17 — Numeric claim without evidence marker.
|
RULE 0.18 — Numeric claim without evidence marker.
|
||||||
═══════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
Found in Edit/Write content:
|
Found in Edit/Write content:
|
||||||
|
|
@ -70,4 +70,4 @@ See: ~/.claude/rules/numeric-claims-evidence.md
|
||||||
═══════════════════════════════════════════════════════════════════
|
═══════════════════════════════════════════════════════════════════
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
exit 1
|
exit 2
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,5 @@ if echo "$CMD" | grep -qE 'git\s+commit'; then
|
||||||
echo "═══════════════════════════════════════════════════"
|
echo "═══════════════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
|
|
||||||
129
hooks/secrets-pre-guard.sh
Executable file
129
hooks/secrets-pre-guard.sh
Executable 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
|
||||||
|
|
@ -31,8 +31,9 @@ case "$EVENT" in
|
||||||
if [[ -f "$START_FILE" ]]; then
|
if [[ -f "$START_FILE" ]]; then
|
||||||
START="$(cat "$START_FILE")"
|
START="$(cat "$START_FILE")"
|
||||||
DURATION=$((NOW_EPOCH - START))
|
DURATION=$((NOW_EPOCH - START))
|
||||||
printf '{"kind":"session","id":"%s","start_epoch":%s,"end_epoch":%s,"duration_s":%s,"ts":"%s"}\n' \
|
jq -nc --arg id "$SESSION_ID" --arg ts "$NOW_ISO" \
|
||||||
"$SESSION_ID" "$START" "$NOW_EPOCH" "$DURATION" "$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"
|
>> "$JOURNAL_DIR/sessions.jsonl"
|
||||||
rm -f "$START_FILE"
|
rm -f "$START_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
@ -49,8 +50,10 @@ case "$EVENT" in
|
||||||
AGENT_TYPE="$(printf '%s' "$INPUT" | jq -r '.tool_input.subagent_type // "fork"' 2>/dev/null)"
|
AGENT_TYPE="$(printf '%s' "$INPUT" | jq -r '.tool_input.subagent_type // "fork"' 2>/dev/null)"
|
||||||
if [[ -n "$AGENT_ID" ]]; then
|
if [[ -n "$AGENT_ID" ]]; then
|
||||||
TASK_START="$JOURNAL_DIR/.task-${AGENT_ID}.start"
|
TASK_START="$JOURNAL_DIR/.task-${AGENT_ID}.start"
|
||||||
printf '{"id":"%s","desc":"%s","type":"%s","start_epoch":%s}' \
|
jq -nc --arg id "$AGENT_ID" --arg desc "$DESC" --arg type "$AGENT_TYPE" \
|
||||||
"$AGENT_ID" "$DESC" "$AGENT_TYPE" "$NOW_EPOCH" > "$TASK_START"
|
--argjson start "$NOW_EPOCH" \
|
||||||
|
'{"id":$id,"desc":$desc,"type":$type,"start_epoch":$start}' \
|
||||||
|
> "$TASK_START"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
|
|
@ -66,8 +69,10 @@ case "$EVENT" in
|
||||||
DESC="$(echo "$START_RAW" | jq -r '.desc')"
|
DESC="$(echo "$START_RAW" | jq -r '.desc')"
|
||||||
AGENT_TYPE="$(echo "$START_RAW" | jq -r '.type')"
|
AGENT_TYPE="$(echo "$START_RAW" | jq -r '.type')"
|
||||||
DURATION=$((NOW_EPOCH - START_EPOCH))
|
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' \
|
jq -nc --arg id "$AGENT_ID" --arg desc "$DESC" --arg type "$AGENT_TYPE" \
|
||||||
"$AGENT_ID" "$DESC" "$AGENT_TYPE" "$START_EPOCH" "$NOW_EPOCH" "$DURATION" "$NOW_ISO" \
|
--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"
|
>> "$JOURNAL_DIR/tasks.jsonl"
|
||||||
rm -f "$TASK_START"
|
rm -f "$TASK_START"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,11 @@
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "~/.claude/hooks/agent-fork-done.sh",
|
"command": "~/.claude/hooks/agent-fork-done.sh",
|
||||||
"statusMessage": "agent-fork-done — close ledger lifecycle..."
|
"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",
|
"command": "~/.claude/hooks/error-spike-detector.sh",
|
||||||
"statusMessage": "error-spike rolling window (RULE 0.14)..."
|
"statusMessage": "error-spike rolling window (RULE 0.14)..."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"type": "command",
|
|
||||||
"command": "~/.claude/hooks/check-error-patterns.sh",
|
|
||||||
"statusMessage": "error-pattern check..."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "~/.claude/hooks/agent-heartbeat-tick.sh"
|
"command": "~/.claude/hooks/agent-heartbeat-tick.sh"
|
||||||
|
|
@ -108,6 +108,11 @@
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "~/.claude/hooks/no-python-without-approval.sh",
|
"command": "~/.claude/hooks/no-python-without-approval.sh",
|
||||||
"statusMessage": "rust-first python gate (RULE 0.2)..."
|
"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",
|
"type": "command",
|
||||||
"command": "~/.claude/hooks/citation-verify.sh",
|
"command": "~/.claude/hooks/citation-verify.sh",
|
||||||
"statusMessage": "citation-verify (RULE 0.4)..."
|
"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",
|
"command": "~/.claude/hooks/orchestrator-branch-check.sh",
|
||||||
"statusMessage": "orchestrator branch ownership (RULE 0.13)..."
|
"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",
|
"type": "command",
|
||||||
"command": "~/.claude/hooks/task-timer.sh"
|
"command": "~/.claude/hooks/task-timer.sh"
|
||||||
|
|
@ -197,6 +202,11 @@
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "~/.claude/hooks/alignment-check.sh"
|
"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",
|
"type": "command",
|
||||||
"command": "~/.claude/hooks/extract-task-durations.sh",
|
"command": "~/.claude/hooks/extract-task-durations.sh",
|
||||||
"statusMessage": "extract-task-durations — pull async durations from notifications..."
|
"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)..."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue