Skip to content
Security
Hook

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.

From plugin
repo-forensics
1775 skills3 hooks
Install
> /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

  • MatchesBashbash "${CLAUDE_PLUGIN_ROOT}/hooks/run_pre_scan.sh"

PostToolUse

  • MatchesBashbash "${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"
Read hooks/hooks.json

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.

Ships withrepo-forensics

Offline security scanner for AI-agent repos, skills, plugins, and MCP servers.

Get the whole plugin
Stats
177
Stars
29
Forks
Active
Maintenance
Python
Language
17h ago
Last commit
6mo ago
Created

Repo: alexgreensh/repo-forensics