Hooks
What deadeye runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
> /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
- Matches
Agent"${CLAUDE_PLUGIN_ROOT}/hooks/deadeye-hook.sh" PreToolUse - Matches
Bash|Edit|Write|Read|Grep|WebFetch|Workflow"${CLAUDE_PLUGIN_ROOT}/hooks/deadeye-hook.sh" PreToolUse
PostToolUse
- Matches
Bash|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
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.
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.

