Skip to content
Development
Hook

Hooks

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

From plugin
claudikins-kernel
1274 skills8 agents4 commands8 hooks
Install
$ npx -y skills add povvo/claudikins-kernel --agent claude-code

Ships with claudikins-kernel. 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.

  • Matches*${CLAUDE_PLUGIN_ROOT}/hooks/session-startup.sh
  • Matchesclaudikins-kernel:verify${CLAUDE_PLUGIN_ROOT}/hooks/verify-init.sh
  • Matchesclaudikins-kernel:ship${CLAUDE_PLUGIN_ROOT}/hooks/ship-init.sh

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.

  • Matches*${CLAUDE_PLUGIN_ROOT}/hooks/skill-activation-hook.sh
  • Matchesclaudikins-kernel:execute${CLAUDE_PLUGIN_ROOT}/hooks/validate-plan-format.sh
  • Matchesclaudikins-kernel:execute.*(--status|status)${CLAUDE_PLUGIN_ROOT}/hooks/execute-status.sh

SubagentStart

  • Matches*${CLAUDE_PLUGIN_ROOT}/hooks/trace-start.sh
  • Matchesbabyclaude${CLAUDE_PLUGIN_ROOT}/hooks/create-task-branch.sh

SubagentStop

  • Matches*${CLAUDE_PLUGIN_ROOT}/hooks/trace-end.sh

PreToolUse

  • MatchesBash${CLAUDE_PLUGIN_ROOT}/hooks/git-branch-guard.sh${CLAUDE_PLUGIN_ROOT}/hooks/sanitize-bash.sh${CLAUDE_PLUGIN_ROOT}/hooks/merge-gate.sh
  • MatchesTask${CLAUDE_PLUGIN_ROOT}/hooks/pre-task-gate.sh

PostToolUse

  • MatchesEdit|Write${CLAUDE_PLUGIN_ROOT}/hooks/autoformat.sh
  • Matches*${CLAUDE_PLUGIN_ROOT}/hooks/execute-tracker.sh

Stop

  • Matchesclaudikins-kernel:verify${CLAUDE_PLUGIN_ROOT}/hooks/verify-gate.sh
  • Matchesclaudikins-kernel:ship${CLAUDE_PLUGIN_ROOT}/hooks/ship-complete.sh
  • Matchesclaudikins-kernel:outline${CLAUDE_PLUGIN_ROOT}/hooks/validate-plan-completion.sh
  • Matchesclaudikins-kernel:execute${CLAUDE_PLUGIN_ROOT}/hooks/batch-checkpoint-gate.sh

PreCompact

  • Matches*${CLAUDE_PLUGIN_ROOT}/hooks/preserve-state.sh
Read hooks/hooks.json

In the plugin's words

How claudikins-kernel describes its own hook set.

Hooks for claudikins-kernel /outline, /execute, /verify, and /ship command workflows

