Claude Code Hooks System and Migration Guide: Workflow Automation and Smooth Upgrades
Claude Code 2.0 Core Features Deep Dive Series
This is Part 3 (Final) of the series. The complete series includes:
- Skills and Sandbox - Enhancing Extensibility and Security
- Subagents and Plan Mode - The New Era of Intelligent Collaboration
- Hooks System and Migration Guide - Workflow Automation and Smooth Upgrades (This Article)
Claude Code 2.0.30 introduces the powerful Prompt-based Stop Hooks feature, along with important migration requirements: Output Styles deprecation and SDK updates. This article provides an in-depth exploration of the Hooks system’s complete applications and detailed migration guides to ensure smooth upgrades to the latest version.
Hooks System Updates
Prompt-based Stop Hooks Overview
Stop Hooks are hook functions executed when Claude prepares to end a conversation, capable of checking if work is complete, validating state, and even preventing Claude from stopping to request continued work.
Operating Flow
graph TD
A[Claude Completes Task] --> B[Preparing to Stop]
B --> C{Has Stop Hook?}
C -->|No| D[Normal Exit]
C -->|Yes| E[Execute Stop Hook]
E --> F[Hook Script Checks]
F --> G{Check Result}
G -->|Exit Code 0| H[Allow Stop]
G -->|Exit Code 2| I[Block Stop]
G -->|Other Exit Code| J[Non-blocking Warning]
I --> K[Hook Returns Message to Claude]
K --> L[Claude Continues Work]
L --> B
H --> D
J --> M[Show Warning but Allow Stop]
M --> D
style A fill:#e1f5fe
style I fill:#ff6b6b
style H fill:#c8e6c9
Configuring Stop Hooks
Basic settings.json Configuration
{
"hooks": {
"Stop": [
{
"matcher": "", // Empty string matches all Stop events
"hooks": [
{
"type": "command",
"command": "/path/to/stop_hook.sh"
}
]
}
]
}
}
Exit Code Behavior Specifications
| Exit Code | Behavior | Description | Use Case |
|---|---|---|---|
| 0 | Success, allow stop | Hook check passed, Claude can end | All checks passed |
| 2 | Block stop | Hook found issues, request Claude continue | Found incomplete tasks |
| Other | Non-blocking error | Show warning but allow stop | Hook itself errored |
JSON Response Format
Hooks can return JSON format for more control:
{
"continue": false, // false = block stop, true = allow stop
"stopReason": "Reason description", // Why blocking (when continue=false)
"suppressOutput": false, // Hide stdout output?
"systemMessage": "Message to Claude" // Additional system message
}
JSON vs Exit Code
# Method 1: Using Exit Code
#!/bin/bash
if [ check failed ]; then
echo "Issue found: todos not complete" >&2
exit 2 # Block stop
fi
exit 0 # Allow stop
# Method 2: Using JSON (more flexible)
#!/bin/bash
if [ check failed ]; then
echo '{
"continue": false,
"stopReason": "Todos not complete",
"suppressOutput": true,
"systemMessage": "Please complete all todos before ending"
}'
exit 0
fi
echo '{"continue": true}'
exit 0
Practical Application Cases
Case 1: Todo List Completion Check
Ensure Claude completes all todos before ending.
stop_hook.sh:
#!/bin/bash
# Check if running inside stop hook (prevent infinite loop)
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
echo '{"continue": true}' # Already in hook, allow directly
exit 0
fi
# Check todo list
TODO_FILE=".claude/todos.json"
if [ ! -f "$TODO_FILE" ]; then
# No todo list, allow stop
echo '{"continue": true}'
exit 0
fi
# Parse todo list using jq
PENDING_COUNT=$(jq '[.todos[] | select(.status == "pending" or .status == "in_progress")] | length' "$TODO_FILE")
if [ "$PENDING_COUNT" -gt 0 ]; then
# Has incomplete todos, block stop
PENDING_ITEMS=$(jq -r '.todos[] | select(.status == "pending" or .status == "in_progress") | "- [\(.status)] \(.content)"' "$TODO_FILE")
cat <<EOF
{
"continue": false,
"stopReason": "Found ${PENDING_COUNT} incomplete todos",
"suppressOutput": false,
"systemMessage": "Please complete the following todos:\n${PENDING_ITEMS}\n\nFinish these before ending."
}
EOF
exit 0
fi
# All todos complete, allow stop
echo '{"continue": true}'
exit 0
settings.json:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "/Users/yourname/.claude/hooks/stop_hook.sh"
}
]
}
]
}
}
Usage Effect:
User: Help me implement login feature
Claude: [Creates todo list]
1. [ ] Create login API endpoint
2. [ ] Implement password verification
3. [ ] Add JWT token generation
4. [ ] Write tests
Claude: [Completes items 1, 2]
Claude: Task complete!
Stop Hook: ❌ Block stop
Message: Found 2 incomplete todos:
- [pending] Add JWT token generation
- [pending] Write tests
Please complete these before ending.
Claude: Sorry, I need to complete remaining todos...
[Continues to complete JWT and tests]
Case 2: Test Verification Hook
Ensure all tests pass before ending.
test_verification_hook.sh:
#!/bin/bash
# Prevent infinite loop
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
echo '{"continue": true}'
exit 0
fi
# Check for package.json (Node.js project)
if [ -f "package.json" ]; then
# Run tests
npm test > /tmp/test_output.txt 2>&1
TEST_EXIT_CODE=$?
if [ $TEST_EXIT_CODE -ne 0 ]; then
# Tests failed, block stop
TEST_OUTPUT=$(cat /tmp/test_output.txt | tail -n 20)
cat <<EOF
{
"continue": false,
"stopReason": "Tests failed",
"systemMessage": "Tests did not pass, please fix these issues:\n\n\`\`\`\n${TEST_OUTPUT}\n\`\`\`"
}
EOF
exit 0
fi
fi
# Check for pytest (Python project)
if [ -f "pytest.ini" ] || [ -f "setup.py" ]; then
pytest --tb=short > /tmp/test_output.txt 2>&1
TEST_EXIT_CODE=$?
if [ $TEST_EXIT_CODE -ne 0 ]; then
TEST_OUTPUT=$(cat /tmp/test_output.txt | tail -n 20)
cat <<EOF
{
"continue": false,
"stopReason": "Tests failed",
"systemMessage": "Python tests did not pass:\n\n\`\`\`\n${TEST_OUTPUT}\n\`\`\`"
}
EOF
exit 0
fi
fi
# Tests passed or no tests, allow stop
echo '{"continue": true}'
exit 0
Case 3: Code Quality Check Hook
Ensure code meets linting standards.
lint_check_hook.sh:
#!/bin/bash
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
echo '{"continue": true}'
exit 0
fi
# Check for eslint
if [ -f ".eslintrc.js" ] || [ -f ".eslintrc.json" ]; then
npx eslint . --max-warnings 0 > /tmp/lint_output.txt 2>&1
LINT_EXIT_CODE=$?
if [ $LINT_EXIT_CODE -ne 0 ]; then
LINT_OUTPUT=$(cat /tmp/lint_output.txt)
cat <<EOF
{
"continue": false,
"stopReason": "Linting failed",
"systemMessage": "Code doesn't meet linting standards:\n\n\`\`\`\n${LINT_OUTPUT}\n\`\`\`\n\nPlease fix these issues."
}
EOF
exit 0
fi
fi
# Check TypeScript compilation
if [ -f "tsconfig.json" ]; then
npx tsc --noEmit > /tmp/tsc_output.txt 2>&1
TSC_EXIT_CODE=$?
if [ $TSC_EXIT_CODE -ne 0 ]; then
TSC_OUTPUT=$(cat /tmp/tsc_output.txt | head -n 30)
cat <<EOF
{
"continue": false,
"stopReason": "TypeScript compilation errors",
"systemMessage": "Found type errors:\n\n\`\`\`\n${TSC_OUTPUT}\n\`\`\`"
}
EOF
exit 0
fi
fi
echo '{"continue": true}'
exit 0
Preventing Infinite Loops
Problem Scenario
Claude completes work → Stop Hook finds issue → Requests Claude continue
→ Claude attempts fix → Stops again → Stop Hook finds issue again
→ Requests Claude continue → ... (infinite loop)
Solutions
Method 1: Use stop_hook_active Environment Variable
#!/bin/bash
# Check at hook start
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
# Already in hook execution, avoid recursion
echo '{"continue": true}'
exit 0
fi
# Normal check logic...
Method 2: Check Conversation History
#!/bin/bash
# Check if recently triggered this hook
HOOK_LOG=".claude/hook_history.log"
CURRENT_TIME=$(date +%s)
if [ -f "$HOOK_LOG" ]; then
LAST_TRIGGER=$(tail -n 1 "$HOOK_LOG")
TIME_DIFF=$((CURRENT_TIME - LAST_TRIGGER))
# If triggered within 30 seconds, avoid repeat
if [ $TIME_DIFF -lt 30 ]; then
echo '{"continue": true, "systemMessage": "Preventing hook loop trigger"}'
exit 0
fi
fi
# Record this trigger
echo "$CURRENT_TIME" >> "$HOOK_LOG"
# Normal check logic...
Method 3: Limit Trigger Count
#!/bin/bash
COUNTER_FILE=".claude/stop_hook_counter.txt"
# Read counter
if [ -f "$COUNTER_FILE" ]; then
COUNTER=$(cat "$COUNTER_FILE")
else
COUNTER=0
fi
# Increment counter
COUNTER=$((COUNTER + 1))
echo "$COUNTER" > "$COUNTER_FILE"
# Force allow stop after 3 times
if [ $COUNTER -gt 3 ]; then
echo '{"continue": true, "systemMessage": "Hook triggered 3 times, forcing allow stop"}'
rm "$COUNTER_FILE" # Reset counter
exit 0
fi
# Normal check logic...
Output Styles Migration Guide
Deprecation Reason and Impact
Starting from v2.0.30, Output Styles feature is officially deprecated.
Official Statement
Output styles are now deprecated. Review options in
/output-styleand use--system-prompt-file,--system-prompt,--append-system-prompt, CLAUDE.md, or plugins instead.
Impact Scope
- Not Affected: CLAUDE.md, plugins, command-line parameters
- Affected: Users using
/output-stylecommand or.claude/output-styles/directory - Recommended Action: Migrate as soon as possible before removal in future versions
Migration Paths
graph TD
A[Output Styles Users] --> B{Use Case}
B -->|Simple Prompt Adjustments| C[Migrate to CLAUDE.md]
B -->|Complex Behavior Definition| D[Migrate to Plugin System]
B -->|Temporary Experiments| E[Use CLI Parameters]
C --> F[Edit .claude/CLAUDE.md]
D --> G[Create Plugin]
E --> H[--append-system-prompt]
F --> I[Migration Complete]
G --> I
H --> I
style A fill:#e1f5fe
style I fill:#c8e6c9
Migration Steps
Step 1: Review Current Configuration
# Run this command to view current output style
/output-style
Example output:
Current Output Style: explanatory
Content:
---
name: Explanatory
description: Provides educational insights while working
---
# Explanatory Mode
When performing tasks, provide "Insights" sections that explain:
- Why certain approaches were chosen
- Key concepts involved
- Best practices being followed
...
Step 2: Choose Migration Target
Option A: Migrate to CLAUDE.md (Recommended)
Suitable for:
- Project-specific behavior adjustments
- Team-shared standards
- Long-term stable configurations
Operation:
# Create or edit CLAUDE.md
vim .claude/CLAUDE.md
Add Output Style content:
# CLAUDE.md
## Code Style Guide
When performing tasks, follow these principles:
### Explanatory Output
When executing important operations, provide "Insight" sections explaining:
- Why specific approaches were chosen
- Key concepts involved
- Best practices followed
### Example
\```typescript
// ✅ Insight: Using dependency injection improves testability
class UserService {
constructor(private db: Database) {}
// ...
}
\```
## Code Review Focus
When reviewing code, check:
1. Type safety
2. Error handling
3. Performance considerations
4. Security issues
Option B: Migrate to Plugin System
Suitable for:
- Complex behavior definitions
- Version control needs
- Cross-project sharing
Create Plugin Structure:
mkdir -p my-code-style-plugin
cd my-code-style-plugin
plugin.json:
{
"name": "my-code-style",
"version": "1.0.0",
"description": "My custom code style and behavior",
"systemPrompt": "system-prompt.md"
}
system-prompt.md:
# Custom Code Style
When writing code, follow these guidelines:
## Code Quality
- Use TypeScript strict mode
- Implement comprehensive error handling
- Write descriptive variable names
## Documentation
- Add JSDoc comments for public APIs
- Include usage examples
- Document edge cases
## Testing
- Write tests for all new features
- Cover edge cases
- Use descriptive test names
Install Plugin:
# Local install
claude plugin install ./my-code-style-plugin
# Or push to Git and install
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/yourname/my-code-style-plugin.git
git push -u origin main
claude plugin install git@github.com:yourname/my-code-style-plugin.git
Option C: Use CLI Parameters (Temporary Use)
Suitable for:
- One-time experiments
- Quick testing
- No persistence needed
# Using file
claude --system-prompt-file custom-prompt.md
# Direct prompt specification
claude --append-system-prompt "Focus on performance optimization"
# Complete override of system prompt (not recommended)
claude --system-prompt "You are a code reviewer..."
Alternative Solution Comparison
| Solution | Advantages | Disadvantages | Use Cases |
|---|---|---|---|
| CLAUDE.md | • Simple and direct • Built into project • Git version control |
• Each project needs separate config • Not easy to share across projects |
Project-specific standards |
| Plugin | • Reusable • Version management • Easy to share |
• More complex setup • Needs extra maintenance |
Team-shared standards |
| –system-prompt-file | • Flexible • Temporary override |
• Manual specification each time • Not persistent |
Experiments and testing |
| –append-system-prompt | • Quick • Doesn’t override existing |
• Long CLI parameters | Quick adjustments |
| –system-prompt | • Complete control | • Loses default behavior • Not recommended |
Special requirements |
Migration Decision Tree
graph TD
A[Output Style Migration] --> B{Need Cross-Project Sharing?}
B -->|Yes| C{Need Version Control?}
B -->|No| D[CLAUDE.md]
C -->|Yes| E[Plugin System]
C -->|No| F[Global CLAUDE.md]
D --> G[Create .claude/CLAUDE.md in project root]
E --> H[Create Plugin and publish to Git]
F --> I[Create global config in ~/.claude/CLAUDE.md]
style A fill:#e1f5fe
style D fill:#c8e6c9
style E fill:#fff3e0
SDK Updates and Backward Compatibility
SDK Renaming
Changes
# Old (deprecated)
@anthropic-ai/claude-code
# New
@anthropic-ai/claude-agent-sdk
Migration Steps
1. Update package.json:
{
"dependencies": {
// Remove old
// "@anthropic-ai/claude-code": "^1.0.0",
// Add new
"@anthropic-ai/claude-agent-sdk": "^1.0.0"
}
}
2. Update import statements:
// Old
import { Agent } from '@anthropic-ai/claude-code';
// New
import { Agent } from '@anthropic-ai/claude-agent-sdk';
3. Install and test:
# Remove old
npm uninstall @anthropic-ai/claude-code
# Install new
npm install @anthropic-ai/claude-agent-sdk
# Run tests
npm test
Behavior Changes
1. System Prompt No Longer Default
Old Behavior:
const agent = new Agent({
apiKey: process.env.ANTHROPIC_API_KEY
});
// Automatically includes Claude Code's system prompt
New Behavior:
const agent = new Agent({
apiKey: process.env.ANTHROPIC_API_KEY
// No longer automatically includes system prompt
});
// To get Claude Code behavior, need explicit specification
const agent = new Agent({
apiKey: process.env.ANTHROPIC_API_KEY,
systemPrompt: customSystemPrompt // Needs explicit provision
});
2. Settings File Reading Changes
Old Behavior:
// Automatically reads settings from ~/.claude/settings.json
const agent = new Agent({ apiKey: '...' });
New Behavior:
// Need explicit settings source specification
import { loadSettings } from '@anthropic-ai/claude-agent-sdk';
const settings = loadSettings(); // Manual loading
const agent = new Agent({
apiKey: process.env.ANTHROPIC_API_KEY,
settings: settings
});
3. Required Code Adjustments
Migration Example:
// --- Old Code ---
import { Agent } from '@anthropic-ai/claude-code';
const agent = new Agent({
apiKey: process.env.ANTHROPIC_API_KEY
});
await agent.run('Create a new feature');
// --- New Code ---
import { Agent, loadSettings } from '@anthropic-ai/claude-agent-sdk';
// Load settings
const settings = loadSettings({
settingsPath: '.claude/settings.json' // Optional
});
// Create agent
const agent = new Agent({
apiKey: process.env.ANTHROPIC_API_KEY,
settings: settings,
// For Claude Code system prompt behavior
systemPromptFile: '.claude/system-prompt.md',
// Or provide directly
systemPrompt: `
You are a helpful coding assistant...
`
});
await agent.run('Create a new feature');
Other Backward Compatibility Issues
Ripgrep Configuration Removal
Impact:
- Removed custom ripgrep configuration support
- Resolves issues with no search results and config discovery failures
Action:
- Remove any
.ripgreprcor custom configurations - Use Claude Code’s default search settings
VSCode Keyboard Shortcut Changes (Windows)
v2.0.31 Changes:
| Function | Old Shortcut | New Shortcut |
|---|---|---|
| Mode Switch | Alt + M |
Shift + Tab |
Reason:
Alt + Mconflicts with some system shortcutsShift + Tabmore consistent with native installation behavior
Fixed Bugs
v2.0.28:
- ✅ Fixed Explore agent creating unnecessary .md investigation files
- ✅ Fixed
/contextfailing with “max_tokens must be greater than thinking.budget_tokens” error
v2.0.30:
- ✅ Fixed
/compactfailing due toprompt_too_long - ✅ Fixed plugin uninstall not removing plugins
v2.0.31:
- ✅ Fixed VSCode respectGitIgnore configuration issues
Complete Migration Checklist
Output Styles Migration
- Run
/output-styleto view current configuration - Decide migration target (CLAUDE.md / Plugin / CLI)
- Create new configuration
- Test new configuration works correctly
- Remove old output styles files
- Update team documentation
SDK Updates
- Update package.json dependencies
- Update import statements
- Add explicit system prompt (if needed)
- Add settings loading logic
- Run complete test suite
- Update CI/CD configuration
- Update deployment scripts
Configuration File Checks
- Remove
.ripgreprccustom configuration - Check settings.json compatibility
- Confirm Hooks configuration correct
- Verify Sandbox settings
- Check MCP server configuration
Testing Verification
- Local development environment testing
- CI/CD pipeline testing
- Team member validation
- Documentation updates
- Version control commit
Series Summary
Through three in-depth articles, we’ve comprehensively covered Claude Code 2.0’s core features:
Part 1 Review: Skills and Sandbox
- Skills: Model-driven intelligent extension system
- Sandbox: OS-level security isolation
- Integration: Secure and powerful development environment
Part 2 Review: Subagents and Plan Mode
- Plan Subagent: Planning-focused read-only assistant
- Interactive Questions: Interactive requirement clarification
- Subagent Enhancements: Resumption, dynamic models, tool blocking
Part 3 Focus: Hooks and Migration
- Stop Hooks: Automated workflow validation
- Output Styles Migration: Smooth upgrade path
- SDK Updates: Backward compatibility handling
Practice Recommendations
- Start Small: Try new features in a single project first
- Gradual Migration: Migrate features one by one by priority
- Team Communication: Ensure team members understand changes
- Document Updates: Keep documentation synced with configuration
- Continuous Learning: Follow official updates and best practices
Series Navigation
← Previous: Subagents and Plan Mode - The New Era of Intelligent Collaboration
View complete series: Claude Code 2.0 Core Features Deep Dive
Thank you for reading this series! We hope these articles help you better utilize Claude Code 2.0’s powerful features. Feel free to share questions or suggestions in the comments.