From 0c2d24563233c5cac9b697da98c4b98596681b21 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Sun, 5 Apr 2026 23:24:27 -0700 Subject: [PATCH] fix: SESSION_FILE shadow bug + hooks matcher + 5 security/privacy mons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit state.sh used SESSION_FILE for session.json (global tracking data). Both hook handlers override SESSION_FILE to sessions/.json for per-session isolation. buddymon_session_reset() at session-stop end was writing _version:1 data INTO sessions/.json, leaving stale wrong-format session files that broke buddy lookup. Fix: rename state.sh variable to SESSION_DATA_FILE — no more collision. Also: - Add matcher:'*' to SessionStart and Stop hooks (CC 2.x compatibility) - Add dead-session GC to session-start.sh (cleans up orphaned PGRP files) - Add 5 new privacy/security bug_monsters: LeakWraith (uncommon) — exposed credentials CipherNull (uncommon) — weak/broken crypto (MD5, ECB, rand() for secrets) ConsentShadow (rare) — tracking/analytics without consent (CF flagship villain) ThrottleDemon (common) — ignored 429s and missing backoff PrivacyLich (legendary) — GDPR/CCPA/breach debt; unkillable, only containable --- hooks-handlers/session-start.sh | 22 ++++ hooks-handlers/session-stop.sh | 4 +- hooks/hooks.json | 2 + lib/catalog.json | 200 ++++++++++++++++++++++++++++++++ lib/state.sh | 8 +- 5 files changed, 231 insertions(+), 5 deletions(-) diff --git a/hooks-handlers/session-start.sh b/hooks-handlers/session-start.sh index 98814e4..df0fd72 100755 --- a/hooks-handlers/session-start.sh +++ b/hooks-handlers/session-start.sh @@ -30,6 +30,28 @@ json.dump(session_state, open('${SESSION_FILE}', 'w'), indent=2) PYEOF fi +# Clean up session files for dead processes (runs async-style; errors ignored) +python3 << 'PYEOF' 2>/dev/null & +import os, glob, json +for f in glob.glob(os.path.expanduser("~/.claude/buddymon/sessions/*.json")): + pid_str = os.path.basename(f).replace('.json', '') + try: + pid = int(pid_str) + # Check if the process group still exists + os.killpg(pid, 0) + except (ValueError, ProcessLookupError, PermissionError): + # PermissionError means the process exists (just not ours) — keep it + # ProcessLookupError means it's dead — remove + try: + d = json.load(open(f)) + if '_version' not in d: # only remove valid-format dead sessions + os.remove(f) + except Exception: + pass + except Exception: + pass +PYEOF + ACTIVE_ID=$(python3 -c "import json; d=json.load(open('${SESSION_FILE}')); print(d.get('buddymon_id',''))" 2>/dev/null) SESSION_XP=$(python3 -c "import json; d=json.load(open('${SESSION_FILE}')); print(d.get('session_xp',0))" 2>/dev/null) diff --git a/hooks-handlers/session-stop.sh b/hooks-handlers/session-stop.sh index ec9f226..23ba574 100755 --- a/hooks-handlers/session-stop.sh +++ b/hooks-handlers/session-stop.sh @@ -41,7 +41,7 @@ import json, os catalog_file = '${CATALOG}' session_state_file = '${SESSION_FILE}' roster_file = '${BUDDYMON_DIR}/roster.json' -session_file = '${BUDDYMON_DIR}/session.json' +session_file = '${BUDDYMON_DIR}/session.json' # SESSION_DATA_FILE from state.sh catalog = json.load(open(catalog_file)) session_state = json.load(open(session_state_file)) @@ -132,7 +132,7 @@ import json, os from datetime import datetime, timezone session_state_file = '${SESSION_FILE}' -session_file = '${BUDDYMON_DIR}/session.json' +session_file = '${BUDDYMON_DIR}/session.json' # SESSION_DATA_FILE from state.sh encounters_file = '${BUDDYMON_DIR}/encounters.json' handoff_file = '${BUDDYMON_DIR}/handoff.json' diff --git a/hooks/hooks.json b/hooks/hooks.json index 19b6e4c..44800c6 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -3,6 +3,7 @@ "hooks": { "SessionStart": [ { + "matcher": "*", "hooks": [ { "type": "command", @@ -38,6 +39,7 @@ ], "Stop": [ { + "matcher": "*", "hooks": [ { "type": "command", diff --git a/lib/catalog.json b/lib/catalog.json index 69d4c6d..2991bfe 100644 --- a/lib/catalog.json +++ b/lib/catalog.json @@ -646,6 +646,206 @@ } ], "flavor": "If you catch everything, you learn nothing." + }, + "LeakWraith": { + "id": "LeakWraith", + "display": "\ud83d\udd11 LeakWraith", + "type": "bug_monster", + "rarity": "uncommon", + "base_strength": 70, + "xp_reward": 120, + "catchable": true, + "defeatable": false, + "catch_requires": [ + "rotate_secret", + "audit_exposure", + "add_secret_scan" + ], + "description": "A specter that feasts on exposed credentials. It doesn't steal them \u2014 it just shows them to everyone.", + "error_patterns": [ + "api_key", + "secret_key", + "BEGIN.*PRIVATE KEY", + "PRIVATE KEY-----", + "password\\s*=\\s*['\"][^'\"]+['\"]", + "token\\s*=\\s*['\"][^'\"]+['\"]", + "Authorization: Bearer", + "hardcoded.*secret", + "leaked.*credential", + "exposed.*secret", + "sk-[a-zA-Z0-9]{20}", + "credential.*plain" + ], + "weaken_actions": [ + { + "action": "rotate_secret", + "strength_reduction": 40 + }, + { + "action": "audit_exposure", + "strength_reduction": 20 + }, + { + "action": "add_secret_scan", + "strength_reduction": 20 + } + ], + "flavor": "Rotation is not optional. Neither is the audit." + }, + "CipherNull": { + "id": "CipherNull", + "display": "\ud83d\udd10 CipherNull", + "type": "bug_monster", + "rarity": "uncommon", + "base_strength": 60, + "xp_reward": 100, + "catchable": true, + "defeatable": true, + "catch_requires": [ + "replace_weak_crypto", + "add_crypto_test" + ], + "description": "A demon born from broken encryption. Every MD5 it touches makes it stronger.", + "error_patterns": [ + "md5\\(", + "sha1\\(", + "hashlib.md5", + "hashlib.sha1", + "DES.new", + "AES.*ECB", + "ECB mode", + "Math.random.*token", + "Math.random.*secret", + "rand().*password", + "random.*nonce", + "base64.*password", + "rot13" + ], + "weaken_actions": [ + { + "action": "replace_weak_crypto", + "strength_reduction": 50 + }, + { + "action": "add_crypto_test", + "strength_reduction": 25 + } + ], + "flavor": "MD5 was deprecated before some of your dependencies were written." + }, + "ConsentShadow": { + "id": "ConsentShadow", + "display": "\ud83d\udc64 ConsentShadow", + "type": "bug_monster", + "rarity": "rare", + "base_strength": 85, + "xp_reward": 220, + "catchable": true, + "defeatable": false, + "catch_requires": [ + "add_consent_gate", + "document_data_flow", + "add_opt_out" + ], + "description": "A privacy violation given form. It tracks everything, logs everything, consents to nothing. At CircuitForge, it's the end boss.", + "error_patterns": [ + "track.*user", + "user.*tracking", + "analytics.*event", + "fingerprint.*user", + "browser.*fingerprint", + "log.*email", + "log.*ip.*address", + "collect.*personal", + "behavioral.*profil", + "third.party.*tracking", + "telemetry.*enabled", + "send.*analytics" + ], + "weaken_actions": [ + { + "action": "add_consent_gate", + "strength_reduction": 35 + }, + { + "action": "document_data_flow", + "strength_reduction": 25 + }, + { + "action": "add_opt_out", + "strength_reduction": 30 + } + ], + "flavor": "Plain-language consent. Always. No pre-checked boxes." + }, + "ThrottleDemon": { + "id": "ThrottleDemon", + "display": "\u23f1\ufe0f ThrottleDemon", + "type": "bug_monster", + "rarity": "common", + "base_strength": 40, + "xp_reward": 60, + "catchable": true, + "defeatable": true, + "catch_requires": [ + "add_backoff", + "add_rate_limit_handling" + ], + "description": "Manifests from ignored 429s. Feed it a proper backoff strategy.", + "error_patterns": [ + "429", + "Too Many Requests", + "rate.limit.*exceeded", + "rate limit", + "quota.*exceeded", + "throttle", + "throttled", + "RetryAfter", + "Retry-After", + "backoff.*error", + "RateLimitError", + "burst.*limit" + ], + "weaken_actions": [ + { + "action": "add_backoff", + "strength_reduction": 35 + }, + { + "action": "add_rate_limit_handling", + "strength_reduction": 40 + } + ], + "flavor": "Exponential backoff with jitter. Every time." + }, + "PrivacyLich": { + "id": "PrivacyLich", + "display": "\ud83d\udcdc PrivacyLich", + "type": "bug_monster", + "rarity": "legendary", + "base_strength": 100, + "xp_reward": 500, + "catchable": false, + "defeatable": false, + "catch_requires": [], + "description": "An ancient entity. Data it touched never truly disappears. Cannot be caught or defeated \u2014 only contained through rigorous compliance work.", + "error_patterns": [ + "GDPR", + "CCPA", + "LGPD", + "erasure.*request", + "right to be forgotten", + "data subject", + "DSAR", + "data.*breach", + "personal.*data.*leak", + "PII.*exposed", + "regulatory.*violation", + "compliance.*failure", + "notification.*breach" + ], + "weaken_actions": [], + "flavor": "Some debts cannot be paid. They can only be carried responsibly." } }, "event_encounters": { diff --git a/lib/state.sh b/lib/state.sh index 4793414..518617c 100644 --- a/lib/state.sh +++ b/lib/state.sh @@ -6,7 +6,9 @@ BUDDYMON_DIR="${HOME}/.claude/buddymon" ROSTER_FILE="${BUDDYMON_DIR}/roster.json" ENCOUNTERS_FILE="${BUDDYMON_DIR}/encounters.json" ACTIVE_FILE="${BUDDYMON_DIR}/active.json" -SESSION_FILE="${BUDDYMON_DIR}/session.json" +# Named SESSION_DATA_FILE (not SESSION_FILE) to avoid shadowing the +# per-session state file hooks define as SESSION_FILE=sessions/.json +SESSION_DATA_FILE="${BUDDYMON_DIR}/session.json" buddymon_init() { mkdir -p "${BUDDYMON_DIR}" @@ -42,7 +44,7 @@ EOF EOF fi - if [[ ! -f "${SESSION_FILE}" ]]; then + if [[ ! -f "${SESSION_DATA_FILE}" ]]; then buddymon_session_reset fi } @@ -50,7 +52,7 @@ EOF buddymon_session_reset() { local ts ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - cat > "${SESSION_FILE}" << EOF + cat > "${SESSION_DATA_FILE}" << EOF { "_version": 1, "started_at": "${ts}",