Hooks

Automation triggers for events

Modified

February 25, 2026

Advanced 8 min read Phase 4: Advanced

Hooks run automatically in response to Claude Code events, enabling automation without manual invocation.

Hook Types

Hook Triggers When
Pre-command Before any bash command runs
Post-command After any bash command completes
Pre-commit Before a git commit
Post-commit After a git commit
Session start When Claude session begins
Session stop When Claude session ends

Configuration

Hooks are defined in .claude/settings.json:

{
  "hooks": {
    "pre-commit": {
      "command": "Rscript scripts/validate_pipeline_consistency.R"
    },
    "post-commit": {
      "command": "./scripts/post_commit_review.sh"
    },
    "session-stop": {
      "agent": "lab-reviewer"
    }
  }
}

Common Hook Patterns

Pre-Commit Validation

Catch issues before they’re committed:

{
  "hooks": {
    "pre-commit": {
      "command": "Rscript scripts/validate_pipeline_consistency.R",
      "description": "Check globals.yml consistency"
    }
  }
}

The validation script might check: - All globals.yml values are used correctly - No hardcoded values in manuscript - Data provenance is documented

Post-Commit Review

Automatically review after commits:

{
  "hooks": {
    "post-commit": {
      "command": "./scripts/post_commit_review.sh",
      "description": "Review changes after commit"
    }
  }
}

Session Stop Review

Run a review when ending a session:

{
  "hooks": {
    "session-stop": {
      "agent": "lab-reviewer",
      "description": "Review session changes before exit"
    }
  }
}

Pre-Slurm Checks

Validate before job submission:

{
  "hooks": {
    "pre-command": {
      "pattern": "sbatch",
      "command": "./scripts/preflight_check.sh",
      "description": "Pre-submission validation"
    }
  }
}

Writing Hook Scripts

Validation Script Example

# scripts/validate_pipeline_consistency.R

library(yaml)

# Load globals
globals <- read_yaml("config/globals.yml")

# Check R files for hardcoded values
r_files <- list.files("R/", pattern = "\\.R$", full.names = TRUE)
issues <- character()

for (file in r_files) {
  content <- readLines(file)

  # Check for hardcoded values that should be in globals
  if (any(grepl("n_iterations\\s*=\\s*\\d+", content))) {
    issues <- c(issues, paste(file, ": hardcoded n_iterations"))
  }
}

if (length(issues) > 0) {
  cat("Validation failed:\n")
  cat(paste("-", issues), sep = "\n")
  quit(status = 1)
} else {
  cat("Validation passed\n")
}

Review Script Example

#!/bin/bash
# scripts/post_commit_review.sh

SINCE_COMMIT=${1:-HEAD~1}

echo "Reviewing changes since $SINCE_COMMIT"

# Get changed files
CHANGED_FILES=$(git diff --name-only $SINCE_COMMIT HEAD)

# Check for critical file changes
CRITICAL="globals.yml _targets.R"
for file in $CRITICAL; do
  if echo "$CHANGED_FILES" | grep -q "$file"; then
    echo "WARNING: Critical file changed: $file"
  fi
done

# Log the review
echo "$(date): Reviewed $CHANGED_FILES" >> logs/code_review.log

Hook Best Practices

Keep Hooks Fast

Hooks run synchronously. Slow hooks block Claude:

{
  "hooks": {
    "pre-commit": {
      "command": "quick_check.sh",  // Good: fast
      "timeout": 10
    }
  }
}

Make Hooks Informative

Output should explain what happened:

#!/bin/bash
echo "Running pre-commit checks..."
if ./check.sh; then
  echo "✓ All checks passed"
else
  echo "✗ Checks failed - commit blocked"
  exit 1
fi

Handle Failures Gracefully

Non-zero exit blocks the action:

# Return 0 to allow, non-zero to block
if (has_issues) {
  cat("Issues found - blocking commit\n")
  quit(status = 1)  # Block
} else {
  quit(status = 0)  # Allow
}

Log Everything

Keep a record of hook executions:

LOG_FILE="logs/hooks.log"
echo "$(date): $HOOK_NAME executed" >> $LOG_FILE

Debugging Hooks

Check hook configuration

> What hooks are configured for this project?

Test hook manually

# Run the hook script directly
./scripts/post_commit_review.sh

# Check exit code
echo $?  # 0 = success, non-zero = failure

Disable temporarily

> Run this command without hooks

Or edit .claude/settings.json to comment out the hook.

Session Start Hooks

Run setup tasks or reminders when Claude starts a session. These are useful for environment checks, status displays, and context reminders.

Reminder to review CLAUDE.md:

{
  "hooks": {
    "SessionStart": [{
      "command": "echo 'Reminder: Review CLAUDE.md if project context has changed'"
    }]
  }
}

Check for uncommitted work from last session:

{
  "hooks": {
    "SessionStart": [{
      "command": "bash -c 'CHANGES=$(git status --porcelain | wc -l); if [ $CHANGES -gt 0 ]; then echo \"⚠ $CHANGES uncommitted changes from last session\"; fi'",
      "description": "Warn about uncommitted changes on session start"
    }]
  }
}

Display pipeline status on startup:

{
  "hooks": {
    "SessionStart": [{
      "command": "Rscript -e 'p <- targets::tar_progress(); cat(sprintf(\"Pipeline: %d complete, %d errored, %d dispatched\\n\", sum(p$progress==\"completed\"), sum(p$progress==\"errored\"), sum(p$progress==\"dispatched\")))'",
      "description": "Show targets pipeline status on session start"
    }]
  }
}

Check for updates to a plugin or tool:

{
  "hooks": {
    "SessionStart": [{
      "command": "node scripts/check-updates.js",
      "description": "Background check for tool updates"
    }]
  }
}
TipKeep SessionStart Hooks Fast

SessionStart hooks run before you can interact with Claude. Keep them under 2-3 seconds. For slower checks, use background processes that write results to a file Claude can read later.

Caution

Hooks with bugs can block all operations. Test thoroughly:

  1. Run hook script manually first
  2. Test with a dummy commit
  3. Have a way to disable (edit settings.json)