Skip to content

Watch out for Hades

According to Reddit, horrible people have made a nasty supply-chain attack called Hades: [Link] Looks like it’s marked at this time by: 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...

Murphy R
Jun 11, 202610 min read
1

ccording 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.pth files in your python site-packages
  • A suspicious .bun_ran marker in your tmp dirs
  • 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

Did you enjoy this article?

Recommend it — Standard Reader surfaces well-loved writing to more readers across the network.

Across the AtmosphereDiscussions