Project 1: Hook Hello World - Session Greeter

Project 1: Hook Hello World - Session Greeter


Project Overview

Attribute Value
Project Number 1 of 40
Category Hooks System Mastery
Main Programming Language Bash
Alternative Languages Python, JavaScript (Bun), Go
Difficulty Level Level 1: Beginner
Time Estimate 2-4 hours
Coolness Level Level 2: Practical but Forgettable
Business Potential Resume Gold
Knowledge Area Hooks / Event-Driven Architecture
Primary Tool Claude Code Hooks System
Main Reference “Wicked Cool Shell Scripts” by Dave Taylor

Summary: Build a SessionStart hook that greets you with the current date, weather, and a motivational quote when you start a Claude session. This is your introduction to the Claude Code hooks system.


Real World Outcome

When you start any Claude Code session, you will see a personalized greeting:

$ claude

+--------------------------------------------------+
|                  SESSION GREETER                  |
+--------------------------------------------------+

  Good morning, Douglas!

  Today: Sunday, December 22, 2025
  Weather: San Francisco - 58 F, Partly Cloudy

  "The only way to do great work is to love what you do."
                                        - Steve Jobs

+--------------------------------------------------+

Starting Claude Code session...

This hook runs every time Claude starts, giving you contextual awareness before diving into work. It demonstrates that you can inject custom behavior at precise points in Claude’s lifecycle.

What This Teaches You

  • How hooks are configured in settings.json
  • The SessionStart event and when it fires
  • Reading JSON from stdin in shell scripts
  • Exit codes and their meaning in hook context
  • Making external API calls from hooks

The Core Question You’re Answering

“How do I make Claude Code do something automatically when specific events happen?”

Before you write any code, understand this fundamental concept:

Hooks are the deterministic backbone of Claude Code automation. Unlike tool use (which Claude decides when to invoke), hooks fire predictably on specific events. This predictability is what makes them powerful for:

  • Security enforcement (blocking dangerous operations)
  • Workflow automation (auto-formatting, notifications)
  • State management (session persistence)
  • Developer experience (greetings, context enrichment)
                    HOOK vs TOOL INVOCATION

  +-----------------+              +-----------------+
  |    HOOKS        |              |    TOOLS        |
  +-----------------+              +-----------------+
  | Deterministic   |              | Non-deterministic|
  | Event-triggered |              | Model-decided   |
  | Your code runs  |              | Claude decides  |
  | Always fires    |              | May or may not  |
  +-----------------+              +-----------------+
          |                                |
          v                                v
  "SessionStart"     vs      "Claude, read this file"
  ALWAYS runs your                 Claude chooses
  script when session              whether to use
  begins                           Read tool

Concepts You Must Understand First

Stop and research these before coding:

1. Hook Event Types

Claude Code has 10+ hook events. For this project, focus on SessionStart:

Event When It Fires Use Case
SessionStart When Claude session begins Initialize environment, greet user
UserPromptSubmit Before prompt reaches Claude Validate/transform input
PreToolUse Before any tool executes Block dangerous operations
PostToolUse After tool completes Format, log, notify
Stop When session ends Cleanup, save state
Notification Claude needs attention Alert user
PreCompact Before context compression Save important state
SubagentStop Subagent completes Collect results

Reference: Claude Code Docs - “Hooks” section

2. Hook Configuration Schema

Hooks are configured in settings.json with this structure:

{
  "hooks": [
    {
      "event": "SessionStart",
      "type": "command",
      "command": "/path/to/your/script.sh",
      "timeout": 10000
    }
  ]
}
Field Required Description
event Yes Which lifecycle event triggers this hook
type Yes “command” (shell) or “prompt” (Haiku LLM)
command Yes* Path to script (for type: “command”)
timeout No Max execution time in ms (default: varies by event)

Where hooks live:

  • ~/.claude/settings.json - Personal hooks (all projects)
  • .claude/settings.json - Project-specific hooks (team-shared)
  • .claude/settings.local.json - Project-specific, not in git

Reference: Claude Code Docs - “Hooks Configuration”

3. Exit Codes and Control Flow

Exit codes are how hooks communicate with Claude:

  +-------------+     +-------------+     +-------------+
  |  Exit 0     |     |  Exit 2     |     | Other Exit  |
  +-------------+     +-------------+     +-------------+
  | SUCCESS     |     | BLOCK       |     | ERROR       |
  | Allow action|     | Deny action |     | Log warning |
  | Continue    |     | Stop action |     | Continue    |
  +-------------+     +-------------+     +-------------+

For SessionStart:

  • Exit 0: Session starts normally
  • Exit 2: Session is aborted (useful for environment checks)
  • Other: Warning logged, session continues

Reference: “The Linux Command Line” Ch. 24 by William Shotts


Questions to Guide Your Design

Before implementing, think through these design decisions:

1. Event Selection

  • Which event fires the greeting? SessionStart is the only choice here
  • Should this block Claude if it fails? Probably not - a failed greeting shouldn’t prevent work
  • What happens if the weather API is down? Your hook should handle this gracefully

2. Data Fetching

  • How will you get weather data?
    • Option A: curl -s "wttr.in/YourCity?format=3" (simple, no API key)
    • Option B: OpenWeatherMap API (more reliable, needs key)
    • Option C: Local weather cache (fastest, may be stale)
  • How will you get a motivational quote?
    • Option A: fortune command (if installed)
    • Option B: Hardcoded array of quotes
    • Option C: Quote API (adds latency)
  • Should fetching happen synchronously or async?
    • Sync: Simpler, but delays session start
    • Async: Faster start, but output may appear late

3. Configuration

  • Should the greeting be customizable? Consider user preferences
  • How will you handle different timezones? System time vs explicit config
  • Should users be able to disable it? Environment variable toggle?

Thinking Exercise

Trace the Hook Execution

Before coding, trace what happens when you run claude:

  USER TERMINAL                      CLAUDE CODE INTERNALS
  +--------------+                   +------------------------+
  |              |                   |                        |
  | $ claude     |                   |  1. Parse CLI args     |
  |              | ----------------> |                        |
  |              |                   |  2. Load settings.json |
  |              |                   |                        |
  |              |                   |  3. Find SessionStart  |
  |              |                   |     hooks              |
  |              |                   |                        |
  |              |                   |  4. For each hook:     |
  |              |                   |     a. Spawn shell     |
  |              |                   |     b. Pipe JSON stdin |
  |              |     <-------------|     c. Wait (timeout)  |
  | [Greeting    |                   |     d. Check exit code |
  |  appears]    |                   |                        |
  |              |                   |  5. Start REPL         |
  |              |                   |                        |
  +--------------+                   +------------------------+

JSON Payload on stdin:

{
  "hook_event_name": "SessionStart",
  "session_id": "abc123-def456-ghi789",
  "cwd": "/Users/douglas/projects/myapp",
  "timestamp": "2025-12-22T10:30:00Z"
}

Questions While Tracing:

  1. What data is available in the stdin JSON?
    • session_id, cwd (current working directory), timestamp
  2. What happens if your script takes too long?
    • Hook times out (default ~10s), warning logged, session continues
  3. Can you output to the terminal from a hook?
    • Yes! stdout goes to terminal, stderr goes to logs

The Interview Questions They’ll Ask

Prepare to answer these questions about hooks:

1. “How would you automate a task that needs to run every time a developer starts their AI coding assistant?”

Answer: Use a SessionStart hook. Configure it in settings.json with the event type “SessionStart”, point it to a shell script, and ensure it exits with code 0 for normal operation. The script receives session metadata via stdin as JSON.

2. “Explain the difference between a hook and a tool in Claude Code.”

Answer:

  • Hooks are deterministic, event-driven scripts that run at specific lifecycle points. YOU control when they fire.
  • Tools are capabilities Claude can invoke (Read, Write, Bash). CLAUDE decides when to use them.
  • Hooks run your code; tools run Claude’s built-in capabilities.

3. “What’s the security implication of exit code 2 in hooks?”

Answer: Exit code 2 is a hard block. For PreToolUse hooks, it prevents the tool from executing entirely. This creates a deterministic security boundary that even the user cannot override without modifying the hook configuration. It’s useful for protecting sensitive files or preventing dangerous operations.

4. “How would you debug a hook that’s not firing?”

Answer:

  1. Check file is executable: chmod +x script.sh
  2. Validate settings.json is valid JSON: jq . settings.json
  3. Verify event name is exact: “SessionStart” not “sessionStart”
  4. Check hook timeout hasn’t passed
  5. Run script manually: echo '{}' | ./script.sh
  6. Check stderr output in Claude logs

5. “Can hooks modify Claude’s behavior, or only perform side effects?”

Answer: Mostly side effects, but some hooks can modify behavior:

  • UserPromptSubmit: Can modify the prompt Claude sees
  • PreToolUse: Can block tool execution
  • SessionStart: Can set environment variables via CLAUDE_ENV_FILE Most hooks (PostToolUse, Stop, Notification) are for side effects only.

Hints in Layers

Use these hints progressively when stuck:

Hint 1: Starting Point

Create a file at ~/.claude/hooks/session-greeter.sh and make it executable:

mkdir -p ~/.claude/hooks
touch ~/.claude/hooks/session-greeter.sh
chmod +x ~/.claude/hooks/session-greeter.sh

Hint 2: Configuration

Add a hook entry to ~/.claude/settings.json:

{
  "hooks": [
    {
      "event": "SessionStart",
      "type": "command",
      "command": "~/.claude/hooks/session-greeter.sh"
    }
  ]
}

Hint 3: Script Structure

Your script should follow this pattern:

#!/bin/bash

# 1. Read stdin (even if you don't use it - prevent broken pipe)
cat > /dev/null

# 2. Print greeting to stdout (appears in terminal)
echo "Hello, $(whoami)!"

# 3. Exit with code 0 (success - allow session to continue)
exit 0

Hint 4: Debugging

If the hook doesn’t fire:

  1. Test manually: echo '{"session_id": "test"}' | ~/.claude/hooks/session-greeter.sh
  2. Check Claude logs: ~/.claude/logs/ for errors
  3. Verify settings.json path matches your actual file location
  4. Try absolute path instead of ~ expansion

Books That Will Help

Topic Book Chapter Why It Helps
Shell scripting basics “The Linux Command Line” by William Shotts Ch. 24 Understanding exit codes, scripts, stdin/stdout
JSON processing in bash “jq Manual” Filters section Parsing the JSON payload hooks receive
Event-driven patterns “Designing Event-Driven Systems” by Ben Stopford Ch. 1 Understanding event-driven architecture concepts
Cool shell scripts “Wicked Cool Shell Scripts” by Dave Taylor Ch. 2-4 Practical bash scripting patterns
Clean code principles “Clean Code” by Robert C. Martin Ch. 2-3 Writing readable, maintainable scripts

Implementation Guide

Step 1: Create the Hook Script

#!/bin/bash
# ~/.claude/hooks/session-greeter.sh
# SessionStart hook that displays a personalized greeting

# Consume stdin to prevent broken pipe errors
cat > /dev/null

# Get current user
USER_NAME=$(whoami)

# Get time of day for appropriate greeting
HOUR=$(date +%H)
if [ $HOUR -lt 12 ]; then
    GREETING="Good morning"
elif [ $HOUR -lt 17 ]; then
    GREETING="Good afternoon"
else
    GREETING="Good evening"
fi

# Get current date
DATE_STR=$(date "+%A, %B %d, %Y")

# Get weather (with timeout to prevent slow starts)
# Uses wttr.in - a free weather service that returns plain text
CITY="${CLAUDE_GREETER_CITY:-San Francisco}"
WEATHER=$(timeout 3 curl -s "wttr.in/${CITY}?format=%l:+%t,+%C" 2>/dev/null || echo "Weather unavailable")

# Array of motivational quotes
QUOTES=(
    "The only way to do great work is to love what you do.|Steve Jobs"
    "Code is like humor. When you have to explain it, it is bad.|Cory House"
    "First, solve the problem. Then, write the code.|John Johnson"
    "The best error message is the one that never shows up.|Thomas Fuchs"
    "Simplicity is the soul of efficiency.|Austin Freeman"
    "Make it work, make it right, make it fast.|Kent Beck"
)

# Pick a random quote
QUOTE_INDEX=$((RANDOM % ${#QUOTES[@]}))
QUOTE_LINE="${QUOTES[$QUOTE_INDEX]}"
QUOTE_TEXT=$(echo "$QUOTE_LINE" | cut -d'|' -f1)
QUOTE_AUTHOR=$(echo "$QUOTE_LINE" | cut -d'|' -f2)

# Print the greeting
echo ""
echo "+--------------------------------------------------+"
echo "|                  SESSION GREETER                  |"
echo "+--------------------------------------------------+"
echo ""
echo "  $GREETING, $USER_NAME!"
echo ""
echo "  Today: $DATE_STR"
echo "  Weather: $WEATHER"
echo ""
echo "  \"$QUOTE_TEXT\""
echo "                                        - $QUOTE_AUTHOR"
echo ""
echo "+--------------------------------------------------+"
echo ""

# Exit 0 to allow session to continue
exit 0

Step 2: Configure settings.json

{
  "hooks": [
    {
      "event": "SessionStart",
      "type": "command",
      "command": "~/.claude/hooks/session-greeter.sh",
      "timeout": 5000
    }
  ]
}

Step 3: Make It Configurable

Create ~/.claude/greeter-config.json:

{
  "city": "San Francisco",
  "show_weather": true,
  "show_quote": true,
  "custom_greeting": null
}

Enhanced script that reads config:

#!/bin/bash
# Read config if it exists
CONFIG_FILE="$HOME/.claude/greeter-config.json"
if [ -f "$CONFIG_FILE" ]; then
    CITY=$(jq -r '.city // "San Francisco"' "$CONFIG_FILE")
    SHOW_WEATHER=$(jq -r '.show_weather // true' "$CONFIG_FILE")
    SHOW_QUOTE=$(jq -r '.show_quote // true' "$CONFIG_FILE")
else
    CITY="San Francisco"
    SHOW_WEATHER=true
    SHOW_QUOTE=true
fi

# ... rest of script uses these variables

Architecture Diagram

+-----------------------------------------------------------------------+
|                        HOOK EXECUTION FLOW                             |
+-----------------------------------------------------------------------+
|                                                                        |
|  +-------------------+                                                 |
|  | $ claude          |                                                 |
|  +--------+----------+                                                 |
|           |                                                            |
|           v                                                            |
|  +-------------------+                                                 |
|  | Claude Code Init  |                                                 |
|  +--------+----------+                                                 |
|           |                                                            |
|           v                                                            |
|  +-------------------+    +------------------+                         |
|  | Load settings.json|--->| hooks: [         |                         |
|  +--------+----------+    |   {              |                         |
|           |               |     event:       |                         |
|           |               |     "SessionStart"|                        |
|           |               |     command: ... |                         |
|           |               |   }              |                         |
|           |               | ]                |                         |
|           v               +------------------+                         |
|  +-------------------+                                                 |
|  | Fire SessionStart |                                                 |
|  +--------+----------+                                                 |
|           |                                                            |
|           |  spawn process                                             |
|           v                                                            |
|  +---------------------------------------+                             |
|  |  session-greeter.sh                   |                             |
|  |  +----------------------------------+ |                             |
|  |  | stdin: {"session_id": "...",    | |                             |
|  |  |         "cwd": "/path/..."}     | |                             |
|  |  +----------------------------------+ |                             |
|  |                                       |                             |
|  |  1. Read stdin (consume)              |                             |
|  |  2. Fetch weather (curl wttr.in)      |                             |
|  |  3. Select random quote               |                             |
|  |  4. Print greeting to stdout          |                             |
|  |  5. exit 0                            |                             |
|  +---------------------------------------+                             |
|           |                                                            |
|           | exit code 0                                                |
|           v                                                            |
|  +-------------------+                                                 |
|  | Session Continues |                                                 |
|  +-------------------+                                                 |
|                                                                        |
+-----------------------------------------------------------------------+

Learning Milestones

Track your progress through these checkpoints:

Milestone 1: Hook Fires on Session Start

Goal: See any output when starting Claude

Test: Run claude and verify your greeting appears

What You’ve Learned:

  • Hook configuration in settings.json
  • Event binding to SessionStart
  • Basic shell script structure

Milestone 2: Weather Displays Correctly

Goal: Dynamic weather from wttr.in appears

Test: Weather should match your location

What You’ve Learned:

  • Making HTTP calls from hooks
  • Handling API timeouts gracefully
  • String formatting in bash

Milestone 3: Greeting is Customizable

Goal: Change city or greeting without editing script

Test: Set CLAUDE_GREETER_CITY=London and verify

What You’ve Learned:

  • Environment variable integration
  • External configuration files
  • Separation of config from code

Common Mistakes to Avoid

Mistake 1: Forgetting to Consume stdin

# WRONG - may cause broken pipe
#!/bin/bash
echo "Hello!"
exit 0

# RIGHT - always consume stdin
#!/bin/bash
cat > /dev/null  # Consume stdin
echo "Hello!"
exit 0

Mistake 2: Not Making Script Executable

# After creating the script:
chmod +x ~/.claude/hooks/session-greeter.sh

Mistake 3: Using Exit Code 2 Accidentally

# WRONG - this blocks the session!
if [ -z "$WEATHER" ]; then
    exit 2  # Oops! Session won't start
fi

# RIGHT - exit 0 even on partial failure
if [ -z "$WEATHER" ]; then
    WEATHER="unavailable"
fi
exit 0

Mistake 4: Slow API Calls Without Timeout

# WRONG - could hang forever
curl -s "https://slow-api.example.com/weather"

# RIGHT - use timeout
timeout 3 curl -s "https://api.example.com/weather" || echo "fallback"

Extension Ideas

Once you have the basic greeter working, try these enhancements:

  1. Project-Aware Greeting: Read the current directory and show relevant project info
  2. Git Status Integration: Show branch name and pending changes
  3. Calendar Integration: Show upcoming meetings or deadlines
  4. System Health: Display CPU/memory/disk status
  5. Custom Themes: Allow different visual styles for the greeting

Summary

This project introduced you to the Claude Code hooks system through the simplest possible example: a SessionStart hook that displays a greeting. You learned:

  • Hook Events: When and why different events fire
  • Configuration: How to set up hooks in settings.json
  • Exit Codes: How hooks communicate success/failure
  • Shell Scripting: Reading stdin, making API calls, outputting to terminal

With this foundation, you’re ready to tackle more complex hooks like PreToolUse (Project 2) and PostToolUse (Project 3).