Skip to content
Development
Hook

Hooks

What deadeye runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
deadeye
511 skills3 commands7 hooks
Install
> /plugin marketplace add deepaksinghcs14/deadeye-cc
> /plugin install deadeye@deadeye

Ships with deadeye. Installing the plugin gets these hooks.

What fires, and when

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.

  • "${CLAUDE_PLUGIN_ROOT}/hooks/deadeye-hook.sh" SessionStart

UserPromptSubmit

Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.

  • "${CLAUDE_PLUGIN_ROOT}/hooks/deadeye-hook.sh" UserPromptSubmit

PreToolUse

  • MatchesAgent"${CLAUDE_PLUGIN_ROOT}/hooks/deadeye-hook.sh" PreToolUse
  • MatchesBash|Edit|Write|Read|Grep|WebFetch|Workflow"${CLAUDE_PLUGIN_ROOT}/hooks/deadeye-hook.sh" PreToolUse

PostToolUse

  • MatchesBash|Edit|Write|Agent|Read|Grep|Glob|WebFetch|WebSearch|mcp__.*"${CLAUDE_PLUGIN_ROOT}/hooks/deadeye-hook.sh" PostToolUse

SubagentStart

  • "${CLAUDE_PLUGIN_ROOT}/hooks/deadeye-hook.sh" SubagentStart

Stop

  • "${CLAUDE_PLUGIN_ROOT}/hooks/deadeye-hook.sh" Stop

SessionEnd

  • "${CLAUDE_PLUGIN_ROOT}/hooks/deadeye-hook.sh" SessionEnd
Read hooks/hooks.json

