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.
$ npx -y skills add povvo/claudikins-kernel --agent claude-codeShips 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 - Matches
claudikins-kernel:verify${CLAUDE_PLUGIN_ROOT}/hooks/verify-init.sh - Matches
claudikins-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 - Matches
claudikins-kernel:execute${CLAUDE_PLUGIN_ROOT}/hooks/validate-plan-format.sh - Matches
claudikins-kernel:execute.*(--status|status)${CLAUDE_PLUGIN_ROOT}/hooks/execute-status.sh
SubagentStart
- Matches
*${CLAUDE_PLUGIN_ROOT}/hooks/trace-start.sh - Matches
babyclaude${CLAUDE_PLUGIN_ROOT}/hooks/create-task-branch.sh
SubagentStop
- Matches
*${CLAUDE_PLUGIN_ROOT}/hooks/trace-end.sh
PreToolUse
- Matches
Bash${CLAUDE_PLUGIN_ROOT}/hooks/git-branch-guard.sh${CLAUDE_PLUGIN_ROOT}/hooks/sanitize-bash.sh${CLAUDE_PLUGIN_ROOT}/hooks/merge-gate.sh - Matches
Task${CLAUDE_PLUGIN_ROOT}/hooks/pre-task-gate.sh
PostToolUse
- Matches
Edit|Write${CLAUDE_PLUGIN_ROOT}/hooks/autoformat.sh - Matches
*${CLAUDE_PLUGIN_ROOT}/hooks/execute-tracker.sh
Stop
- Matches
claudikins-kernel:verify${CLAUDE_PLUGIN_ROOT}/hooks/verify-gate.sh - Matches
claudikins-kernel:ship${CLAUDE_PLUGIN_ROOT}/hooks/ship-complete.sh - Matches
claudikins-kernel:outline${CLAUDE_PLUGIN_ROOT}/hooks/validate-plan-completion.sh - Matches
claudikins-kernel:execute${CLAUDE_PLUGIN_ROOT}/hooks/batch-checkpoint-gate.sh
PreCompact
- Matches
*${CLAUDE_PLUGIN_ROOT}/hooks/preserve-state.sh
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.
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.

