Added SessionStart hook that adds $CLAUDE_PROJECT_DIR/target/release to PATH via CLAUDE_ENV_FILE. This makes `blue` available by name in all subsequent hooks. - .claude/hooks/session-start.sh: Sets PATH on session start - .claude/hooks/guard-write.sh: Now uses `blue` instead of full path - .claude/settings.json: Added SessionStart hook Requires Claude Code restart to take effect. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3.8 KiB
RFC 0051: Portable Hook Binary Resolution
Status: Draft Created: 2026-02-01 Author: Claude Opus 4.5 Related: RFC 0038, RFC 0049
Problem Statement
Claude Code PreToolUse hooks run in a minimal environment without full PATH initialization. When hooks invoke commands by name (e.g., blue guard), the shell cannot resolve the binary and hangs indefinitely.
Current Workaround
The guard hook uses a hardcoded absolute path:
/Users/ericg/letemcook/blue/target/release/blue guard --path="$FILE_PATH"
This works but is:
- Not portable: Different paths on different machines
- Fragile: Breaks if binary moves
- Not team-friendly: Each developer needs different paths
Root Cause
Hook processes don't inherit the user's shell initialization (.bashrc, .zshrc). This means:
- Custom PATH entries (like
~/.cargo/bin) are not available - Homebrew paths may be missing
- Language version managers (nvm, rbenv) don't work
This is likely intentional for security (preventing secrets/aliases in hooks).
Proposed Solution
Use $CLAUDE_PROJECT_DIR environment variable for portable binary resolution.
Option A: Project-Relative Binary (Recommended)
Update hook to use $CLAUDE_PROJECT_DIR:
#!/bin/bash
# .claude/hooks/guard-write.sh
FILE_PATH=$(jq -r '.tool_input.file_path // empty')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
# Use CLAUDE_PROJECT_DIR for portable path resolution
"$CLAUDE_PROJECT_DIR/target/release/blue" guard --path="$FILE_PATH"
Pros:
- Portable across team members
- Works with any checkout location
- Documented Claude Code pattern
Cons:
- Requires binary in project directory
- Must rebuild after checkout
Option B: SessionStart PATH Injection
Add a SessionStart hook that adds blue to PATH:
{
"hooks": {
"SessionStart": [
{
"hooks": [{
"type": "command",
"command": ".claude/hooks/setup-path.sh"
}]
}
]
}
}
#!/bin/bash
# .claude/hooks/setup-path.sh
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo "export PATH=\"$CLAUDE_PROJECT_DIR/target/release:\$PATH\"" >> "$CLAUDE_ENV_FILE"
fi
exit 0
Pros:
blueworks by name in all subsequent hooks- Cleaner hook scripts
Cons:
- Requires SessionStart hook
- More complex setup
- Session-specific (resets on restart)
Option C: Installed Binary with Explicit PATH
For teams that install blue globally:
#!/bin/bash
# Explicitly set PATH to include common install locations
export PATH="$HOME/.cargo/bin:/usr/local/bin:$PATH"
FILE_PATH=$(jq -r '.tool_input.file_path // empty')
if [ -z "$FILE_PATH" ]; then
exit 0
fi
blue guard --path="$FILE_PATH"
Pros:
- Works with installed binaries
- Standard Unix pattern
Cons:
- Must enumerate all possible install locations
- Still not fully portable
Recommendation
Option B (SessionStart PATH injection) is recommended because:
- Cleaner hook scripts - just use
blueby name - Works for any hook that needs blue
- Consistent with existing SessionStart hooks pattern
- PATH set once, used everywhere
Implementation Plan
- Update
.claude/hooks/guard-write.shto use$CLAUDE_PROJECT_DIR - Update
.claude/settings.jsonto use quoted project dir path - Test on fresh checkout
- Document in README or CONTRIBUTING.md
Migration
Before:
/Users/ericg/letemcook/blue/target/release/blue guard --path="$FILE_PATH"
After:
"$CLAUDE_PROJECT_DIR/target/release/blue" guard --path="$FILE_PATH"
Testing
- Clone repo to new location
- Build:
cargo build --release -p blue - Run Claude Code
- Attempt a write operation
- Verify guard runs without hanging
References
- Claude Code Hooks Documentation: https://code.claude.com/docs/en/hooks.md
- RFC 0038: SDLC Workflow Discipline
- RFC 0049: Synchronous Guard Command