fix: SESSION_FILE shadow bug + hooks matcher + 5 security/privacy mons
state.sh used SESSION_FILE for session.json (global tracking data). Both hook handlers override SESSION_FILE to sessions/<pgrp>.json for per-session isolation. buddymon_session_reset() at session-stop end was writing _version:1 data INTO sessions/<pgrp>.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
This commit is contained in:
parent
caa655ab9a
commit
0c2d245632
5 changed files with 231 additions and 5 deletions
|
|
@ -30,6 +30,28 @@ json.dump(session_state, open('${SESSION_FILE}', 'w'), indent=2)
|
||||||
PYEOF
|
PYEOF
|
||||||
fi
|
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)
|
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)
|
SESSION_XP=$(python3 -c "import json; d=json.load(open('${SESSION_FILE}')); print(d.get('session_xp',0))" 2>/dev/null)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ import json, os
|
||||||
catalog_file = '${CATALOG}'
|
catalog_file = '${CATALOG}'
|
||||||
session_state_file = '${SESSION_FILE}'
|
session_state_file = '${SESSION_FILE}'
|
||||||
roster_file = '${BUDDYMON_DIR}/roster.json'
|
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))
|
catalog = json.load(open(catalog_file))
|
||||||
session_state = json.load(open(session_state_file))
|
session_state = json.load(open(session_state_file))
|
||||||
|
|
@ -132,7 +132,7 @@ import json, os
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
session_state_file = '${SESSION_FILE}'
|
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'
|
encounters_file = '${BUDDYMON_DIR}/encounters.json'
|
||||||
handoff_file = '${BUDDYMON_DIR}/handoff.json'
|
handoff_file = '${BUDDYMON_DIR}/handoff.json'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"SessionStart": [
|
"SessionStart": [
|
||||||
{
|
{
|
||||||
|
"matcher": "*",
|
||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
|
|
@ -38,6 +39,7 @@
|
||||||
],
|
],
|
||||||
"Stop": [
|
"Stop": [
|
||||||
{
|
{
|
||||||
|
"matcher": "*",
|
||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
|
|
|
||||||
200
lib/catalog.json
200
lib/catalog.json
|
|
@ -646,6 +646,206 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"flavor": "If you catch everything, you learn nothing."
|
"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": {
|
"event_encounters": {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@ BUDDYMON_DIR="${HOME}/.claude/buddymon"
|
||||||
ROSTER_FILE="${BUDDYMON_DIR}/roster.json"
|
ROSTER_FILE="${BUDDYMON_DIR}/roster.json"
|
||||||
ENCOUNTERS_FILE="${BUDDYMON_DIR}/encounters.json"
|
ENCOUNTERS_FILE="${BUDDYMON_DIR}/encounters.json"
|
||||||
ACTIVE_FILE="${BUDDYMON_DIR}/active.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/<pgrp>.json
|
||||||
|
SESSION_DATA_FILE="${BUDDYMON_DIR}/session.json"
|
||||||
|
|
||||||
buddymon_init() {
|
buddymon_init() {
|
||||||
mkdir -p "${BUDDYMON_DIR}"
|
mkdir -p "${BUDDYMON_DIR}"
|
||||||
|
|
@ -42,7 +44,7 @@ EOF
|
||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -f "${SESSION_FILE}" ]]; then
|
if [[ ! -f "${SESSION_DATA_FILE}" ]]; then
|
||||||
buddymon_session_reset
|
buddymon_session_reset
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +52,7 @@ EOF
|
||||||
buddymon_session_reset() {
|
buddymon_session_reset() {
|
||||||
local ts
|
local ts
|
||||||
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
cat > "${SESSION_FILE}" << EOF
|
cat > "${SESSION_DATA_FILE}" << EOF
|
||||||
{
|
{
|
||||||
"_version": 1,
|
"_version": 1,
|
||||||
"started_at": "${ts}",
|
"started_at": "${ts}",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue