Hooks
What repo-forensics runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /plugin marketplace add alexgreensh/repo-forensics > /plugin install repo-forensics@alexgreensh-repo-forensics
Ships with repo-forensics. Installing the plugin gets these hooks.
What fires, and when
PreToolUse
- Matches
Bashbash "${CLAUDE_PLUGIN_ROOT}/hooks/run_pre_scan.sh"
PostToolUse
- Matches
Bashbash "${CLAUDE_PLUGIN_ROOT}/hooks/run_auto_scan.sh"
SessionStart
Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.
bash "${CLAUDE_PLUGIN_ROOT}/hooks/run_session_scan.sh"bash "${CLAUDE_PLUGIN_ROOT}/hooks/first-run-nudge.sh"
Where it lives
- hooks/ensure_refresh_daemon.shGitHub
Read the script
#!/usr/bin/env bash # SessionStart adapter for the cross-platform refresh controller. set -u CALLER_PATH="${PATH:-}" # Trusted command roots for this wrapper's own utilities (dirname, cd, pwd): # FHS + NixOS system profiles (root-owned) + per-user Nix/XDG profile dirs # (same trust as Homebrew). NixOS has no /usr/bin or /bin coreutils. PATH="/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin:/run/current-system/sw/bin:/run/wrappers/bin:/nix/var/nix/profiles/default/bin" if [ -n "${HOME:-}" ]; then PATH="$PATH:$HOME/.nix-profile/bin:$HOME/.local/bin:$HOME/.local/state/nix/profile/bin" fi _rf_profile_user="${USER:-}" if [ -z "$_rf_profile_user" ] && [ -n "${HOME:-}" ]; then _rf_profile_user="${HOME##*/}" fi if [ -n "$_rf_profile_user" ]; then PATH="$PATH:/etc/profiles/per-user/$_rf_profile_user/bin" fi unset _rf_profile_user export PATH SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" || exit 0 PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${KIMI_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}}" CONTROLLER="$PLUGIN_ROOT/skills/repo-forensics/scripts/refresh_controller.py" LAUNCHER="$PLUGIN_ROOT/hooks/python-launcher.sh" [ -f "$CONTROLLER" ] || exit 0 [ -f "$LAUNCHER" ] || exit 0 # Preserve caller PATH only for the interpreter launcher: it independently # allowlists every Python location, including standard Windows installs. The # wrapper's own utilities above resolve exclusively through trusted paths. # Repair is detached so a slow/broken scheduler API cannot block SessionStart. PATH="${CALLER_PATH:+$CALLER_PATH:}$PATH" \ "${BASH:-$(command -v bash || echo /bin/bash)}" "$LAUNCHER" "$CONTROLLER" ensure \ </dev/null >/dev/null 2>&1 & exit 0 - hooks/first-run-nudge.shRunsGitHub
Read the script
#!/usr/bin/env bash # repo-forensics - SessionStart First-Run Auto-Update Nudge # # Marketplace installs can go stale. This hook prints a one-time platform-aware # message telling users how to keep repo-forensics current so they get new IOCs, # detection rules, and critical security patches. # # For a security scanner specifically, stale installs are especially dangerous: # users running repo-forensics against known supply chain attacks need the IOC # list that was current at the time of the attack, not six weeks ago. # # Conditions (all must be true for the nudge to fire): # - running from a plugin cache (marketplace install, not a dev checkout) # - flag file absent (one-shot per user) # - REPO_FORENSICS_NUDGE environment variable not set to 0 (kill switch) # # Copyright (C) 2026 Alex Greenshpun # SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0 SCRIPT_DIR="$(dirname "$0")" PLUGIN_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" CODEX_ROOT="${CODEX_HOME:-${HOME}/.codex}" # If the user explicitly set CODEX_HOME, validate it exists before proceeding. # When unset we fall back to the default guess ($HOME/.codex), which may simply # not exist yet on Claude-only machines — that is normal and should not abort. if [ -n "${CODEX_HOME:-}" ] && [ ! -d "$CODEX_ROOT" ]; then echo "Warning: CODEX_ROOT not found: $CODEX_ROOT" >&2 exit 0 fi if [ -d "$CODEX_ROOT" ]; then CODEX_ROOT="$(cd "$CODEX_ROOT" && pwd)" fi CLAUDE_ROOT="${HOME}/.claude" if [ -d "$CLAUDE_ROOT" ]; then CLAUDE_ROOT="$(cd "$CLAUDE_ROOT" && pwd)" fi CURSOR_ROOT="${CURSOR_HOME:-${HOME}/.cursor}" if [ -d "$CURSOR_ROOT" ]; then CURSOR_ROOT="$(cd "$CURSOR_ROOT" && pwd)" fi KIMI_ROOT="${KIMI_CODE_HOME:-${HOME}/.kimi-code}" if [ -d "$KIMI_ROOT" ]; then KIMI_ROOT="$(cd "$KIMI_ROOT" && pwd)" fi PLATFORM="generic" STATE_ROOT="${HOME}/.repo-forensics" if [[ "$PLUGIN_ROOT" == "$CODEX_ROOT/"* || "$PLUGIN_ROOT" == *"/.codex/"* ]]; then PLATFORM="codex" STATE_ROOT="${CODEX_ROOT}/repo-forensics" elif [[ "$PLUGIN_ROOT" == "$CLAUDE_ROOT/"* || "$PLUGIN_ROOT" == *"/.claude/"* ]]; then PLATFORM="claude" STATE_ROOT="${CLAUDE_ROOT}/repo-forensics" elif [[ "$PLUGIN_ROOT" == "$CURSOR_ROOT/"* || "$PLUGIN_ROOT" == *"/.cursor/"* ]]; then PLATFORM="cursor" STATE_ROOT="${CURSOR_ROOT}/repo-forensics" elif [[ "$PLUGIN_ROOT" == "$KIMI_ROOT/"* || "$PLUGIN_ROOT" == *"/.kimi-code/"* ]]; then PLATFORM="kimi" STATE_ROOT="${KIMI_ROOT}/repo-forensics" fi NUDGE_FLAG="${STATE_ROOT}/.marketplace-nudge-shown" # Kill switch if [ "${REPO_FORENSICS_NUDGE:-1}" = "0" ]; then exit 0 fi # Only fire for marketplace/cache installs. Dev-symlink or script-install users # have their own update paths and do not need this hint. Kimi Code installs # land in plugins/managed rather than plugins/cache. if [[ "$PLUGIN_ROOT" != *"/plugins/cache/"* && "$PLUGIN_ROOT" != *"/plugins/managed/"* ]]; then exit 0 fi # One-shot: if we've already shown the nudge, stay silent. if [ -f "$NUDGE_FLAG" ]; then exit 0 fi if [ "$PLATFORM" = "claude" ]; then cat <<'NUDGE' [repo-forensics] First-run tip: enable auto-update for this marketplace so you get new IOCs, detection rules, and critical security patches automatically. For a security scanner, stale installs are especially dangerous. In Claude Code: /plugin -> Marketplaces -> select your repo-forensics marketplace -> Enable auto-update Third-party marketplaces ship with auto-update off by default in Claude Code. This is not our choice. Opt out of this hint permanently with REPO_FORENSICS_NUDGE=0. This message will not show again. NUDGE elif [ "$PLATFORM" = "codex" ]; then cat <<'NUDGE' [repo-forensics] First-run tip: keep your Codex marketplace snapshot fresh so you get new IOCs, detection rules, and critical security patches. codex plugin marketplace upgrade If repo-forensics was already installed from that marketplace, reinstall it after refreshing the snapshot. Opt out of this hint permanently with REPO_FORENSICS_NUDGE=0. This message will not show again. NUDGE elif [ "$PLATFORM" = "cursor" ]; then cat <<'NUDGE' [repo-forensics] First-run tip: keep this install fresh so you get new IOCs, detection rules, and critical security patches. beforeShellExecution blocks known-malicious installs before they run, and it can only block what its IOC database knows about. python3 scripts/cursor_install.py --verify re-checks that the hooks are wired and current. Opt out of this hint permanently with REPO_FORENSICS_NUDGE=0. This message will not show again. NUDGE elif [ "$PLATFORM" = "kimi" ]; then cat <<'NUDGE' [repo-forensics] First-run tip: keep this plugin fresh so you get new IOCs, detection rules, and critical security patches. For a security scanner, stale installs are especially dangerous. In Kimi Code: /plugins -> Installed -> repo-forensics -> Enter (install update) Kimi Code does not auto-update plugins installed from GitHub; reinstall when a new version is available. Opt out of this hint permanently with REPO_FORENSICS_NUDGE=0. This message will not show again. NUDGE else cat <<'NUDGE' [repo-forensics] First-run tip: keep your plugin source fresh so you get new IOCs, detection rules, and critical security patches. Update or reinstall repo-forensics from your agent's plugin marketplace when a new version is available. Opt out of this hint permanently with REPO_FORENSICS_NUDGE=0. This message will not show again. NUDGE fi mkdir -p "$(dirname "$NUDGE_FLAG")" 2>/dev/null touch "$NUDGE_FLAG" 2>/dev/null exit 0 - hooks/install_refresh_daemon.shGitHub
Read the script
#!/usr/bin/env bash # Compatibility entry point: install/repair native refresh automation. set -u CALLER_PATH="${PATH:-}" # Trusted command roots for this wrapper's own utilities (dirname, cd, pwd): # FHS + NixOS system profiles (root-owned) + per-user Nix/XDG profile dirs # (same trust as Homebrew). NixOS has no /usr/bin or /bin coreutils. PATH="/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin:/run/current-system/sw/bin:/run/wrappers/bin:/nix/var/nix/profiles/default/bin" if [ -n "${HOME:-}" ]; then PATH="$PATH:$HOME/.nix-profile/bin:$HOME/.local/bin:$HOME/.local/state/nix/profile/bin" fi _rf_profile_user="${USER:-}" if [ -z "$_rf_profile_user" ] && [ -n "${HOME:-}" ]; then _rf_profile_user="${HOME##*/}" fi if [ -n "$_rf_profile_user" ]; then PATH="$PATH:/etc/profiles/per-user/$_rf_profile_user/bin" fi unset _rf_profile_user export PATH SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" || exit 1 PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${KIMI_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/.." && pwd)}}" CONTROLLER="$PLUGIN_ROOT/skills/repo-forensics/scripts/refresh_controller.py" LAUNCHER="$PLUGIN_ROOT/hooks/python-launcher.sh" PATH="${CALLER_PATH:+$CALLER_PATH:}$PATH" \ exec "${BASH:-$(command -v bash || echo /bin/bash)}" "$LAUNCHER" "$CONTROLLER" ensure --json - hooks/python-launcher.shGitHub
- hooks/run_auto_scan.shRunsGitHub
Read the script
#!/usr/bin/env bash # Wrapper for PostToolUse auto-scan that fails loud if the target script # is missing instead of silently dying. # # Caught by torture-room security-sentinel Finding 6. # # Without this wrapper, if the canonical skills/repo-forensics/scripts/ # auto_scan.py path is missing for any reason — plugin install corruption, # partial file restore after a tamper attempt, botched rename, tarball # extraction failure, or a future layout refactor that breaks the hook # path — the agent hook runner would silently swallow the "file not # found" failure and the user would have zero indication that their # security hook is no longer firing. For a security tool, silent failure # IS the worst failure mode. # # This wrapper: # 1. Checks that auto_scan.py exists at the expected canonical path # 2. If missing: prints a clear warning and exits 0 so the hook chain is not broken for the # user's Bash command — graceful degradation over blocking every # command # 3. If present: exec into python3 on auto_scan.py with argv forwarding # preserved (the hook receives stdin from the agent hook runner) set -u PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${KIMI_PLUGIN_ROOT:-}}" SCRIPT="$PLUGIN_ROOT/skills/repo-forensics/scripts/auto_scan.py" LAUNCHER="$PLUGIN_ROOT/hooks/python-launcher.sh" if [ ! -f "$SCRIPT" ]; then echo "[repo-forensics] WARNING: auto_scan.py not found at: $SCRIPT" echo "[repo-forensics] Plugin install may be corrupt, or the skill layout may have changed." echo "[repo-forensics] Auto-scan hook disabled for this command. Update or reinstall repo-forensics." # exit 0 so we don't break the user's Bash command chain. Hook is # PostToolUse — its failure should not retroactively fail the command. exit 0 fi # Bound how often this scan may run (2.14.7). PreToolUse/PostToolUse fire on # every Bash command and each scan fans out into eight scanners, which stacked # into 16 concurrent trees and a load average of 175 before this guard existed. # A MISSING guard file degrades to the old unbounded behaviour on purpose: for a # security tool, silently not scanning is worse than scanning too often. GUARD="${CLAUDE_PLUGIN_ROOT}/hooks/scan_guard.sh" if [ -f "$GUARD" ]; then # shellcheck source=/dev/null . "$GUARD" if ! rf_scan_guard auto 90; then exit 0 fi fi if [ -f "$LAUNCHER" ]; then exec "${BASH:-$(command -v bash || echo /bin/bash)}" "$LAUNCHER" "$SCRIPT" fi exec python3 "$SCRIPT" - hooks/run_pre_scan.shRunsGitHub
Read the script
#!/usr/bin/env bash # Wrapper for PreToolUse pre-scan that fails gracefully if the target script # is missing — same pattern as run_auto_scan.sh. # # IMPORTANT: This is a PreToolUse hook. If pre_scan.py is missing, we MUST # exit 0 (approve) to avoid silently blocking every Bash command. A broken # security hook that blocks all work is worse than a temporarily absent one. set -u PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${KIMI_PLUGIN_ROOT:-}}" SCRIPT="$PLUGIN_ROOT/skills/repo-forensics/scripts/pre_scan.py" LAUNCHER="$PLUGIN_ROOT/hooks/python-launcher.sh" if [ ! -f "$SCRIPT" ]; then echo "[repo-forensics] WARNING: pre_scan.py not found at: $SCRIPT" echo "[repo-forensics] Plugin install may be corrupt, or the skill layout may have changed." echo "[repo-forensics] Pre-scan hook disabled for this command. Update or reinstall repo-forensics." # exit 0 = approve. NEVER exit 2 when the script is missing — that would # block every Bash command. exit 0 fi # Bound how often this scan may run (2.14.7). PreToolUse/PostToolUse fire on # every Bash command and each scan fans out into eight scanners, which stacked # into 16 concurrent trees and a load average of 175 before this guard existed. # A MISSING guard file degrades to the old unbounded behaviour on purpose: for a # security tool, silently not scanning is worse than scanning too often. GUARD="${CLAUDE_PLUGIN_ROOT}/hooks/scan_guard.sh" if [ -f "$GUARD" ]; then # shellcheck source=/dev/null . "$GUARD" if ! rf_scan_guard pre 0; then exit 0 fi fi if [ -f "$LAUNCHER" ]; then exec "${BASH:-$(command -v bash || echo /bin/bash)}" "$LAUNCHER" "$SCRIPT" fi exec python3 "$SCRIPT" - hooks/run_session_scan.shRunsGitHub
Read the script
#!/usr/bin/env bash # Wrapper for SessionStart session_scan.py — same safety pattern as the # other hook wrappers. # # SessionStart hooks should NEVER prevent a session from starting. # If session_scan.py is missing, we log a warning and exit cleanly. set -u PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-${KIMI_PLUGIN_ROOT:-}}" SCRIPT="$PLUGIN_ROOT/skills/repo-forensics/scripts/session_scan.py" LAUNCHER="$PLUGIN_ROOT/hooks/python-launcher.sh" ENSURE_REFRESH="$PLUGIN_ROOT/hooks/ensure_refresh_daemon.sh" # Bootstrap or repair the background updater before checking freshness. This # stays silent and never blocks SessionStart if the platform scheduler fails. if [ -f "$ENSURE_REFRESH" ]; then "${BASH:-$(command -v bash || echo /bin/bash)}" "$ENSURE_REFRESH" || true fi if [ ! -f "$SCRIPT" ]; then echo "[repo-forensics] WARNING: session_scan.py not found at: $SCRIPT" echo "[repo-forensics] Session security scan disabled. Update or reinstall repo-forensics." exit 0 fi # Bound how often this scan may run (2.14.7). PreToolUse/PostToolUse fire on # every Bash command and each scan fans out into eight scanners, which stacked # into 16 concurrent trees and a load average of 175 before this guard existed. # A MISSING guard file degrades to the old unbounded behaviour on purpose: for a # security tool, silently not scanning is worse than scanning too often. GUARD="${CLAUDE_PLUGIN_ROOT}/hooks/scan_guard.sh" if [ -f "$GUARD" ]; then # shellcheck source=/dev/null . "$GUARD" if ! rf_scan_guard session 60; then exit 0 fi fi if [ -f "$LAUNCHER" ]; then exec "${BASH:-$(command -v bash || echo /bin/bash)}" "$LAUNCHER" "$SCRIPT" fi exec python3 "$SCRIPT" - hooks/scan_guard.shGitHub
- hooks/uninstall_refresh_daemon.shGitHub
All 9 scripts are listed above. The source is inlined for 6 of them, starting with whatever hooks.json actually runs. See all of them in the repo.
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Offline security scanner for AI-agent repos, skills, plugins, and MCP servers.
Repo: alexgreensh/repo-forensics