Where it lives

  • hooks/autoformat.shRunsGitHub
    Read the script
    #!/bin/bash
    # autoformat.sh - PostToolUse hook for claudikins-kernel
    # Runs prettier on edited/written files (Boris's pattern)
    #
    # Only formats files that prettier supports. Fails silently if prettier
    # not installed or file type not supported.
    
    set -euo pipefail
    
    # Don't fail the hook if prettier isn't available, but log for debugging
    trap 'echo "autoformat.sh: non-critical failure at line $LINENO (continuing)" >&2; exit 0' ERR
    
    PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
    
    # Read input from stdin
    INPUT=$(cat)
    
    # Extract file path from tool input
    FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty' 2>/dev/null || echo "")
    
    if [ -z "$FILE_PATH" ]; then
        exit 0
    fi
    
    # Make path absolute if relative
    if [[ "$FILE_PATH" != /* ]]; then
        FILE_PATH="$PROJECT_DIR/$FILE_PATH"
    fi
    
    # Check if file exists
    if [ ! -f "$FILE_PATH" ]; then
        exit 0
    fi
    
    # Get file extension
    EXT="${FILE_PATH##*.}"
    
    # Only format supported file types
    case "$EXT" in
        js|jsx|ts|tsx|json|md|mdx|css|scss|less|html|yaml|yml|graphql|vue|svelte)
            # Check if prettier is available
            if command -v npx &> /dev/null; then
                # Run prettier with --write, allow stderr through for debugging
                npx prettier --write "$FILE_PATH" || true
            elif command -v prettier &> /dev/null; then
                prettier --write "$FILE_PATH" || true
            fi
            ;;
        *)
            # Unsupported file type, skip silently
            ;;
    esac
    
    exit 0
    
  • hooks/batch-checkpoint-gate.shRunsGitHub
    Read the script
    #!/bin/bash
    # batch-checkpoint-gate.sh - Stop hook for /execute
    # Saves checkpoint state when session ends during active execution.
    # Enables resume from last checkpoint if context exhausted or session dies.
    #
    # Exit codes:
    #   0 - Always (checkpoint save, never blocks)
    
    set -euo pipefail
    
    # Get project directory
    PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
    CLAUDE_DIR="$PROJECT_DIR/.claude"
    STATE_FILE="$CLAUDE_DIR/execute-state.json"
    TRACE_FILE="$CLAUDE_DIR/execute-trace.json"
    CHECKPOINTS_DIR="$CLAUDE_DIR/checkpoints"
    
    # Read input JSON from stdin (Stop hook receives stop reason)
    INPUT=$(cat)
    STOP_REASON=$(echo "$INPUT" | jq -r '.stop_reason // "unknown"')
    
    # Check if we're in an active execution session
    if [ ! -f "$STATE_FILE" ]; then
        exit 0  # No active session, nothing to checkpoint
    fi
    
    STATUS=$(jq -r '.status // ""' "$STATE_FILE" 2>/dev/null || echo "")
    if [ "$STATUS" != "executing" ]; then
        exit 0  # Not actively executing
    fi
    
    # Create checkpoints directory if needed
    mkdir -p "$CHECKPOINTS_DIR"
    
    # Generate checkpoint ID
    CHECKPOINT_ID="checkpoint-$(date +%Y%m%d-%H%M%S)"
    CHECKPOINT_FILE="$CHECKPOINTS_DIR/${CHECKPOINT_ID}.json"
    TIMESTAMP=$(date -Iseconds)
    
    # Get current execution state
    CURRENT_BATCH=$(jq -r '.current_batch // 0' "$STATE_FILE" 2>/dev/null || echo "0")
    CURRENT_TASK=$(jq -r '.current_task // null' "$STATE_FILE" 2>/dev/null || echo "null")
    SESSION_ID=$(jq -r '.session_id // "unknown"' "$STATE_FILE" 2>/dev/null || echo "unknown")
    
    # Count task statuses
    TOTAL_TASKS=$(jq -r '.tasks | length // 0' "$STATE_FILE" 2>/dev/null || echo "0")
    COMPLETED_TASKS=$(jq -r '[.tasks[] | select(.status == "completed")] | length // 0' "$STATE_FILE" 2>/dev/null || echo "0")
    IN_PROGRESS_TASKS=$(jq -r '[.tasks[] | select(.status == "in_progress")] | length // 0' "$STATE_FILE" 2>/dev/null || echo "0")
    
    # Build checkpoint data
    CHECKPOINT_DATA=$(jq -n \
        --arg id "$CHECKPOINT_ID" \
        --arg sessionId "$SESSION_ID" \
        --arg timestamp "$TIMESTAMP" \
        --arg stopReason "$STOP_REASON" \
        --argjson currentBatch "$CURRENT_BATCH" \
        --argjson currentTask "$CURRENT_TASK" \
        --argjson totalTasks "$TOTAL_TASKS" \
        --argjson completedTasks "$COMPLETED_TASKS" \
        --argjson inProgressTasks "$IN_PROGRESS_TASKS" \
        '{
          "checkpoint_id": $id,
          "session_id": $sessionId,
          "timestamp": $timestamp,
          "stop_reason": $stopReason,
          "execution_state": {
            "current_batch": $currentBatch,
            "current_task": $currentTask,
            "total_tasks": $totalTasks,
            "completed_tasks": $completedTasks,
            "in_progress_tasks": $inProgressTasks
          },
          "recovery_instructions": "Run claudikins-kernel:execute --resume to continue from this checkpoint"
        }')
    
    # Add full state snapshot
    CHECKPOINT_DATA=$(echo "$CHECKPOINT_DATA" | jq --slurpfile state "$STATE_FILE" '. + {"state_snapshot": $state[0]}')
    
    # Add trace snapshot if exists
    if [ -f "$TRACE_FILE" ]; then
        CHECKPOINT_DATA=$(echo "$CHECKPOINT_DATA" | jq --slurpfile trace "$TRACE_FILE" '. + {"trace_snapshot": $trace[0]}')
    fi
    
    # Save checkpoint
    echo "$CHECKPOINT_DATA" > "$CHECKPOINT_FILE"
    
    # Update state with checkpoint reference
    jq --arg checkpointId "$CHECKPOINT_ID" \
       --arg checkpointFile "$CHECKPOINT_FILE" \
       --arg timestamp "$TIMESTAMP" \
       '. + {
          "last_checkpoint": $checkpointId,
          "last_checkpoint_file": $checkpointFile,
          "last_checkpoint_at": $timestamp
        }' \
       "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
    
    # Clean old checkpoints (keep last 5, only delete files >1min old to avoid race)
    find "$CHECKPOINTS_DIR" -name "checkpoint-*.json" -mmin +1 -type f 2>/dev/null | \
        sort -r | tail -n +6 | xargs -r rm -f
    
    # Build resume message
    if [ "$IN_PROGRESS_TASKS" -gt 0 ]; then
        RESUME_MSG="Execution paused with $IN_PROGRESS_TASKS task(s) in progress."
    else
        RESUME_MSG="Checkpoint saved at batch $CURRENT_BATCH."
    fi
    
    # Build formatted message for user display
    read -r -d '' DISPLAY_MSG << EOM || true
    CHECKPOINT SAVED
    
    Checkpoint: ${CHECKPOINT_ID}
    Batch: ${CURRENT_BATCH}
    Completed: ${COMPLETED_TASKS}/${TOTAL_TASKS} tasks
    
    ${RESUME_MSG}
    Run claudikins-kernel:execute --resume to continue.
    EOM
    
    # Output checkpoint notification using jq for proper JSON escaping
    # Stop events don't support hookSpecificOutput — use systemMessage instead
    jq -n --arg msg "$DISPLAY_MSG" '{
      "systemMessage": $msg
    }'
    
    exit 0
    
  • hooks/block-git-commands.shGitHub
  • hooks/capture-catastrophiser.shGitHub
  • hooks/capture-cynic.shGitHub
  • hooks/capture-perfectionist.shGitHub
  • hooks/capture-research.shGitHub
  • hooks/cleanup-task-worktree.shGitHub
  • hooks/create-task-branch.shRunsGitHub
    Read the script
    #!/bin/bash
    # create-task-branch.sh - SubagentStart hook for /execute
    # Creates git branch AND worktree when babyclaude spawns for task execution.
    # Worktree enables safe parallel execution - each agent gets isolated filesystem.
    #
    # Matcher: babyclaude (only triggers for this agent type)
    # Exit codes:
    #   0 - Branch + worktree created successfully (worktree_path in context)
    #   2 - Creation failed (blocks agent spawn, informs user)
    
    set -euo pipefail
    
    # Get project directory
    PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
    CLAUDE_DIR="$PROJECT_DIR/.claude"
    STATE_FILE="$CLAUDE_DIR/execute-state.json"
    WORKTREE_BASE="/tmp/kernel-worktrees"
    
    # Read input JSON from stdin
    INPUT=$(cat)
    
    # Extract agent name
    AGENT_NAME=$(echo "$INPUT" | jq -r '.agent_name // ""')
    
    # Only act on babyclaude spawns
    if [ "$AGENT_NAME" != "babyclaude" ]; then
        exit 0
    fi
    
    # Extract task info from prompt (passed by execute command)
    # Format expected: TASK_ID: <id> TASK_SLUG: <slug>
    PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""')
    TASK_ID=$(echo "$PROMPT" | grep -oP 'TASK_ID:\s*\K[^\s]+' || echo "")
    TASK_SLUG=$(echo "$PROMPT" | grep -oP 'TASK_SLUG:\s*\K[^\s]+' || echo "unknown")
    
    # If no task ID, this isn't a task execution - allow spawn
    if [ -z "$TASK_ID" ]; then
        exit 0
    fi
    
    # Verify we're in a git repository
    if ! git rev-parse --git-dir > /dev/null 2>&1; then
        echo "ERROR: Not in a git repository. Cannot create task branch." >&2
        echo "" >&2
        echo "The claudikins-kernel:execute command requires a git repository to manage task branches." >&2
        echo "Run 'git init' first, or navigate to an existing repository." >&2
        exit 2
    fi
    
    # Check for uncommitted changes that would prevent branch creation
    if ! git diff-index --quiet HEAD -- 2>/dev/null; then
        echo "ERROR: Uncommitted changes detected. Cannot create task branch." >&2
        echo "" >&2
        echo "Please commit or stash your changes before running claudikins-kernel:execute:" >&2
        echo "  git stash        # Temporarily store changes" >&2
        echo "  git commit -am 'WIP'  # Commit changes" >&2
        exit 2
    fi
    
    # Generate UUID suffix for collision prevention (per branch-collision-detection.md)
    UUID_SUFFIX=$(uuidgen | cut -d'-' -f1)
    
    # Create branch name: execute/task-{id}-{slug}-{uuid}
    BRANCH_NAME="execute/task-${TASK_ID}-${TASK_SLUG}-${UUID_SUFFIX}"
    
    # Create worktree directory
    mkdir -p "$WORKTREE_BASE"
    WORKTREE_PATH="${WORKTREE_BASE}/task-${TASK_ID}-${UUID_SUFFIX}"
    
    # Create branch first (without checkout - we'll use worktree)
    if ! git branch "$BRANCH_NAME" 2>/dev/null; then
        # Branch might already exist from a previous failed attempt - that's ok
        if ! git rev-parse --verify "$BRANCH_NAME" >/dev/null 2>&1; then
            echo "ERROR: Failed to create branch: $BRANCH_NAME" >&2
            exit 2
        fi
    fi
    
    # Create worktree for the branch
    if GIT_OUTPUT=$(git worktree add "$WORKTREE_PATH" "$BRANCH_NAME" 2>&1); then
        # Update state file with branch and worktree info
        if [ -f "$STATE_FILE" ]; then
            jq --arg branch "$BRANCH_NAME" --arg taskId "$TASK_ID" --arg worktree "$WORKTREE_PATH" \
               '.tasks = [.tasks[] | if .id == $taskId then .branch = $branch | .worktree_path = $worktree else . end]' \
               "$STATE_FILE" > "${STATE_FILE}.tmp" && mv "${STATE_FILE}.tmp" "$STATE_FILE"
        fi
    
        # SubagentStart events don't support hookSpecificOutput — use systemMessage instead
        cat <<EOF
    {
      "systemMessage": "WORKTREE_PATH: ${WORKTREE_PATH}\nBRANCH: ${BRANCH_NAME}\n\nYou are working in an isolated worktree. All your file operations happen in: ${WORKTREE_PATH}\n\nDo NOT use git commands - the orchestrator handles all git operations."
    }
    EOF
        exit 0
    else
        # Worktree creation failed - cleanup branch and block
        git branch -D "$BRANCH_NAME" 2>/dev/null || true
    
        echo "ERROR: Failed to create worktree: $WORKTREE_PATH" >&2
        echo "Git error: $GIT_OUTPUT" >&2
        echo "" >&2
        echo "Possible causes:" >&2
        echo "  - Worktree path already exists (stale from previous run)" >&2
        echo "  - Insufficient permissions on /tmp" >&2
        echo "  - Git worktree limit reached" >&2
        echo "" >&2
        echo "To recover:" >&2
        echo "  1. Clean stale worktrees: git worktree prune" >&2
        echo "  2. Remove manually: rm -rf ${WORKTREE_PATH}" >&2
        echo "  3. List worktrees: git worktree list" >&2
        exit 2
    fi
    
  • hooks/execute-status.shRunsGitHub
    Read the script
    #!/bin/bash
    # execute-status.sh - UserPromptSubmit hook for /execute --status
    # Shows current execution status when user requests it.
    #
    # Exit codes:
    #   0 - Always (adds context, never blocks)
    
    set -euo pipefail
    
    # Get project directory
    PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
    CLAUDE_DIR="$PROJECT_DIR/.claude"
    STATE_FILE="$CLAUDE_DIR/execute-state.json"
    
    # Read input JSON from stdin
    INPUT=$(cat)
    
    # Extract prompt from JSON
    PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""')
    
    # Only respond to /execute --status or /execute status
    if ! echo "$PROMPT" | grep -qiE '^/execute.*(--status|status)'; then
        exit 0
    fi
    
    # Check if state file exists
    if [ ! -f "$STATE_FILE" ]; then
        cat <<EOF
    {
      "hookSpecificOutput": {
        "hookEventName": "UserPromptSubmit",
        "additionalContext": "No active execution session found. Run /execute <plan.md> to start."
      }
    }
    EOF
        exit 0
    fi
    
    # Read state file
    STATE=$(cat "$STATE_FILE")
    
    # Extract key information
    SESSION_ID=$(echo "$STATE" | jq -r '.session_id // "unknown"')
    PLAN_SOURCE=$(echo "$STATE" | jq -r '.plan_source // "unknown"')
    STARTED_AT=$(echo "$STATE" | jq -r '.started_at // "unknown"')
    CURRENT_BATCH=$(echo "$STATE" | jq -r '.current_batch // 0')
    TOTAL_BATCHES=$(echo "$STATE" | jq -r '.batches | length // 0')
    STATUS=$(echo "$STATE" | jq -r '.status // "unknown"')
    
    # Count tasks by status
    TOTAL_TASKS=$(echo "$STATE" | jq -r '.tasks | length // 0')
    COMPLETED_TASKS=$(echo "$STATE" | jq -r '[.tasks[] | select(.status == "completed")] | length // 0')
    IN_PROGRESS_TASKS=$(echo "$STATE" | jq -r '[.tasks[] | select(.status == "in_progress")] | length // 0')
    BLOCKED_TASKS=$(echo "$STATE" | jq -r '[.tasks[] | select(.status == "blocked")] | length // 0')
    PENDING_TASKS=$(echo "$STATE" | jq -r '[.tasks[] | select(.status == "pending")] | length // 0')
    
    # Get current batch tasks
    CURRENT_BATCH_TASKS=$(echo "$STATE" | jq -r --argjson batch "$CURRENT_BATCH" '.batches[$batch - 1].tasks // [] | join(", ")' 2>/dev/null || echo "none")
    
    # Calculate age
    if [ "$STARTED_AT" != "unknown" ] && [ "$STARTED_AT" != "null" ]; then
        START_EPOCH=$(date -d "$STARTED_AT" +%s 2>/dev/null || echo "0")
        NOW_EPOCH=$(date +%s)
        AGE_MINUTES=$(( (NOW_EPOCH - START_EPOCH) / 60 ))
        AGE_DISPLAY="${AGE_MINUTES}m ago"
    else
        AGE_DISPLAY="unknown"
    fi
    
    # Build status summary
    read -r -d '' STATUS_SUMMARY << EOM || true
    ## Execution Status
    
    **Session:** ${SESSION_ID}
    **Plan:** ${PLAN_SOURCE}
    **Started:** ${AGE_DISPLAY}
    **Status:** ${STATUS}
    
    ### Progress
    
    | Metric | Count |
    |--------|-------|
    | Total tasks | ${TOTAL_TASKS} |
    | Completed | ${COMPLETED_TASKS} |
    | In progress | ${IN_PROGRESS_TASKS} |
    | Blocked | ${BLOCKED_TASKS} |
    | Pending | ${PENDING_TASKS} |
    
    ### Current Batch
    
    Batch ${CURRENT_BATCH}/${TOTAL_BATCHES}: ${CURRENT_BATCH_TASKS}
    EOM
    
    # Escape for JSON
    STATUS_ESCAPED=$(echo "$STATUS_SUMMARY" | jq -Rs '.')
    
    # Output context injection
    cat <<EOF
    {
      "hookSpecificOutput": {
        "hookEventName": "UserPromptSubmit",
        "additionalContext": ${STATUS_ESCAPED}
      }
    }
    EOF
    
    exit 0
    
  • hooks/execute-tracker.shRunsGitHub
    Read the script
    #!/bin/bash
    # execute-tracker.sh - PostToolUse hook for /execute
    # Tracks tool usage during task execution for stuck detection and tracing.
    #
    # Exit codes:
    #   0 - Always (tracking only, never blocks)
    
    set -euo pipefail
    
    # Get project directory
    PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
    CLAUDE_DIR="$PROJECT_DIR/.claude"
    STATE_FILE="$CLAUDE_DIR/execute-state.json"
    TRACE_FILE="$CLAUDE_DIR/execute-trace.json"
    
    # === File Locking (C-8) — portable (works on macOS + Linux) ===
    LOCK_DIR="${STATE_FILE}.lock"
    if ! mkdir "$LOCK_DIR" 2>/dev/null; then
        echo "execute-tracker: Another process is modifying state, skipping" >&2
        exit 0  # Don't block, just skip this update
    fi
    trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT
    
    # Read input JSON from stdin
    INPUT=$(cat)
    
    # Check if we're in an active execution session
    if [ ! -f "$STATE_FILE" ]; then
        exit 0  # No active session
    fi
    
    STATUS=$(jq -r '.status // ""' "$STATE_FILE" 2>/dev/null || echo "")
    if [ "$STATUS" != "executing" ]; then
        exit 0  # Not actively executing
    fi
    
    # Extract tool info
    TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
    TOOL_RESULT=$(echo "$INPUT" | jq -r '.tool_result // ""' | head -c 500)  # Truncate for storage
    TIMESTAMP=$(date -Iseconds)
    
    # Get current task from state
    CURRENT_TASK=$(jq -r '.current_task // ""' "$STATE_FILE" 2>/dev/null || echo "")
    
    if [ -z "$CURRENT_TASK" ]; then
        exit 0  # No current task being tracked
    fi
    
    # Initialize trace file if it doesn't exist
    if [ ! -f "$TRACE_FILE" ]; then
        echo '{"spans": [], "tool_calls": []}' > "$TRACE_FILE"
    fi
    
    # Record tool call for tracing
    if ! jq --arg task "$CURRENT_TASK" \
       --arg tool "$TOOL_NAME" \
       --arg time "$TIMESTAMP" \
       --arg result "${TOOL_RESULT:0:200}" \
       '.tool_calls += [{"task_id": $task, "tool": $tool, "timestamp": $time, "result_preview": $result}]' \
       "$TRACE_FILE" > "${TRACE_FILE}.tmp" 2>&1; then
        echo "execute-tracker: WARNING - trace file update failed" >&2
    fi
    mv "${TRACE_FILE}.tmp" "$TRACE_FILE" 2>/dev/null || true
    
    # Update task stats in state file
    # Increment tool call count
    if ! jq --arg taskId "$CURRENT_TASK" \
       --arg tool "$TOOL_NAME" \
       '(.tasks[] | select(.id == $taskId)).tool_calls += 1 |
        (.tasks[] | select(.id == $taskId)).last_tool = $tool |
        (.tasks[] | select(.id == $taskId)).last_activity = now' \
       "$STATE_FILE" > "${STATE_FILE}.tmp" 2>&1; then
        echo "execute-tracker: WARNING - state file update failed" >&2
    fi
    mv "${STATE_FILE}.tmp" "$STATE_FILE" 2>/dev/null || true
    
    # --- Stuck Detection ---
    
    # Get recent tool calls for this task
    RECENT_CALLS=$(jq --arg task "$CURRENT_TASK" \
        '[.tool_calls[] | select(.task_id == $task)] | .[-20:]' \
        "$TRACE_FILE" 2>/dev/null || echo "[]")
    
    # Check for repeated same tool (potential stuck indicator)
    REPEATED_COUNT=$(echo "$RECENT_CALLS" | jq --arg tool "$TOOL_NAME" \
        '[.[] | select(.tool == $tool)] | length' 2>/dev/null || echo "0")
    
    # Check for tool flood (many calls without file changes)
    TOTAL_RECENT=$(echo "$RECENT_CALLS" | jq 'length' 2>/dev/null || echo "0")
    FILE_CHANGING_TOOLS=$(echo "$RECENT_CALLS" | jq \
        '[.[] | select(.tool == "Edit" or .tool == "Write")] | length' 2>/dev/null || echo "0")
    
    # Calculate stuck score
    STUCK_SCORE=0
    
    # Same tool repeated 5+ times in last 20 calls
    if [ "$REPEATED_COUNT" -ge 5 ]; then
        STUCK_SCORE=$((STUCK_SCORE + 40))
    fi
    
    # 15+ tool calls without file changes
    if [ "$TOTAL_RECENT" -ge 15 ] && [ "$FILE_CHANGING_TOOLS" -eq 0 ]; then
        STUCK_SCORE=$((STUCK_SCORE + 50))
    fi
    
    # Update stuck score in state
    if ! jq --arg taskId "$CURRENT_TASK" \
       --argjson score "$STUCK_SCORE" \
       '(.tasks[] | select(.id == $taskId)).stuck_score = $score' \
       "$STATE_FILE" > "${STATE_FILE}.tmp" 2>&1; then
        echo "execute-tracker: WARNING - stuck score update failed" >&2
    fi
    mv "${STATE_FILE}.tmp" "$STATE_FILE" 2>/dev/null || true
    
    # Output warning if stuck score is high (but don't block)
    if [ "$STUCK_SCORE" -ge 60 ]; then
        cat <<EOF
    {
      "hookSpecificOutput": {
        "hookEventName": "PostToolUse",
        "additionalContext": "WARNING: Task may be stuck (score: ${STUCK_SCORE}/100). ${REPEATED_COUNT} repeated ${TOOL_NAME} calls. Consider trying a different approach or asking for help."
      }
    }
    EOF
    fi
    
    exit 0
    
  • hooks/git-branch-guard.shRunsGitHub
    Read the script
    #!/bin/bash
    # git-branch-guard.sh - PreToolUse hook for /execute
    # Blocks dangerous git operations during task execution.
    #
    # Matcher: Bash (only checks bash commands)
    # Exit codes:
    #   0 - Command allowed
    #   2 - Command blocked (dangerous git operation)
    
    set -euo pipefail
    
    # Get project directory
    PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
    CLAUDE_DIR="$PROJECT_DIR/.claude"
    STATE_FILE="$CLAUDE_DIR/execute-state.json"
    
    # Read input JSON from stdin
    INPUT=$(cat)
    
    # Extract tool name
    TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // ""')
    
    # Only check Bash tool
    if [ "$TOOL_NAME" != "Bash" ]; then
        exit 0
    fi
    
    # Check if we're in an active execution session
    if [ ! -f "$STATE_FILE" ]; then
        exit 0  # No active session, allow all
    fi
    
    STATUS=$(jq -r '.status // ""' "$STATE_FILE" 2>/dev/null || echo "")
    if [ "$STATUS" != "executing" ]; then
        exit 0  # Not actively executing, allow all
    fi
    
    # Extract the command
    COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
    
    # Skip if not a git command
    if ! echo "$COMMAND" | grep -qE '^\s*git\s'; then
        exit 0
    fi
    
    # ALLOWLIST APPROACH - Only permit known-safe git operations
    # Everything else is blocked by default. This is safer than blocklisting
    # because new dangerous commands (like cherry-pick) can't slip through.
    
    # Safe git subcommands that agents may use during task execution:
    #   add        - Stage changes
    #   status     - Check working tree state
    #   diff       - View changes
    #   log        - View history
    #   show       - View commits/objects
    #   ls-files   - List tracked files
    #   check-ignore - Check gitignore rules
    #   rev-parse  - Parse revisions
    #   symbolic-ref - Read/modify symbolic refs
    #   config     - Read config (--get, --list only)
    #   commit     - Commit staged changes
    
    # Extract the git subcommand
    GIT_SUBCOMMAND=$(echo "$COMMAND" | sed -n 's/.*git\s\+\([a-z-]\+\).*/\1/p')
    
    # Allowlist of safe git subcommands
    case "$GIT_SUBCOMMAND" in
        add|status|diff|log|show|ls-files|check-ignore|rev-parse|symbolic-ref|commit)
            # These are safe - allow them
            exit 0
            ;;
        config)
            # git config is safe only for reading (--get, --list, --get-all, --get-regexp)
            if echo "$COMMAND" | grep -qE 'git\s+config\s+(--get|--list|--get-all|--get-regexp)'; then
                exit 0
            fi
            echo "BLOCKED: git config write operation during task execution" >&2
            echo "" >&2
            echo "Command: $COMMAND" >&2
            echo "" >&2
            echo "Only read operations allowed: git config --get, git config --list" >&2
            exit 2
            ;;
        *)
            # Everything else is blocked
            echo "BLOCKED: Unsafe git operation during task execution" >&2
            echo "" >&2
            echo "Command: $COMMAND" >&2
            echo "Subcommand: $GIT_SUBCOMMAND" >&2
            echo "" >&2
            echo "During claudikins-kernel:execute, only safe git operations are allowed:" >&2
            echo "  - git add, git commit (modify your work)" >&2
            echo "  - git status, git diff, git log, git show (inspect state)" >&2
            echo "  - git ls-files, git check-ignore (query files)" >&2
            echo "  - git rev-parse, git symbolic-ref (query refs)" >&2
            echo "  - git config --get/--list (read config)" >&2
            echo "" >&2
            echo "Blocked operations include: checkout, switch, reset, clean, push," >&2
            echo "pull, fetch, rebase, merge, stash, cherry-pick, revert, tag, branch -d" >&2
            echo "" >&2
            echo "If you need these operations, complete your task first." >&2
            exit 2
            ;;
    esac
    
  • hooks/merge-gate.shRunsGitHub
  • hooks/pre-task-gate.shRunsGitHub
  • hooks/preserve-state.shRunsGitHub
  • hooks/sanitize-bash.shRunsGitHub
  • hooks/session-startup.shRunsGitHub
  • hooks/ship-complete.shRunsGitHub
  • hooks/ship-init.shRunsGitHub
  • hooks/skill-activation-hook.shRunsGitHub
  • hooks/task-completion-capture.shGitHub
  • hooks/trace-end.shRunsGitHub
  • hooks/trace-start.shRunsGitHub
  • hooks/validate-plan-completion.shRunsGitHub
  • hooks/validate-plan-format.shRunsGitHub
  • hooks/verify-gate.shRunsGitHub
  • hooks/verify-init.shRunsGitHub

All 27 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 withclaudikins-kernel

SRE thinking applied to Claude Code, based on Boris Cherny's Q&A. It enforces a strict 4-stage pipeline with gates between each step. You literally cannot skip verification. You cannot ship without approval.

Get the whole plugin