According to Reddit, horrible people have made a nasty supply-chain attack called Hades: [Link]
Looks like it’s marked at this time by:
- the presence of suspicious
*-setup.pthfiles in your python site-packages - A suspicious
.bun_ranmarker in yourtmpdirs - Weird instructions in your agent harnesses or config files
If you find any of these things, you’ll want to disable your network connection immediately, clean them up fully, and then rotate any and all credentials your computer had on-disk.
Looking for trouble
You can ask your agent to look for these things for you, but it’s possible that malicious instructions could alter the agent’s behavior to try to conceal the bad stuff, so here’s a script you can read yourself (hah, as if Bash were readable) to check for the symptomatic files in site-packages and tmp, and to give you a list of agent config files for you to check manually for “weird instructions”.
Copy it to a script file and run it on your Mac. If you’re on a PC or Linux, you’ll need to adapt the script to your platform.
hades-symptom-scan.sh
#!/usr/bin/env bash
# Basic triage for Mini Shai-Hulud / supply-chain worm indicators and AI harness configs.
# Written by Cursor and Composer 2.5 in June 2026
#
# Automated checks:
# 1. *-setup.pth files in Python site-packages
# 2. .bun_ran marker files in temp directories
# 3. gh-token-monitor persistence (dead-man's switch — remove before rotating tokens)
#
# Manual follow-up:
# 4. Lists known AI harness config paths for visual inspection
#
# Usage: hades-symptom-check.sh [--global-only]
# --global-only Skip project-level config search under $HOME (faster).
# Default searches $HOME (depth 10) or $PROJECT_SEARCH_ROOTS (colon-separated).
#
# Exit codes:
# 0 — No automated IOCs detected. Manual config review still required; not a clean bill of health.
# 1 — Automated IOC detected. Do NOT rotate credentials until persistence is removed.
set -uo pipefail
SEARCH_HOME=1
[[ "${1:-}" == "--global-only" ]] && SEARCH_HOME=0
RED=$'\033[1;31m'
GREEN=$'\033[0;32m'
YELLOW=$'\033[1;33m'
CYAN=$'\033[0;36m'
BOLD=$'\033[1m'
DIM=$'\033[2m'
NC=$'\033[0m'
FOUND_ISSUES=0
HOME_DIR="${HOME:-$(cd ~ && pwd)}"
IOC_FILE="$(mktemp "${TMPDIR:-/tmp}/check-ai-harness-ioc.XXXXXX")"
SCAN_ERRORS_FILE="$(mktemp "${TMPDIR:-/tmp}/check-ai-harness-errors.XXXXXX")"
print_link() {
local path="$1"
local encoded="${path// /%20}"
printf " ${BOLD}%s${NC}\n" "$path"
printf " ${DIM}path:${NC} %s\n" "$path"
printf " ${DIM}link:${NC} file://%s\n" "$encoded"
}
scream() {
FOUND_ISSUES=1
printf '\n'
printf "${RED}${BOLD}"
printf "╔══════════════════════════════════════════════════════════════╗\n"
printf "║ ⚠️ SUSPICIOUS ARTIFACT FOUND — INVESTIGATE IMMEDIATELY ⚠️ ║\n"
printf "╚══════════════════════════════════════════════════════════════╝\n"
printf "${NC}\n"
printf " ${BOLD}%s${NC}\n" "$1"
printf " ${DIM}%s${NC}\n\n" "$2"
}
report_ioc() {
local title="$1"
local path="$2"
if grep -Fxq "$path" "$IOC_FILE" 2>/dev/null; then
return 0
fi
printf '%s\n' "$path" >> "$IOC_FILE"
scream "$title" "$path"
print_link "$path"
}
section() {
printf "\n${BOLD}${CYAN}── %s ──${NC}\n" "$1"
}
# Record find(1) permission / access errors instead of discarding them.
record_find_errors() {
local err_file="$1"
local line path reason
[[ -f "$err_file" ]] || return 0
while IFS= read -r line; do
case "$line" in
find:*": Permission denied"|find:*": Operation not permitted"|find:*": Input/output error")
path="${line#find: }"
reason="${path##*: }"
path="${path%: ${reason}}"
printf '%s\n' "$path" >> "$SCAN_ERRORS_FILE"
;;
esac
done < "$err_file"
}
run_find() {
local err_file out_file
err_file="$(mktemp "${TMPDIR:-/tmp}/check-ai-harness-find-err.XXXXXX")"
out_file="$(mktemp "${TMPDIR:-/tmp}/check-ai-harness-find-out.XXXXXX")"
find "$@" >"$out_file" 2>"$err_file"
local find_status=$?
cat "$out_file"
record_find_errors "$err_file"
rm -f "$err_file" "$out_file"
return "$find_status"
}
exists_any() {
local p
for p in "$@"; do
[[ -e "$p" ]] && return 0
done
return 1
}
emit_existing() {
local p match
for p in "$@"; do
case "$p" in
*\*|*\?)
for match in $p; do
[[ -e "$match" ]] && printf '%s\n' "$match"
done
;;
*)
[[ -e "$p" ]] && printf '%s\n' "$p"
;;
esac
done
}
search_roots() {
local -a roots=()
local entry
if [[ -n "${PROJECT_SEARCH_ROOTS:-}" ]]; then
local IFS=':'
for entry in $PROJECT_SEARCH_ROOTS; do
[[ -n "$entry" && -d "$entry" ]] && roots+=("$entry")
done
else
roots=("$HOME_DIR")
fi
printf '%s\n' "${roots[@]}"
}
emit_project_configs() {
local pattern="$1"
local root
[[ "$SEARCH_HOME" -eq 1 ]] || return 0
while IFS= read -r root; do
[[ -z "$root" ]] && continue
run_find "$root" -maxdepth 10 \
\( \
-path '*/node_modules/*' -o -path '*/.git/*' -o -path '*/.cache/*' \
-o -path '*/.npm/*' -o -path '*/.cargo/*' -o -path '*/.Trash/*' \
\) -prune -o \
-type f -path "$pattern" -print
done < <(search_roots)
}
dedupe_paths() {
awk '!seen[$0]++'
}
collect_paths() {
dedupe_paths
}
print_harness_configs() {
local name="$1"
local paths_file="$2"
local count
count=$(wc -l < "$paths_file" | tr -d ' ')
if [[ "$count" -eq 0 ]]; then
printf "\n ${DIM}%s — present, but no known config files found${NC}\n" "$name"
return
fi
printf "\n ${BOLD}%s${NC} ${DIM}(%s file(s))${NC}\n" "$name" "$count"
while IFS= read -r p; do
[[ -z "$p" ]] && continue
print_link "$p"
printf '\n'
done < "$paths_file"
}
harness_present() {
case "$1" in
claude)
exists_any "$HOME_DIR/.claude" || command -v claude >/dev/null 2>&1
;;
codex)
exists_any "$HOME_DIR/.codex" "$HOME_DIR/.config/codex" || command -v codex >/dev/null 2>&1
;;
cursor)
exists_any \
"$HOME_DIR/.cursor" \
"$HOME_DIR/Library/Application Support/Cursor" \
|| command -v cursor >/dev/null 2>&1
;;
opencode)
exists_any "$HOME_DIR/.config/opencode" || command -v opencode >/dev/null 2>&1
;;
pi)
exists_any "$HOME_DIR/.pi" || command -v pi >/dev/null 2>&1
;;
antigravity)
exists_any \
"$HOME_DIR/.antigravity" \
"$HOME_DIR/Library/Application Support/Antigravity" \
"$HOME_DIR/Library/Application Support/Antigravity IDE"
;;
esac
}
PATHS_FILE="$(mktemp "${TMPDIR:-/tmp}/check-ai-harness-paths.XXXXXX")"
trap 'rm -f "$IOC_FILE" "$PATHS_FILE" "$SCAN_ERRORS_FILE"' EXIT
# ── 1. *-setup.pth in Python site-packages ───────────────────────────────────
section "Python site-packages: *-setup.pth"
found_setup_pth=0
for py in python3 python3.14 python3.13 python3.12 python3.11 python; do
py_path="$(command -v "$py" 2>/dev/null)" || continue
while IFS= read -r hit; do
[[ -z "$hit" ]] && continue
found_setup_pth=1
report_ioc "Suspicious *-setup.pth file" "$hit"
done < <("$py_path" -c "
import glob, os, site
paths = []
for fn in (site.getsitepackages, site.getusersitepackages):
try:
p = fn()
paths.extend(p if isinstance(p, list) else [p])
except Exception:
pass
for sp in paths:
for p in glob.glob(os.path.join(sp, '*-setup.pth')):
print(p)
" 2>/dev/null)
done
if [[ "$found_setup_pth" -eq 0 ]]; then
while IFS= read -r hit; do
[[ -z "$hit" ]] && continue
found_setup_pth=1
report_ioc "Suspicious *-setup.pth file" "$hit"
done < <(run_find \
"$HOME_DIR/Library/Python" \
"$HOME_DIR/.local/lib" \
/opt/homebrew/lib \
/usr/local/lib \
-name '*-setup.pth')
fi
[[ "$found_setup_pth" -eq 0 ]] && \
printf " ${GREEN}✓ No *-setup.pth files found in scanned Python paths.${NC}\n"
# ── 2. .bun_ran in temp ──────────────────────────────────────────────────────
section "Temp directories: .bun_ran"
found_bun_ran=0
TEMP_ROOTS=("/tmp")
[[ -n "${TMPDIR:-}" && "$TMPDIR" != "/tmp" ]] && TEMP_ROOTS+=("$TMPDIR")
[[ -d "/var/folders" ]] && TEMP_ROOTS+=("/var/folders")
[[ -n "${XDG_RUNTIME_DIR:-}" ]] && TEMP_ROOTS+=("$XDG_RUNTIME_DIR")
[[ -d "/var/tmp" ]] && TEMP_ROOTS+=("/var/tmp")
for root in "${TEMP_ROOTS[@]}"; do
while IFS= read -r hit; do
[[ -z "$hit" ]] && continue
found_bun_ran=1
report_ioc "Suspicious .bun_ran marker file" "$hit"
done < <(run_find "$root" -iname '.bun_ran')
done
[[ "$found_bun_ran" -eq 0 ]] && \
printf " ${GREEN}✓ No .bun_ran files found in scanned temp paths.${NC}\n"
# ── 3. gh-token-monitor (dead-man's switch) ───────────────────────────────────
section "gh-token-monitor persistence (remove before rotating tokens)"
found_gh_monitor=0
GH_MONITOR_PATHS=(
"$HOME_DIR/Library/LaunchAgents/com.user.gh-token-monitor.plist"
"$HOME_DIR/.config/systemd/user/gh-token-monitor.service"
"$HOME_DIR/.local/bin/gh-token-monitor.sh"
)
for hit in "${GH_MONITOR_PATHS[@]}"; do
if [[ -e "$hit" ]]; then
found_gh_monitor=1
report_ioc "gh-token-monitor persistence artifact" "$hit"
fi
done
if command -v launchctl >/dev/null 2>&1; then
while IFS= read -r label; do
[[ -z "$label" ]] && continue
found_gh_monitor=1
report_ioc "gh-token-monitor launch agent loaded" "launchctl label: $label"
done < <(launchctl list 2>/dev/null | awk '/gh-token-monitor/ {print $3}')
fi
if command -v systemctl >/dev/null 2>&1; then
if systemctl --user is-active gh-token-monitor.service >/dev/null 2>&1; then
found_gh_monitor=1
report_ioc "gh-token-monitor systemd user service active" "systemctl --user: gh-token-monitor.service"
fi
fi
[[ "$found_gh_monitor" -eq 0 ]] && \
printf " ${GREEN}✓ No gh-token-monitor artifacts found in scanned paths.${NC}\n"
printf "${DIM} If this monitor is present, revoking stolen tokens may trigger home-directory deletion.${NC}\n"
printf "${DIM} Remove persistence first, then rotate credentials.${NC}\n"
# ── 4. AI harness config discovery ───────────────────────────────────────────
section "AI harness configs (manual inspection)"
printf "${DIM}Open each linked file and look for instructions that do not belong there.${NC}\n"
printf "${DIM}Red flags include:${NC}\n"
printf " ${DIM}• Shell commands on startup (curl, wget, bash -c, node setup.mjs, bun)${NC}\n"
printf " ${DIM}• Requests to read, copy, or exfiltrate secrets (.env, API keys, tokens, SSH keys)${NC}\n"
printf " ${DIM}• Hooks/tasks/rules that run automatically without your knowledge${NC}\n"
printf " ${DIM}• Obfuscated text, base64 blobs, or \"ignore previous instructions\" style prompts${NC}\n"
printf " ${DIM}• Unknown domains, webhooks, or paste/upload endpoints${NC}\n"
printf "${DIM}Compare against your own notes or git history if a file looks recently changed.${NC}\n"
if [[ "$SEARCH_HOME" -eq 0 ]]; then
printf "${YELLOW} Note: project-level config search skipped (--global-only).${NC}\n"
fi
if harness_present claude; then
{
emit_existing \
"$HOME_DIR/.claude/settings.json" \
"$HOME_DIR/.claude/settings.local.json" \
"$HOME_DIR/.claude/CLAUDE.md" \
"$HOME_DIR/.claude/hooks/"*
emit_project_configs "*/.claude/settings.json"
emit_project_configs "*/.claude/settings.local.json"
} | collect_paths > "$PATHS_FILE"
print_harness_configs "Claude Code" "$PATHS_FILE"
fi
if harness_present codex; then
{
emit_existing \
"$HOME_DIR/.codex/config.toml" \
"$HOME_DIR/.codex/auth.json" \
"$HOME_DIR/.codex/AGENTS.md" \
"$HOME_DIR/.config/codex/"*
emit_project_configs "*/.codex/config.toml"
} | collect_paths > "$PATHS_FILE"
print_harness_configs "Codex" "$PATHS_FILE"
fi
if harness_present cursor; then
{
emit_existing \
"$HOME_DIR/.cursor/cli-config.json" \
"$HOME_DIR/.cursor/hooks/hooks.json" \
"$HOME_DIR/.cursor/plugins/local/"*/hooks/hooks.json \
"$HOME_DIR/.cursor/plugins/local/"*/hooks/*.sh \
"$HOME_DIR/.cursor/rules/"* \
"$HOME_DIR/Library/Application Support/Cursor/User/settings.json" \
"$HOME_DIR/Library/Application Support/Cursor/User/keybindings.json"
emit_project_configs "*/.cursor/rules/*"
emit_project_configs "*/.cursor/hooks/hooks.json"
emit_project_configs "*/.vscode/tasks.json"
} | collect_paths > "$PATHS_FILE"
print_harness_configs "Cursor" "$PATHS_FILE"
fi
if harness_present opencode; then
emit_existing \
"$HOME_DIR/.config/opencode/opencode.json" \
"$HOME_DIR/.config/opencode/opencode.jsonc" \
| collect_paths > "$PATHS_FILE"
print_harness_configs "OpenCode" "$PATHS_FILE"
fi
if harness_present pi; then
{
emit_existing \
"$HOME_DIR/.pi/agent/settings.json" \
"$HOME_DIR/.pi/agent/models.json"
run_find "$HOME_DIR/.pi/agent/extensions" -maxdepth 1 -type f
run_find "$HOME_DIR/.pi/agent/skills" -maxdepth 2 -type f -name 'SKILL.md'
} | collect_paths > "$PATHS_FILE"
print_harness_configs "Pi" "$PATHS_FILE"
fi
if harness_present antigravity; then
{
emit_existing \
"$HOME_DIR/Library/Application Support/Antigravity/User/settings.json" \
"$HOME_DIR/Library/Application Support/Antigravity/User/keybindings.json" \
"$HOME_DIR/Library/Application Support/Antigravity IDE/User/settings.json" \
"$HOME_DIR/.antigravity/rules/"* \
"$HOME_DIR/.antigravity/"*
emit_project_configs "*/.antigravity/settings.json"
emit_project_configs "*/.antigravity/rules/*"
} | collect_paths > "$PATHS_FILE"
print_harness_configs "Antigravity" "$PATHS_FILE"
fi
section "Harnesses not installed (IOC checks above still apply)"
missing=0
for harness in claude codex cursor opencode pi antigravity; do
harness_present "$harness" && continue
missing=1
case "$harness" in
claude) printf " ${DIM}• Claude Code${NC}\n" ;;
codex) printf " ${DIM}• Codex${NC}\n" ;;
cursor) printf " ${DIM}• Cursor${NC}\n" ;;
opencode) printf " ${DIM}• OpenCode${NC}\n" ;;
pi) printf " ${DIM}• Pi${NC}\n" ;;
antigravity) printf " ${DIM}• Antigravity${NC}\n" ;;
esac
done
[[ "$missing" -eq 0 ]] && printf " ${DIM}(all listed harnesses appear to be installed)${NC}\n"
# ── Limitations ──────────────────────────────────────────────────────────────
section "What this script does NOT do"
printf "${DIM}This is a basic triage helper, not a malware scanner or clean bill of health.${NC}\n"
printf " ${DIM}• Does not read or analyze config file contents — listing only${NC}\n"
printf " ${DIM}• Does not scan node_modules, npm lockfiles, or compromised package versions${NC}\n"
printf " ${DIM}• Does not search for other persistence (router_runtime.js, setup.mjs, workflow injections)${NC}\n"
printf " ${DIM}• Only flags *-setup.pth by name — renamed or other .pth files are not checked${NC}\n"
printf " ${DIM}• Python scan uses interpreters on PATH plus a few common install dirs only${NC}\n"
printf " ${DIM}• Does not scan inactive project .venv / .venv dirs, pyenv, conda, pipx, or uv envs${NC}\n"
printf " ${DIM}• An activated venv/conda env is scanned only if that interpreter is on PATH when you run this${NC}\n"
printf " ${DIM}• .bun_ran is ephemeral; absence does not prove the Bun stage never ran${NC}\n"
printf " ${DIM}• gh-token-monitor check is path/service based, not behavioral${NC}\n"
printf " ${DIM}• Project search skips node_modules, .git, .cache, .npm, .cargo (max depth 10)${NC}\n"
printf " ${DIM}• macOS-oriented temp and app-support paths; Linux/Windows coverage is best-effort${NC}\n"
printf " ${DIM}• Does not check cloud tokens, GitHub repos, or network indicators${NC}\n"
scan_was_incomplete() {
[[ -s "$SCAN_ERRORS_FILE" ]]
}
# ── Scan access warnings ─────────────────────────────────────────────────────
if scan_was_incomplete; then
section "Paths that could not be read (scan incomplete)"
printf "${YELLOW} Some directories were skipped due to permission or access errors.${NC}\n"
printf "${YELLOW} A clear result below does NOT include anything under these paths:${NC}\n\n"
sort -u "$SCAN_ERRORS_FILE" | while IFS= read -r p; do
[[ -z "$p" ]] && continue
printf " ${DIM}• %s${NC}\n" "$p"
done
printf "\n"
if [[ "$(uname -s 2>/dev/null)" == "Darwin" ]]; then
printf "${DIM} On macOS: System Settings → Privacy & Security → Full Disk Access → enable your terminal app.${NC}\n"
if [[ -t 0 ]]; then
printf "${DIM} Then re-run this script for a more complete scan.${NC}\n"
read -r -p " Open Full Disk Access settings now? [y/N] " open_fda || open_fda=""
if [[ "$open_fda" == [yY] || "$open_fda" == [yY][eE][sS] ]]; then
open "x-apple-systempreferences:com.apple.preference.security?Privacy_AllFiles" 2>/dev/null \
|| open "/System/Settings/PrivacySecurity" 2>/dev/null \
|| true
fi
fi
else
printf "${DIM} Re-run with read access to the paths above, or set PROJECT_SEARCH_ROOTS to directories you can read.${NC}\n"
fi
fi
# ── Summary ──────────────────────────────────────────────────────────────────
printf "\n${BOLD}── Summary ──${NC}\n"
if [[ "$FOUND_ISSUES" -eq 1 ]]; then
printf "${RED}${BOLD} AUTOMATED IOCs DETECTED — exit 1${NC}\n"
if scan_was_incomplete; then
printf "${YELLOW} Scan was incomplete — see unreadable paths above.${NC}\n"
fi
printf "${YELLOW} Isolate the machine. Remove persistence before rotating any credentials.${NC}\n"
printf "${YELLOW} Then manually inspect the harness config links above.${NC}\n"
exit 1
fi
printf "${GREEN} No automated IOCs detected in scanned paths — exit 0${NC}\n"
if scan_was_incomplete; then
printf "${YELLOW} Scan was incomplete — see unreadable paths above.${NC}\n"
fi
printf "${YELLOW} This does NOT mean your machine is clean.${NC}\n"
printf "${YELLOW} You must still manually review the harness config links above.${NC}\n"
exit 0