Advanced Claude Code Hooks: Controlling Sub-Agent Behavior
Claude Code hooks are powerful automation tools that execute at specific points during AI coding sessions. While basic hooks can validate prompts or add context, advanced hooks can enforce sophisticated rules like preventing parallel sub-agents from running test suites that share database connections.
Understanding Claude Code Hooks
Hooks in Claude Code are automated scripts that intercept and control the AI's tool usage. They execute arbitrary shell commands at specific lifecycle events, enabling you to:
- Validate tool usage before execution (PreToolUse)
- Add context to user prompts (UserPromptSubmit)
- Clean up resources when sessions end (SessionEnd)
- Inject environment data at session start (SessionStart)
- Control permissions for file operations
The most powerful hook type is PreToolUse, which runs before any tool executes and can approve, deny, or request user confirmation for the operation.
The Problem: Parallel Execution and Shared Resources
Claude Code's sub-agent system enables parallel task execution. Multiple agents can work simultaneously on different aspects of your codebase. This is excellent for productivity, but creates challenges when those tasks share resources.
Consider a PHP project with PHPUnit tests that use a SQLite database. The test suite isn't optimized for parallel execution because:
- Database locks: SQLite allows only one writer at a time
- Shared state: Tests may create or modify the same fixtures
- Race conditions: Parallel execution causes unpredictable failures
When Claude spawns multiple sub-agents to handle complex refactoring tasks, each might independently decide to run the test suite. The result? Database lock conflicts, failed tests, and confused AI agents.
The Solution: Sub-Agent Detection and Control
We can solve this by creating a hook that detects when it's running in a sub-agent context and blocks test execution, while still allowing other QA tools like static analysis and code style checks.
The key insight is that sub-agents run as child processes of the main claude process. By examining the parent process ID (PPID), we can determine whether we're in the main session or a sub-agent.
Implementation: The PreToolUse Hook
Here's a complete Python hook that implements sub-agent detection and selective command blocking:
#!/usr/bin/env python3
"""
PreToolUse hook to prevent subagents from running tests.
Subagents can run allCS and allStatic, but NOT unit tests, PHPUnit, or Infection.
Tests cannot run in parallel due to database lock conflicts.
Detection: Subagents have the main 'claude' process as their parent (PPID).
"""
import json
import os
import re
import subprocess
import sys
def is_subagent() -> bool:
"""Check if running in subagent context by examining PPID."""
try:
ppid = os.getppid()
# Get parent process command name
result = subprocess.run(
['ps', '-o', 'comm=', '-p', str(ppid)],
capture_output=True,
text=True,
timeout=2
)
parent_cmd = result.stdout.strip()
return parent_cmd == 'claude'
except Exception:
# If we can't determine, assume not subagent (fail open)
return False
def is_test_command(command: str) -> bool:
"""Check if command is a test execution (not allowed in subagents)."""
test_patterns = [
r'\bphpunit\b',
r'\bbin/qa\s+.*-t\s+unit\b',
r'\bbin/qa\s+.*--type\s+unit\b',
r'\binfection\b',
r'\bvendor/bin/phpunit\b',
r'\bphp\s+vendor/bin/phpunit\b',
r'\.\/bin\/qa\s+.*-t\s+unit\b',
]
return any(re.search(pattern, command, re.IGNORECASE) for pattern in test_patterns)
def is_allowed_qa_command(command: str) -> bool:
"""Check if command is an allowed QA command (allCS or allStatic)."""
allowed_patterns = [
r'\bbin/qa\s+.*-t\s+allCs\b',
r'\bbin/qa\s+.*--type\s+allCs\b',
r'\bbin/qa\s+.*-t\s+allStatic\b',
r'\bbin/qa\s+.*--type\s+allStatic\b',
r'\.\/bin\/qa\s+.*-t\s+allCs\b',
r'\.\/bin\/qa\s+.*-t\s+allStatic\b',
]
return any(re.search(pattern, command, re.IGNORECASE) for pattern in allowed_patterns)
def main() -> int:
"""Main hook logic."""
try:
# Read hook payload from stdin
payload = json.loads(sys.stdin.read())
# Only check Bash tool invocations
if payload.get('tool') != 'Bash':
return 0
# Check if we're in a subagent
if not is_subagent():
return 0 # Not a subagent, allow all commands
# Get the command being executed
command = payload.get('parameters', {}).get('command', '')
# Allow QA commands that are explicitly allowed
if is_allowed_qa_command(command):
return 0
# Block test commands in subagents
if is_test_command(command):
error_msg = {
'error': 'Test execution blocked in subagent context',
'reason': 'Tests cannot run in parallel due to database lock conflicts',
'command': command,
'allowed': 'Subagents can run: bin/qa -t allCs, bin/qa -t allStatic',
'blocked': 'Blocked commands: phpunit, bin/qa -t unit, infection'
}
print(json.dumps(error_msg), file=sys.stderr)
return 1 # Block the command
return 0 # Allow all other commands
except Exception as e:
# Log error but don't block (fail open for safety)
print(f"Hook error: {e}", file=sys.stderr)
return 0
if __name__ == '__main__':
sys.exit(main())
How It Works
1. Sub-Agent Detection
The is_subagent() function uses process inspection to determine context:
- Gets the parent process ID using
os.getppid() - Queries the parent's command name using
ps - Returns
Trueif the parent is theclaudeprocess - Fails open (returns
False) on errors to avoid blocking legitimate operations
2. Command Pattern Matching
The hook uses regex patterns to categorize commands:
- Test commands: PHPUnit, Infection,
bin/qa -t unit - Allowed QA commands:
bin/qa -t allCs,bin/qa -t allStatic - All other commands: Allowed without restriction
3. Selective Blocking
The hook implements a whitelist/blacklist strategy:
- Main agent: All commands allowed
- Sub-agents: Static analysis allowed, tests blocked
- Error response: Structured JSON explaining the block
Configuration
To enable this hook, add it to your Claude Code settings file (~/.claude/settings.json or .claude/settings.json):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/path/to/prevent-subagent-tests.py",
"timeout": 2000
}
]
}
]
}
}
Make the script executable:
chmod +x prevent-subagent-tests.py
Real-World Benefits
Prevents Database Lock Conflicts
By blocking parallel test execution, you eliminate SQLite database lock errors that would otherwise cause test failures and confuse the AI agents.
Enables Parallel Static Analysis
Sub-agents can still run code style checks (allCs) and static analysis (allStatic) in parallel, since these tools don't share resources.
Clear Error Messages
When a sub-agent attempts to run tests, it receives a structured JSON response explaining why the operation was blocked and what commands are allowed.
Fail-Safe Design
The hook uses a "fail open" strategy. If it can't determine whether it's in a sub-agent, it allows the command. This prevents blocking legitimate operations due to hook errors.
Extending the Pattern
This technique applies to any shared resource scenario:
- Database migrations: Prevent parallel schema changes
- File system operations: Block concurrent writes to lock files
- External services: Rate-limit API calls across sub-agents
- Build artifacts: Prevent simultaneous builds that share directories
The core pattern remains the same: detect sub-agent context via PPID, match command patterns, and selectively allow or block operations based on resource constraints.
Best Practices
Use Specific Patterns
Make your regex patterns as specific as possible to avoid false positives. Use word boundaries (\b) and full command paths when appropriate.
Fail Open for Safety
When error handling, prefer allowing the operation over blocking it. A blocked legitimate operation is more frustrating than a rare race condition.
Provide Clear Feedback
Structure your error messages as JSON with fields explaining what was blocked, why, and what alternatives are available.
Test Both Contexts
Verify your hook works correctly in both main agent and sub-agent contexts. Use echo $$ and ps commands to understand the process hierarchy.
Keep Hooks Fast
Hooks execute on every tool use. Keep them lightweight. This implementation completes in milliseconds.
Conclusion
Claude Code hooks unlock powerful automation capabilities beyond simple validation. By leveraging process inspection and pattern matching, you can enforce sophisticated execution policies that adapt to context. This allows parallel execution where safe, and prevents it where resources are shared.
This sub-agent control pattern transforms a potential source of race conditions and lock conflicts into a well-orchestrated parallel execution system. The main agent coordinates test execution, while sub-agents handle static analysis in parallel, maximizing productivity without sacrificing reliability.
Whether you're managing database locks, preventing concurrent migrations, or rate-limiting external API calls, this pattern provides a robust foundation for resource-aware parallel execution control.