Where it lives

  • hooks/bootstrap.shGitHub
    Read the script
    #!/usr/bin/env bash
    # Best-effort background download of the deadeye release binary, sha256
    # checksum-verified against the release's checksums.txt. Silent on any
    # failure -- by the time this runs, the hook that spawned it has already
    # returned {} to Claude Code (INV-5).
    #
    # Also doubles as the update path: an install is only skipped if it's
    # already at the version plugin.json declares. Comparing against a local
    # file (rather than querying GitHub for "latest") avoids a network round
    # trip on every check -- deadeye-hook.sh calls this at most once per
    # session, but it's still cheap to keep cheap.
    set -u
    
    REPO="deepaksinghcs14/deadeye-cc"
    DEST_DIR="$HOME/.deadeye/bin"
    DEST="$DEST_DIR/deadeye"
    LOCK_DIR="$DEST_DIR/.bootstrap.lock"
    INSTALL_TMP=""
    
    mkdir -p "$DEST_DIR" 2>/dev/null || exit 0
    # `mkdir -p -m` only guarantees the mode on the deepest directory (BSD and
    # GNU mkdir both leave intermediate dirs at the process umask), and this is
    # the path that FIRST creates ~/.deadeye on a fresh machine -- chmod both
    # explicitly so the state dir matches the 0700 every Go writer in this repo
    # assumes, regardless of the caller's umask.
    chmod 700 "$HOME/.deadeye" "$DEST_DIR" 2>/dev/null
    
    # One install/update at a time. deadeye-hook.sh only fires this on
    # SessionStart, but two Claude Code windows can start within the same
    # second -- without this, both race to curl+mv onto the same destination.
    # `mkdir` as a lock is atomic even across processes/machines sharing $HOME.
    if ! mkdir "$LOCK_DIR" 2>/dev/null; then
      # Someone else is already installing. If that lock is old enough to be
      # from a run that was SIGKILLed mid-install rather than one still in
      # progress, clear it and take over; otherwise just exit -- the next
      # SessionStart retries.
      if [ -n "$(find "$LOCK_DIR" -maxdepth 0 -mmin +10 2>/dev/null)" ]; then
        rmdir "$LOCK_DIR" 2>/dev/null
        mkdir "$LOCK_DIR" 2>/dev/null || exit 0
      else
        exit 0
      fi
    fi
    trap 'rmdir "$LOCK_DIR" 2>/dev/null; rm -f "$INSTALL_TMP"' EXIT
    
    PLUGIN_VERSION=""
    if [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/.claude-plugin/plugin.json" ]; then
      PLUGIN_VERSION="$(grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$CLAUDE_PLUGIN_ROOT/.claude-plugin/plugin.json" | head -1 | sed -E 's/.*"([^"]+)"$/\1/')"
    fi
    
    if [ -x "$DEST" ]; then
      # Can't tell whether it's stale without a version to compare against --
      # leave a working binary alone rather than guess.
      [ -z "$PLUGIN_VERSION" ] && exit 0
      INSTALLED_VERSION="$("$DEST" version 2>/dev/null | awk '{print $2}')"
      [ "$INSTALLED_VERSION" = "$PLUGIN_VERSION" ] && exit 0
    fi
    command -v curl >/dev/null 2>&1 || exit 0
    
    OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
    ARCH="$(uname -m)"
    case "$ARCH" in
      x86_64) ARCH="amd64" ;;
      arm64|aarch64) ARCH="arm64" ;;
      *) exit 0 ;;
    esac
    
    LATEST_URL="https://github.com/${REPO}/releases/latest/download"
    ASSET="deadeye_${OS}_${ARCH}"
    TMP="$(mktemp -d)" || exit 0
    trap 'rmdir "$LOCK_DIR" 2>/dev/null; rm -f "$INSTALL_TMP"; rm -rf "$TMP"' EXIT
    
    # Pin to the checked-out plugin's own version rather than always pulling
    # "latest" -- otherwise the version comparison above (against
    # plugin.json) and what actually gets downloaded (always latest) can
    # permanently disagree, re-downloading the full binary every single
    # session without ever converging. Fall back to latest once if the
    # pinned tag 404s (checkout ahead of the release, or an older plugin
    # checkout with no matching release asset name).
    if [ -n "$PLUGIN_VERSION" ]; then
      BASE_URL="https://github.com/${REPO}/releases/download/v${PLUGIN_VERSION}"
    else
      BASE_URL="$LATEST_URL"
    fi
    
    # A plugin version whose release assets don't exist yet (the minutes between
    # a tag and its finished build -- or forever, if that build failed) falls
    # back to `latest`, which installs something still BEHIND plugin.json. The
    # hook then sees "managed < plugin" again next session and re-downloads, on
    # every session, indefinitely. Stamp the attempt and don't retry the same
    # version for 24h.
    STAMP="$HOME/.deadeye/.bootstrap-attempted-${PLUGIN_VERSION:-latest}"
    if [ -f "$STAMP" ]; then
      STAMP_AGE=$(( $(date +%s) - $(stat -f %m "$STAMP" 2>/dev/null || stat -c %Y "$STAMP" 2>/dev/null || echo 0) ))
      [ "$STAMP_AGE" -lt 86400 ] && exit 0
    fi
    
    if ! curl -fsSL -o "$TMP/deadeye" "$BASE_URL/$ASSET"; then
      [ "$BASE_URL" = "$LATEST_URL" ] && { mkdir -p "$HOME/.deadeye" && : > "$STAMP"; exit 0; }
      BASE_URL="$LATEST_URL"
      mkdir -p "$HOME/.deadeye" && : > "$STAMP"
      curl -fsSL -o "$TMP/deadeye" "$BASE_URL/$ASSET" || exit 0
    fi
    curl -fsSL -o "$TMP/checksums.txt" "$BASE_URL/checksums.txt" || exit 0
    
    WANT="$(grep " $ASSET\$" "$TMP/checksums.txt" | awk '{print $1}')"
    [ -n "$WANT" ] || exit 0
    
    if command -v sha256sum >/dev/null 2>&1; then
      GOT="$(sha256sum "$TMP/deadeye" | awk '{print $1}')"
    else
      GOT="$(shasum -a 256 "$TMP/deadeye" | awk '{print $1}')"
    fi
    [ "$WANT" = "$GOT" ] || exit 0
    
    # Install atomically. $TMP (mktemp -d, usually $TMPDIR) is frequently a
    # different filesystem from $HOME, so `mv` straight into $DEST would
    # silently degrade to copy-then-unlink -- not atomic, and on an update it
    # would truncate-and-rewrite $DEST in place while deadeye-hook.sh's
    # `[ -x "$DEST" ]` check could see a half-written binary mid-copy.
    # chmod BEFORE the same-filesystem rename, then rename within $DEST_DIR --
    # that final step is what's actually atomic.
    chmod +x "$TMP/deadeye"
    INSTALL_TMP="$DEST_DIR/.deadeye.tmp.$$"
    cp "$TMP/deadeye" "$INSTALL_TMP" && mv "$INSTALL_TMP" "$DEST"
    
  • hooks/deadeye-codex-hook.shGitHub
    Read the script
    #!/usr/bin/env bash
    # deadeye hook adapter for Codex CLI. Installed by `deadeye init codex`
    # to ~/.deadeye/hooks/ and referenced from ~/.codex/hooks.json. Same
    # contract as the Claude Code adapter minus the plugin bootstrap: Codex
    # installs have no marketplace, so the binary that ran `init codex` is
    # the binary; updates are manual.
    set -u
    EVENT="${1:-}"
    
    BIN="$(command -v deadeye 2>/dev/null || true)"
    if [ -z "$BIN" ] || [ ! -x "$BIN" ]; then
      BIN="$HOME/.deadeye/bin/deadeye"
    fi
    if [ ! -x "$BIN" ]; then
      cat > /dev/null 2>&1 || true
      printf '{}'
      exit 0
    fi
    
    # Capture rather than exec: if the binary dies without output, Codex
    # still gets valid JSON (fail open, INV-5).
    out="$("$BIN" hook "$EVENT" --host codex 2>/dev/null)" || true
    [ -n "$out" ] || out="{}"
    printf '%s' "$out"
    
  • hooks/deadeye-gemini-hook.shGitHub
    Read the script
    #!/usr/bin/env bash
    # deadeye hook adapter for Gemini CLI. Installed by `deadeye init gemini`
    # to ~/.deadeye/hooks/ and referenced from the deadeye Gemini extension's
    # hooks/hooks.json. Gemini passes the hook payload as JSON on stdin and
    # reads the response as JSON on stdout -- the binary handles both; the
    # --host gemini flag selects Gemini's output dialect (hookSpecificOutput.
    # tool_input, decision:deny, etc.). The event name is passed as $1 (the
    # canonical Claude event the daemon switches on), mapped from Gemini's own
    # event name in hooks.json.
    set -u
    EVENT="${1:-}"
    
    BIN="$(command -v deadeye 2>/dev/null || true)"
    if [ -z "$BIN" ] || [ ! -x "$BIN" ]; then
      BIN="$HOME/.deadeye/bin/deadeye"
    fi
    if [ ! -x "$BIN" ]; then
      cat > /dev/null 2>&1 || true
      printf '{}'
      exit 0
    fi
    
    # Capture rather than exec: if the binary dies without output, Gemini
    # still gets valid JSON (fail open, INV-5).
    out="$("$BIN" hook "$EVENT" --host gemini 2>/dev/null)" || true
    [ -n "$out" ] || out="{}"
    printf '%s' "$out"
    
  • hooks/deadeye-hook.ps1GitHub
    Read the script
    # Resolves the deadeye binary and forwards this hook invocation to it.
    # Fail-open per INV-5: any resolution or execution failure prints {} and
    # exits 0.
    #
    # Binary resolution mirrors deadeye-hook.sh: a `deadeye` on PATH is used only
    # when it is at least the plugin's version; a STALE PATH binary defers to the
    # managed %USERPROFILE%\.deadeye\bin\deadeye.exe so plugin updates aren't
    # shadowed. NOTE: Windows self-bootstrap (auto-downloading the managed binary,
    # as hooks/bootstrap.sh does on macOS/Linux) is still not implemented -- until
    # then, Windows users must put deadeye.exe on PATH or at that managed path
    # themselves. deadeye's own version-skew warning (see /deadeye-status) flags a
    # stale binary when it happens.
    param([string]$Event)
    $ErrorActionPreference = 'SilentlyContinue'
    
    # Recursion guard: deadeye's AI routing judge spawns a nested `claude -p`
    # session; it must run no deadeye hooks (no re-judging, no cost).
    if ($env:DEADEYE_JUDGE) { Write-Output '{}'; exit 0 }
    
    function Get-PluginVersion {
        $pj = "$env:CLAUDE_PLUGIN_ROOT\.claude-plugin\plugin.json"
        if ($env:CLAUDE_PLUGIN_ROOT -and (Test-Path $pj)) {
            try { return (Get-Content $pj -Raw | ConvertFrom-Json).version } catch { return $null }
        }
        return $null
    }
    
    function Get-BinVersion([string]$bin) {
        try { $o = & $bin version 2>$null; if ($o) { return ($o -split '\s+')[1] } } catch {}
        return $null
    }
    
    function ConvertTo-VerParts([string]$s) {
        $out = @(0, 0, 0)
        if ($s) {
            $s = ($s -replace '^v', '') -replace '[^0-9.].*$', ''
            $parts = $s -split '\.'
            for ($i = 0; $i -lt 3; $i++) {
                if ($i -lt $parts.Count -and $parts[$i] -match '^\d+$') { $out[$i] = [int]$parts[$i] }
            }
        }
        return , $out
    }
    
    function Test-VerGE([string]$a, [string]$b) {
        $pa = ConvertTo-VerParts $a
        $pb = ConvertTo-VerParts $b
        for ($i = 0; $i -lt 3; $i++) {
            if ($pa[$i] -gt $pb[$i]) { return $true }
            if ($pa[$i] -lt $pb[$i]) { return $false }
        }
        return $true
    }
    
    $pv = Get-PluginVersion
    $pathCmd = Get-Command deadeye -ErrorAction SilentlyContinue
    $pathBin = if ($pathCmd) { $pathCmd.Source } else { $null }
    $managed = "$env:USERPROFILE\.deadeye\bin\deadeye.exe"
    $hasManaged = Test-Path $managed
    
    $binPath = $null
    if ($pathBin) {
        if ((-not $pv) -or (Test-VerGE (Get-BinVersion $pathBin) $pv)) {
            $binPath = $pathBin          # no plugin context, or current/newer build -- stays in charge
        } elseif ($hasManaged) {
            $binPath = $managed          # stale PATH binary -> managed one takes over
        } else {
            $binPath = $pathBin          # stale, but nothing managed present
        }
    } elseif ($hasManaged) {
        $binPath = $managed
    }
    
    if (-not $binPath) {
        Write-Output '{}'
        exit 0
    }
    
    try {
        $out = & $binPath hook $Event 2>$null
    } catch {
        $out = $null
    }
    
    if ([string]::IsNullOrWhiteSpace($out)) {
        Write-Output '{}'
    } else {
        Write-Output $out
    }
    
  • hooks/deadeye-hook.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Resolves the deadeye binary and forwards this hook invocation to it.
    # Fail-open per INV-5: any resolution or execution failure prints {} and
    # exits 0 -- a broken policy layer must never block the user's work.
    #
    # Binary resolution keeps auto-update RELIABLE. A `deadeye` on PATH is used
    # only when it is at least the plugin's version. A STALE PATH binary (an old
    # `go install`ed one) is bypassed in favor of the managed, self-updating
    # ~/.deadeye/bin/deadeye -- otherwise it would shadow the managed binary
    # forever and never see a plugin update. A current-or-newer PATH build still
    # wins, so a dev's own build stays in charge. Version checks are only paid
    # when a PATH binary actually exists (the ambiguous case); the common
    # plugin-only install resolves straight to the managed binary.
    set -u
    
    # Recursion guard: deadeye's own AI routing judge (mode.routing_judge) spawns a
    # nested `claude -p` session to classify a task. That session must run no
    # deadeye hooks -- no re-judging, no cost from the judge session itself.
    [ -n "${DEADEYE_JUDGE:-}" ] && { echo '{}'; exit 0; }
    
    EVENT="${1:-}"
    # `set -u` above turns an unset HOME into "unbound variable", exit 1, with
    # no JSON at all -- noisy on every single tool call in an environment that
    # strips HOME. Fall back rather than fail: the hook contract is to always
    # answer, even if that answer is {}.
    HOME="${HOME:-$(cd ~ 2>/dev/null && pwd || echo /tmp)}"
    export HOME
    MANAGED="$HOME/.deadeye/bin/deadeye"
    
    plugin_version() {
      [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] && [ -f "$CLAUDE_PLUGIN_ROOT/.claude-plugin/plugin.json" ] || return 0
      grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' "$CLAUDE_PLUGIN_ROOT/.claude-plugin/plugin.json" | head -1 | sed -E 's/.*"([^"]+)"$/\1/'
    }
    
    bin_version() { "$1" version 2>/dev/null | awk '{print $2}'; }
    
    # ver_ge A B: exit 0 if A >= B as x.y.z. Strips a leading v and any -dev /
    # prerelease suffix; a missing or unparseable version compares as 0.0.0, so an
    # unknowable PATH binary is treated as behind (safe: defer to the managed one).
    ver_ge() {
      awk -v a="$1" -v b="$2" 'BEGIN{
        sub(/^v/,"",a); sub(/^v/,"",b);
        gsub(/[^0-9.].*$/,"",a); gsub(/[^0-9.].*$/,"",b);
        na=split(a,x,"."); nb=split(b,y,".");
        for(i=1;i<=3;i++){ai=(i<=na?x[i]+0:0); bi=(i<=nb?y[i]+0:0); if(ai>bi)exit 0; if(ai<bi)exit 1}
        exit 0
      }'
    }
    
    PV="$(plugin_version)"
    PATH_BIN="$(command -v deadeye 2>/dev/null || true)"
    
    BIN=""
    if [ -n "$PATH_BIN" ]; then
      if [ -z "$PV" ] || ver_ge "$(bin_version "$PATH_BIN")" "$PV"; then
        BIN="$PATH_BIN"          # no plugin context, or a current/newer build -- stays in charge
      elif [ -x "$MANAGED" ]; then
        BIN="$MANAGED"           # PATH binary is STALE -> the self-updating managed binary takes over
      else
        BIN="$PATH_BIN"          # stale, but nothing managed yet; the bootstrap below fixes next session
      fi
    elif [ -x "$MANAGED" ]; then
      BIN="$MANAGED"
    fi
    
    # Keep the managed binary converging to the plugin version: on SessionStart,
    # bootstrap it if it's missing or behind. Never touches a PATH binary.
    if [ "$EVENT" = "SessionStart" ] && [ -n "${CLAUDE_PLUGIN_ROOT:-}" ]; then
      if [ ! -x "$MANAGED" ] || { [ -n "$PV" ] && ! ver_ge "$(bin_version "$MANAGED")" "$PV"; }; then
        ( "${CLAUDE_PLUGIN_ROOT}/hooks/bootstrap.sh" >/dev/null 2>&1 & ) 2>/dev/null
      fi
    fi
    
    if [ -z "$BIN" ]; then
      echo '{}'
      exit 0
    fi
    
    OUT="$("$BIN" hook "$EVENT" 2>/dev/null)"
    if [ -z "$OUT" ]; then
      echo '{}'
    else
      echo "$OUT"
    fi
    
  • hooks/deadeye-statusline.shGitHub
    Read the script
    #!/usr/bin/env bash
    # Statusline badge for deadeye's coder mode. Claude Code pipes session JSON
    # on stdin; the session_id in it selects this session's own mode file
    # (~/.deadeye/coder-mode.<id>), so concurrent sessions each show their own
    # badge. No session_id on stdin -> the global ~/.deadeye/coder-mode
    # fallback (last writer wins). Silent when the mode is off/absent -- an
    # empty statusline segment, not an error.
    set -u
    
    # A kill switch makes deadeye silent by design -- but silence is
    # indistinguishable from "forgot I turned it off". Surface it. Env check
    # only: no sockets, no files, nothing slow on the statusline render path.
    if [ "${DEADEYE:-}" = "off" ]; then
      printf '\033[38;5;245m[DEADEYE:OFF]\033[0m'; exit 0
    fi
    if [ "${DEADEYE_CODER:-}" = "off" ]; then
      printf '\033[38;5;245m[DEADEYE:CODER OFF]\033[0m'; exit 0
    fi
    
    MODE_FILE="$HOME/.deadeye/coder-mode"
    sid="$(cat 2>/dev/null | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -1)"
    if [ -n "$sid" ]; then
      # Mirror the daemon's sanitization: anything outside [A-Za-z0-9-] -> _
      sid_safe="$(printf '%s' "$sid" | tr -c 'A-Za-z0-9-' '_')"
      [ -f "$HOME/.deadeye/coder-mode.$sid_safe" ] && MODE_FILE="$HOME/.deadeye/coder-mode.$sid_safe"
    fi
    [ -f "$MODE_FILE" ] || exit 0
    level="$(tr -d '[:space:]' < "$MODE_FILE")"
    [ -n "$level" ] || exit 0
    
    case "$level" in
      marksman) printf '\033[38;5;108m[DEADEYE]\033[0m' ;;          # green -- the default discipline
      spotter)  printf '\033[38;5;65m[DEADEYE:SPOTTER]\033[0m' ;;   # muted green -- light touch
      sniper)   printf '\033[38;5;173m[DEADEYE:SNIPER]\033[0m' ;;   # amber -- maximum minimalism
      review)   printf '\033[38;5;109m[DEADEYE:REVIEW]\033[0m' ;;   # steel blue -- review pass
      *) exit 0 ;;
    esac
    

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 withdeadeye

Claude Code plugin that fits the model, effort, and context to each task — fewer tokens, same quality. Deterministic policy kernel in the hooks; every number it reports is measured, not estimated.

Get the whole plugin, auto-invoked
Stats
5
Stars
1
Forks
Active
Maintenance
Go
Language
MIT
License
15h ago
Last commit
1mo ago
Created

Repo: deepaksinghcs14/deadeye-cc