Skip to content
Agent Orchestration
Hook

Hooks

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

From plugin
agentsmith
17911 skills3 hooks
Install
$ npx -y skills add PromptPartner/agentsmith --agent claude-code

Ships with agentsmith. Installing the plugin gets these hooks.

Where it lives

  • hooks/context-budget-nudge.shGitHub
    Read the script
    #!/usr/bin/env bash
    # Stop hook — BEST-EFFORT context-budget nudge (opt-in, experimental).
    #
    # HONEST CAVEAT: no Claude Code hook receives the live context-% (only the statusline does), so
    # this reads the % that config/statusline.py persisted to a temp file. That makes it
    # inherently fragile — the file can be stale (statusline hasn't rendered since the last turn) or
    # missing (statusline not installed / crashed). The RELIABLE handoff path is the "handoff"
    # keyword (hooks/handoff-on-keyword.sh) and the human-watched ctx:NN% gauge. Treat this as a
    # backstop, not a guarantee. See hooks/README.md.
    #
    # Behaviour: when an operator-configured context USED threshold is reached, it nudges ONCE per
    # session (a marker file prevents a block-loop), asking the agent to safe-state + write a recall
    # prompt before stopping.
    #
    # No percentage is a reliable universal quality boundary: behavior varies by model, task, prompt
    # position, and conversation history. The hook is therefore disabled until HANDOFF_PCT_THRESHOLD
    # is set to an integer from 1–100. Treat that value as a personal workflow cue, calibrated on your
    # own tasks. Signals older than 300 seconds fail open; tune that bounded freshness window with
    # HANDOFF_SIGNAL_MAX_AGE_SECONDS (1–3600).
    #
    # Wire it (global ~/.claude/settings.json):
    #   "hooks": { "Stop": [ { "hooks": [
    #     { "type": "command", "command": "bash ~/.claude/hooks/context-budget-nudge.sh" } ] } ] }
    set -euo pipefail
    command -v jq >/dev/null 2>&1 || exit 0
    
    THRESHOLD="${HANDOFF_PCT_THRESHOLD:-}"
    MAX_SIGNAL_AGE="${HANDOFF_SIGNAL_MAX_AGE_SECONDS:-300}"
    input=$(cat)
    sid=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null) || exit 0
    
    # Invalid configuration and unscoped events must stay silent. Falling back to a shared
    # "default" file can leak a percentage between sessions and produce a false handoff cue.
    [ -n "$THRESHOLD" ] || exit 0
    [[ "$THRESHOLD" =~ ^[0-9]+$ ]] || exit 0
    [ "$THRESHOLD" -ge 1 ] 2>/dev/null && [ "$THRESHOLD" -le 100 ] 2>/dev/null || exit 0
    [[ "$MAX_SIGNAL_AGE" =~ ^[0-9]+$ ]] || exit 0
    [ "$MAX_SIGNAL_AGE" -ge 1 ] 2>/dev/null && [ "$MAX_SIGNAL_AGE" -le 3600 ] 2>/dev/null || exit 0
    [[ "$sid" =~ ^[A-Za-z0-9._-]{1,128}$ ]] || exit 0
    
    pf="${TMPDIR:-/tmp}/claude-ctx-${sid}.pct"
    marker="${TMPDIR:-/tmp}/claude-ctx-${sid}.nudged"
    
    [ -f "$pf" ] || exit 0                       # no signal yet → do nothing
    [ -f "$marker" ] && exit 0                    # already nudged this session → don't loop
    
    # A statusline may stop rendering or a runtime may reuse a session identifier after recovery.
    # Never act on a side-channel value older than five minutes (configurable, capped at one hour).
    if mtime=$(stat -c %Y "$pf" 2>/dev/null); then       # GNU/Linux
      :
    elif mtime=$(stat -f %m "$pf" 2>/dev/null); then    # BSD/macOS
      :
    else
      exit 0
    fi
    now=$(date +%s 2>/dev/null) || exit 0
    [[ "$mtime" =~ ^[0-9]+$ ]] && [[ "$now" =~ ^[0-9]+$ ]] || exit 0
    age=$((now - mtime))
    [ "$age" -ge 0 ] 2>/dev/null && [ "$age" -le "$MAX_SIGNAL_AGE" ] 2>/dev/null || exit 0
    
    pct=$(tr -d '[:space:]' < "$pf" 2>/dev/null) || exit 0
    [[ "$pct" =~ ^[0-9]+([.][0-9]+)?$ ]] || exit 0
    pint=${pct%%.*}
    [ "$pint" -le 100 ] 2>/dev/null || exit 0
    
    if [ "$pint" -ge "$THRESHOLD" ]; then
      output=$(jq -n --arg p "$pint" '{decision:"block", reason:("Context is at " + $p + "% used — at your configured handoff cue. Context reliability varies by model, task, and conversation history; this percentage is a personal workflow heuristic, not a quality boundary. Before you stop: bring the working tree to a safe state, write a handoff note, and output a ready-to-paste recall prompt with the item, branch, completed work, exact next step, and gotchas. Then stop.")}') || exit 0
      : > "$marker"
      printf '%s\n' "$output"
    fi
    exit 0
    
  • hooks/handoff-on-keyword.shGitHub
    Read the script
    #!/usr/bin/env bash
    # UserPromptSubmit hook — RELIABLE handoff trigger for Claude Code and Codex.
    # When the user says "handoff" or "wrap up", inject the handoff protocol so the agent
    # safe-states and emits a recall prompt before context runs out. This path needs NO
    # context-% visibility — it keys off the prompt text, which the hook always receives.
    # See core/50-git-and-handoff and hooks/README.md.
    #
    # Wire it (global ~/.claude/settings.json):
    #   "hooks": { "UserPromptSubmit": [ { "hooks": [
    #     { "type": "command", "command": "bash ~/.claude/hooks/handoff-on-keyword.sh" } ] } ] }
    set -euo pipefail
    command -v jq >/dev/null 2>&1 || exit 0   # no jq → silently no-op, never block the prompt
    
    input=$(cat)
    prompt=$(printf '%s' "$input" | jq -r '.prompt_text // .prompt // empty' 2>/dev/null) || exit 0
    shopt -s nocasematch 2>/dev/null || true
    
    if [[ "$prompt" =~ handoff || "$prompt" =~ wrap[[:space:]]+up ]]; then
      context='HANDOFF REQUESTED. Before anything else: (1) bring the working tree to a SAFE STATE — commit or stash so nothing half-edited is lost; (2) write a durable handoff note (memory and/or the project tracker); (3) output a ready-to-paste fresh-chat recall prompt that carries the issue/branch IDs, what is done (commit SHAs / PR links / merge state), the EXACT next step, and the gotchas/decisions a fresh session would otherwise re-derive. Keep it tight; do this before any new work.'
    
      # Current Claude Code and Codex both require event-specific context beneath hookSpecificOutput.
      # Preserve the pre-current-Claude prompt_text/top-level shape for existing installations that
      # still send it. Unknown/malformed input stays fail-open: it never blocks the user's prompt.
      legacy_claude=$(printf '%s' "$input" | jq -r 'has("prompt_text") and (has("prompt") | not)' 2>/dev/null) || exit 0
      if [ "$legacy_claude" = true ]; then
        jq -n --arg c "$context" '{additionalContext:$c}'
      else
        jq -n --arg c "$context" '{hookSpecificOutput:{hookEventName:"UserPromptSubmit", additionalContext:$c}}'
      fi
    fi
    exit 0
    
  • hooks/ui-design-reminder.shGitHub
    Read the script
    #!/usr/bin/env bash
    # PreToolUse nudge — remind the agent to consult DESIGN.md when it edits UI (opt-in, fail-safe).
    #
    # Fires at most ONCE per session, and only when BOTH hold:
    #   • a Claude Edit/Write/MultiEdit or Codex apply_patch targets a UI file — *.tsx *.jsx *.vue
    #     *.svelte *.css *.scss *.less *.astro, or a path under a components/ or ui/ directory; AND
    #   • a DESIGN.md exists at the project root (so backend/CLI/library projects never see it).
    # It NEVER blocks the edit and NEVER auto-approves it: it only injects a one-line reminder via the
    # PreToolUse `additionalContext` field, then lets the normal permission flow proceed. Every surprise
    # — no jq, malformed input, non-UI path, no DESIGN.md, already nudged — is a silent no-op.
    #
    # Model: hooks/context-budget-nudge.sh (once-per-session marker, jq-guarded, fail-open).
    #
    # Wire it (global ~/.claude/settings.json):
    #   "hooks": { "PreToolUse": [ { "matcher": "Edit|Write|MultiEdit", "hooks": [
    #     { "type": "command", "command": "bash ~/.claude/hooks/ui-design-reminder.sh" } ] } ] }
    #
    # Hook stdin/output schemas can drift between runtimes; this stays fail-open (a bad read or an
    # unknown field → no-op, never a blocked edit). Claude file tools provide tool_input.file_path;
    # Codex apply_patch provides patch text in tool_input.command. Both accept
    # hookSpecificOutput.additionalContext with exit 0 for normal flow.
    set -euo pipefail
    command -v jq >/dev/null 2>&1 || exit 0
    
    input=$(cat)
    tool=$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null) || exit 0
    case "$tool" in
      Edit|Write|MultiEdit)
        fp=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null) || exit 0
        ;;
      apply_patch)
        # Codex sends the whole patch as a string. Extract every affected path, then choose the first
        # UI path; a backend file appearing first in a multi-file patch must not hide a later UI edit.
        paths=$(printf '%s' "$input" | jq -r '
          .tool_input.command // empty
          | split("\n")[]
          | select(test("^\\*\\*\\* (Add|Update|Delete) File: |^\\*\\*\\* Move to: "))
          | sub("^\\*\\*\\* (Add|Update|Delete) File: "; "")
          | sub("^\\*\\*\\* Move to: "; "")
          | sub("\\r$"; "")
        ' 2>/dev/null) || exit 0
        fp=""
        while IFS= read -r candidate; do
          candidate=${candidate//\\//}
          case "$candidate" in
            *.tsx|*.jsx|*.vue|*.svelte|*.css|*.scss|*.less|*.astro|*/components/*|*/ui/*)
              fp="$candidate"; break;;
          esac
        done <<< "$paths"
        ;;
      *) exit 0 ;;
    esac
    
    [ -n "$fp" ] || exit 0
    fp=${fp//\\//}                                 # normalise Windows backslashes so dir globs match
    case "$fp" in
      *.tsx|*.jsx|*.vue|*.svelte|*.css|*.scss|*.less|*.astro|*/components/*|*/ui/*) ;;
      *) exit 0 ;;                                  # not a UI file → silent
    esac
    
    # The self-gate: a DESIGN.md at the project root. No design system declared → stay silent, so backend
    # projects never see this. Check the session cwd first, then the git root (covers a subdir cwd).
    cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null) || exit 0
    [ -n "$cwd" ] || cwd=.
    root="$cwd"
    if [ ! -f "$root/DESIGN.md" ]; then
      gr=$(git -C "$cwd" rev-parse --show-toplevel 2>/dev/null) || gr=""
      [ -n "$gr" ] && root="$gr"
    fi
    [ -f "$root/DESIGN.md" ] || exit 0
    
    # Once per session (marker keyed by session_id): a screen's worth of edits nudges once, not N times.
    sid=$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null) || exit 0
    marker="${TMPDIR:-/tmp}/agentsmith-uidesign-${sid:-default}.nudged"
    [ -f "$marker" ] && exit 0
    : > "$marker" 2>/dev/null || true
    
    jq -n --arg f "$fp" '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        additionalContext: ("Editing UI (" + $f + "). This project declares a design system in DESIGN.md — read it and match it (colors, type, spacing, components, states) before changing UI. If DESIGN.md still reads \"[TODO]\", establish the design system first. Update DESIGN.md in the same change when you add or restyle a component.")
      }
    }'
    exit 0
    

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 withagentsmith

Proof before done. Move from vibe coding to agentic engineering. Give Claude Code or Codex your project rules, a definition of done, and checks that produce evidence.

Get the whole plugin
Stats
179
Stars
7
Forks
Active
Maintenance
Python
Language
MIT
License
10h ago
Last commit
3mo ago
Created
3h ago
Added

Repo: PromptPartner/agentsmith