A note before we begin: Hooks are a power-user feature. They are entirely optional. Everything here describes capabilities we can add to our workflow, and our research works perfectly well without them. Think of hooks as the academic equivalent of setting up email filters: helpful once running, though not essential.
Skills require us to invoke them explicitly. We type /style-pass or /run-analysis and the workflow runs. But some automation should happen without asking. The syntax checker should run before every commit. Data documentation should update when we create new variables. Research logs should capture analysis decisions as we make them.
This is where hooks come in. Think of them like auto-save in a word processor. We set them up once, and they run automatically at the right moments without us having to remember. The difference between "remember to check the do-file syntax" and the syntax check running on its own is the difference between a system that depends on our memory and one that handles the routine work itself.
What Are Hooks? Automatic Helpers for Our Workflow
Hooks are automatic triggers - small helpers that watch for specific moments in our workflow and spring into action when those moments arrive. Unlike skills, which wait for us to call them, hooks run without an explicit command.
A simple analogy: imagine if every time we opened a Word document, it automatically checked for spelling errors and highlighted them. We would not have to remember to run spell check - it would just happen. Hooks work the same way for our research workflow.
Some examples of what hooks can automate:
- Running Stata syntax checks before we save changes to version control
- Checking for hardcoded file paths in analysis scripts before committing
- Loading current research project context when a session starts
- Verifying that citation links actually work when academic URLs appear in our documentation
- Documenting new variables in the data dictionary when created
- Logging analysis decisions to a research log for replication purposes
The key insight is that hooks handle the tasks our future selves will forget. They encode the "always do this" rules that slip our minds when we are focused on the actual analysis.
When Do Hooks Run? Understanding Trigger Points
Hooks need events to trigger them - specific moments when they wake up and do their job. Understanding these moments helps us design effective automation.
Before saving changes (PreCommit): This trigger fires before our code is saved to version control. This is the natural place for syntax checking, path validation, and any verification that should block problematic code from being saved. Think of it as a spell-checker that runs right before we hit "save."
Where Hook Configuration Lives
Hook configuration goes in a settings file. Like skills, hooks can be project-local or global:
- Project-local:
.claude/settings.jsonin the project folder - Global:
~/.claude/settings.jsonin our home folder
Project-local hooks apply only to that project. Global hooks apply everywhere. A syntax checker for Stata do-files makes sense as global. A hook that validates project-specific variable naming might be project-local.
Here is what the configuration looks like:
// File: .claude/settings.json (project-local)
// or: ~/.claude/settings.json (global)
{
"hooks": {
"precommit": {
"script": ".claude/hooks/pre-commit-checks.sh",
"description": "Run syntax checks and path validation before commit"
}
}
}
Let us break down what each part means:
"hooks"- This section defines our automatic helpers"precommit"- Run this before saving changes to version control"script"- The file location of the instructions to run (we will create this file)"description"- A human-readable note so we remember what this does
After files are edited (PostEdit): This trigger fires after files are modified. When Claude writes or edits a do-file or Python script, this hook can validate the changes, update the data dictionary, or log what changed.
When a session starts (SessionStart): This trigger fires when Claude Code launches. We might use this to load current research project context, check which datasets are available, or remind us of where we left off in an analysis. It is like having an assistant hand us a summary of where we left off every time we sit down to work.
When a session ends (SessionEnd): This trigger fires when a session closes. This is ideal for capturing session summaries to the research log, updating progress documentation, or syncing work to shared drives.
When specific patterns appear (PatternMatch): This trigger fires when specific content patterns appear in files or conversation. If we are documenting our analysis and a citation pattern appears, a hook could validate the reference. If we create a new variable, a hook could prompt for data dictionary documentation.
{
"hooks": {
"pattern": {
"trigger": "gen\\s+\\w+|generate\\s+\\w+|\\w+\\s*<-",
"script": ".claude/hooks/document-variable.sh",
"description": "Prompt for variable documentation when new variables created"
}
}
}
This might look intimidating, but the trigger line is just a pattern that matches Stata commands like gen newvar or Python commands like newvar = something. When the system sees us create a new variable, it runs the documentation helper.
Creating Hook Scripts: Writing Instructions for the Computer
Hook scripts are simply lists of instructions that the computer follows, step by step. They live in our project folder, typically in a subfolder called .claude/hooks/. Each script receives information about what triggered it and returns a result that determines whether the workflow continues.
Here is a script that checks for hardcoded file paths in analysis scripts. Do not worry if the syntax looks unfamiliar - the comments (lines starting with #) explain each section:
#!/bin/bash
# .claude/hooks/check-hardcoded-paths.sh
# This script checks our analysis files for paths that only work on one computer
# Step 1: Get list of files we're about to save
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(do|R|py)$')
# Step 2: If no analysis files are being saved, skip this check
if [ -z "$STAGED_FILES" ]; then
echo "No analysis scripts staged. Skipping path check."
exit 0
fi
echo "Checking for hardcoded file paths..."
# Step 3: Look for common hardcoded path patterns
VIOLATIONS=""
for file in $STAGED_FILES; do
# Check for absolute paths (Windows or Unix style)
PATHS=$(grep -nE '(C:\\|/Users/|/home/|/mnt/)' "$file" 2>/dev/null || true)
if [ -n "$PATHS" ]; then
VIOLATIONS="$VIOLATIONS\n$file:\n$PATHS"
fi
done
# Step 4: If we found problems, report them and block the save
if [ -n "$VIOLATIONS" ]; then
echo "Hardcoded paths detected. Use relative paths for portability."
echo -e "$VIOLATIONS"
echo ""
echo "Suggestion: Use project-relative paths or environment variables."
exit 1
fi
# Step 5: No problems found, allow the save to continue
echo "No hardcoded paths found."
exit 0
The script follows a simple pattern:
- Figure out what triggered this hook (which files are being saved)
- Decide if this hook is relevant (are any analysis files involved?)
- Run the actual check (look for hardcoded paths)
- Report the result (success or failure, with helpful messages)
The exit 0 at the end means "everything is fine, continue." An exit 1 would mean "something is wrong, stop." For a before-save hook, a failure stops the save from happening - which is exactly what we want when there is a problem to fix.
A more sophisticated hook might log analysis decisions to a research log:
#!/bin/bash
# .claude/hooks/log-analysis-decision.sh
# This script automatically records our analysis decisions to a research log
RESEARCH_LOG="docs/research-log.md"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M")
# The system provides information about what triggered this hook
# HOOK_EVENT tells us what type of event occurred
# HOOK_CONTEXT provides the details
if [ "$HOOK_EVENT" = "analysis_decision" ]; then
# Extract the decision and reasoning from the context
DECISION=$(echo $HOOK_CONTEXT | jq -r '.decision')
RATIONALE=$(echo $HOOK_CONTEXT | jq -r '.rationale')
# Add an entry to our research log
echo "" >> "$RESEARCH_LOG"
echo "### $TIMESTAMP" >> "$RESEARCH_LOG"
echo "**Decision:** $DECISION" >> "$RESEARCH_LOG"
echo "**Rationale:** $RATIONALE" >> "$RESEARCH_LOG"
echo "Logged decision to research log."
fi
exit 0
How Hooks Run: Blocking vs. Background
Understanding how hooks execute helps us design reliable automation.
Blocking hooks pause the workflow until they complete. Before-save hooks are blocking because we need to know if checks pass before the save proceeds. When a blocking hook fails, the triggering action stops. This is like a security checkpoint - nothing passes through until the check is done.
Background hooks run without pausing the workflow. Notification hooks are typically background because we do not want to wait for logging to complete before continuing analysis. These are like a secretary taking notes in a meeting - the work happens alongside our main activity.
We specify which behavior we want in the configuration:
{
"hooks": {
"precommit": {
"script": ".claude/hooks/stata-syntax-check.sh",
"async": false
},
"analysis_decision": {
"script": ".claude/hooks/log-decision.sh",
"async": true
}
}
}
"async": falsemeans "wait for this to finish" (blocking)"async": truemeans "run this in the background"
Running multiple hooks: When multiple hooks apply to the same trigger, they execute in order. An early failure can prevent later hooks from running. For before-save checks, we might put fast checks first so we fail quickly on obvious problems.
{
"hooks": {
"precommit": [
{
"name": "syntax",
"script": ".claude/hooks/stata-syntax.sh",
"order": 1
},
{
"name": "paths",
"script": ".claude/hooks/check-paths.sh",
"order": 2
},
{
"name": "data-docs",
"script": ".claude/hooks/check-data-docs.sh",
"order": 3
}
]
}
}
Handling errors gracefully: When hooks fail unexpectedly, we need clear error messages that help diagnose the problem. A hook that fails silently or with cryptic output creates debugging headaches.
#!/bin/bash
# Good error handling in hooks
set -e # Stop immediately if any step fails
# If something goes wrong, tell us where
trap 'echo "Hook failed at line $LINENO. Exit code: $?"' ERR
# Check that required software is installed
if ! command -v stata &> /dev/null; then
echo "Error: stata not found. Is Stata installed and in PATH?"
exit 1
fi
# Main hook logic goes here...
Use Cases
Different trigger points serve different purposes. Here are patterns that demonstrate the range of hook automation for applied economics research.
Before Saving: Stata Syntax Check
Before any save to version control, we verify do-files have valid syntax.
#!/bin/bash
# .claude/hooks/stata-syntax-check.sh
# Checks Stata do-files for common syntax problems before saving
echo "Running Stata syntax checks..."
# Get list of do-files being saved
STAGED_DO=$(git diff --cached --name-only --diff-filter=ACM | grep '\.do$')
# Skip if no do-files involved
if [ -z "$STAGED_DO" ]; then
echo "No do-files staged. Skipping syntax check."
exit 0
fi
# Check each do-file for common syntax issues
ERRORS=""
for file in $STAGED_DO; do
# Check for unclosed loops/blocks (common mistake)
OPENS=$(grep -c 'foreach\|forvalues\|while\|if\s*{' "$file" || echo 0)
CLOSES=$(grep -c '^}' "$file" || echo 0)
if [ "$OPENS" -ne "$CLOSES" ]; then
ERRORS="$ERRORS\n$file: Unbalanced braces (opens: $OPENS, closes: $CLOSES)"
fi
# Check for missing commas in option lists (another common mistake)
if grep -q 'robust cluster' "$file"; then
ERRORS="$ERRORS\n$file: Missing comma between options (robust cluster)"
fi
done
# Report any problems found
if [ -n "$ERRORS" ]; then
echo "Syntax issues detected:"
echo -e "$ERRORS"
exit 1
fi
echo "Syntax checks passed."
exit 0
Citation Verification
When documenting our analysis and academic URLs appear, we can validate that the links actually work.
#!/bin/bash
# .claude/hooks/verify-citations.sh
# Checks that citation links resolve to real pages
# This hook receives the citation text as input
CITATION=$1
# Extract DOI if present (DOIs look like: doi.org/10.1234/something)
DOI=$(echo "$CITATION" | grep -oP 'doi\.org/\K[^\s\)]+')
if [ -n "$DOI" ]; then
# Try to access the DOI - check if it resolves
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://doi.org/$DOI")
if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "302" ]; then
echo "Warning: DOI $DOI returned status $HTTP_STATUS"
echo "Please verify this citation is correct."
else
echo "DOI verified: $DOI"
fi
fi
# Check NBER working paper URLs
NBER=$(echo "$CITATION" | grep -oP 'nber\.org/papers/\K[w\d]+')
if [ -n "$NBER" ]; then
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://www.nber.org/papers/$NBER")
if [ "$HTTP_STATUS" != "200" ]; then
echo "Warning: NBER paper $NBER returned status $HTTP_STATUS"
else
echo "NBER paper verified: $NBER"
fi
fi
exit 0
Session Start: Load Research Context
When beginning a new session, we load the current state of our research project - like having a research assistant prepare a briefing for us.
#!/bin/bash
# .claude/hooks/session-start-research.sh
# Loads context about our current research project when we start working
echo "Loading research project context..."
# Find the active research project
PROJECT_FILE="docs/ACTIVE_PROJECT.md"
if [ -f "$PROJECT_FILE" ]; then
PROJECT=$(head -1 "$PROJECT_FILE" | sed 's/# //')
echo "Active project: $PROJECT"
# Show the most recent research log entry
RESEARCH_LOG="docs/research-log.md"
if [ -f "$RESEARCH_LOG" ]; then
echo ""
echo "Last research log entry:"
tail -10 "$RESEARCH_LOG"
fi
# Show when datasets were last modified
echo ""
echo "Dataset status:"
for data in data/processed/*.dta; do
if [ -f "$data" ]; then
MODIFIED=$(stat -f "%Sm" -t "%Y-%m-%d" "$data" 2>/dev/null || stat -c "%y" "$data" | cut -d' ' -f1)
echo " $(basename $data): last modified $MODIFIED"
fi
done
fi
# Remind us about any unsaved changes
UNCOMMITTED=$(git status --porcelain | wc -l | tr -d ' ')
if [ "$UNCOMMITTED" -gt 0 ]; then
echo ""
echo "Note: $UNCOMMITTED uncommitted changes in working directory"
fi
exit 0
Pattern Match: Document New Variables
When we create new variables, we should document them. This hook prompts for documentation.
#!/bin/bash
# .claude/hooks/document-variable.sh
# Prompts us to document new variables when we create them
# The hook receives the variable name and file location
VARIABLE_NAME=$1
FILE_PATH=$2
DATA_DICTIONARY="docs/data-dictionary.md"
# Check if this variable is already documented
if grep -q "| $VARIABLE_NAME |" "$DATA_DICTIONARY" 2>/dev/null; then
echo "Variable '$VARIABLE_NAME' already documented."
exit 0
fi
# Tell us that documentation is needed
echo "New variable detected: $VARIABLE_NAME"
echo "Please add to data dictionary: $DATA_DICTIONARY"
echo ""
echo "Template:"
echo "| $VARIABLE_NAME | [description] | [type] | [source] |"
# Add a placeholder entry so we remember to fill it in
echo "" >> "$DATA_DICTIONARY"
echo "| $VARIABLE_NAME | TODO: document | | Created in $FILE_PATH |" >> "$DATA_DICTIONARY"
exit 0
Session End: Update Research Log
When ending a session, we capture what analysis we performed - automatic record-keeping for replication.
#!/bin/bash
# .claude/hooks/session-end-research.sh
# Records what we did this session for future reference
RESEARCH_LOG="docs/research-log.md"
TIMESTAMP=$(date "+%Y-%m-%d %H:%M")
# Start a new log entry
echo "" >> "$RESEARCH_LOG"
echo "---" >> "$RESEARCH_LOG"
echo "## Session: $TIMESTAMP" >> "$RESEARCH_LOG"
# Log which files changed
MODIFIED=$(git diff --name-only)
if [ -n "$MODIFIED" ]; then
echo "" >> "$RESEARCH_LOG"
echo "**Files modified:**" >> "$RESEARCH_LOG"
echo "$MODIFIED" | sed 's/^/- /' >> "$RESEARCH_LOG"
fi
# Log any saves we made to version control
COMMITS=$(git log --oneline --since="4 hours ago" 2>/dev/null)
if [ -n "$COMMITS" ]; then
echo "" >> "$RESEARCH_LOG"
echo "**Commits:**" >> "$RESEARCH_LOG"
echo "$COMMITS" | sed 's/^/- /' >> "$RESEARCH_LOG"
fi
echo "Session activity logged to $RESEARCH_LOG"
exit 0
Best Practices for Reliable Hooks
Effective hooks share certain characteristics. These guidelines help us build automation that helps rather than hinders our research workflow.
Keep hooks fast. Hooks that take too long become obstacles. A before-save hook that runs the full regression suite will get disabled when we are iterating quickly on code. Run fast checks immediately; trigger slow checks in the background or defer them to later.
Explain failures clearly. When a hook blocks an action, it should explain why and what to do about it. "Syntax check failed" is less helpful than "Syntax check failed: analysis/main.do:42 - Unbalanced braces in foreach loop."
Be consistent. Running a hook twice should produce the same result as running it once. Hooks that behave differently on repeated runs cause confusion.
Do one thing well. Hooks should do one thing predictably. A before-save hook that also reformats code and updates the data dictionary is doing too much. Separate concerns into separate hooks.
Test before deploying. Before adding a hook to our workflow, test it manually with representative inputs. Debugging a broken hook during active analysis is frustrating.
# Test a hook manually by running it directly
.claude/hooks/stata-syntax-check.sh
echo "Exit code: $?"
Hooks vs. Skills: When to Use Each
Both hooks and skills automate workflows, but they serve different purposes.
Skills are like asking an assistant to do something: "Please run the analysis." We invoke them when we are ready, watch them work, and interact with the results.
Hooks are like setting up auto-save: we configure them once and they handle the routine without asking. They watch for specific moments and act automatically.
The distinction helps us decide which to use:
| Characteristic | Skill | Hook |
|---|---|---|
| How we start it | We type a command | Runs automatically |
| Visibility | We watch it work | Runs in background |
| Best for | Complex analysis tasks | Routine checks |
| Interaction | Can refine and retry | Pass/fail only |
Sometimes hooks and skills work together. A before-save hook might invoke a skill for complex validation. A session-end hook might call the full end-of-session workflow automatically.
{
"hooks": {
"session_end": {
"script": ".claude/hooks/session-exit.sh",
"invoke_skill": "session-exit"
}
}
}
This combination gives us the automatic triggering of hooks with the sophisticated workflow capabilities of skills.
Getting Started: First Steps with Hooks
Building hook automation starts with observation. What do we forget to do? What checks would catch problems earlier? What documentation should update automatically?
A practical first step: create a before-save hook that checks for hardcoded file paths in our analysis scripts. This catches portability issues before they enter version control, eliminates the frustration of running a collaborator's code that points to their personal directories, and builds the habit of letting hooks handle routine quality checks.
Once that is working, consider what else should be automatic. Syntax checks on do-files before saves. Variable documentation prompts when new variables are created. Research log updates when sessions end. Citation verification when we add references to our documentation.
Each hook we add removes one thing from our mental checklist and moves it into the system. Over time, the system handles more of the routine work, leaving us focused on the analysis decisions and interpretation that require human judgment.
Remember: hooks are entirely optional. Start with one simple hook, see if it helps, and add more only if we find them useful. There is no requirement to use any of this - it is simply another tool available when we need it.
Suggested Citation
Cholette, V. (2026, March 11). Hooks: Automation without asking. Too Early To Say. https://tooearlytosay.com/research/methodology/hooks-automation/Copy citation