-
-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Fix SessionStart hook hangs on Claude Code startup #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
## Problem
The session-start.sh hook was causing Claude Code to hang on startup,
displaying '⏺ Plugin hook error:' and preventing the superpowers context
from being injected. This manifested in two ways:
1. **sed/awk JSON escaping hang**: The complex sed/awk pipeline for JSON
escaping (~5KB of text) would hang indefinitely on some systems.
2. **grep command hang**: The grep commands used to extract status flags
would mysteriously hang when run within command substitution during
Claude's hook execution environment, even though they ran fine when
executed directly in a shell.
## Environment Details
This bug was triggered on:
- macOS 15.6 (Darwin 24.6.0)
- Mac Mini M2 Pro
- Claude Code v2.0.14
- Bash 3.2.57
- Home directory on external SSD (/Volumes/T9/)
The hang occurred specifically during Claude's hook execution context,
where stdin/stdout/stderr handling differs from normal shell execution.
## Root Causes
1. **sed/awk is unreliable for JSON escaping**: The pipeline
`sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}'`
is fragile and can hang with large inputs or certain content patterns.
2. **grep in command substitution hangs**: When grep is used directly in
command substitution like `$(echo "$var" | grep pattern)`, it can
hang in Claude's hook execution environment, possibly due to pipe
buffering or process group handling.
## Solution
1. **Use jq for JSON escaping**: Replace sed/awk with `jq -Rs .` which is:
- More reliable (dedicated JSON tool)
- Faster (~10x performance improvement)
- Handles all edge cases correctly
2. **Wrap grep in timeout subshells**: Isolate grep commands in timeout-
protected bash subshells: `timeout 1 bash -c "echo '$var' | grep pattern"`
This prevents hangs while maintaining functionality.
3. **Add timeout protection**: Wrap all external commands (initialize-skills,
find-skills, cat) in timeout to prevent any future hangs.
## Debugging Process
This bug was tracked down using the superpowers systematic debugging approach:
- Added diagnostic logging to identify exact hang location
- Tested components in isolation vs. hook environment
- Discovered environment-specific grep behavior
- Iteratively added timeout protection until root cause was found
The skills/debugging/systematic-debugging workflow helped avoid jumping to
solutions and ensured we found the actual root cause rather than just
symptoms.
## Testing
Tested on the environment where the bug originally occurred:
- Hook completes in <3 seconds (was hanging indefinitely)
- No error messages on startup
- Full skills list successfully injected (~11KB of context)
- Debug log shows 'Errors: 0'
Fixes obra/superpowers issue with SessionStart hook timeouts.
WalkthroughIntroduces timeout-wrapped calls for initialization, status parsing, and skill discovery; recalculates SKILLS_UPDATED/SKILLS_BEHIND; assembles a consolidated JSON context (including using-skills, paths, find-skills output); injects it as additionalContext; conditionally appends a status_message when behind; and ends with an explicit exit 0. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant U as session-start.sh
participant Init as init command
participant FS as find-skills
participant JQ as jq (escape)
participant Out as STDOUT
rect rgba(200,230,255,0.3)
note over U: Initialization with timeout (10s)
U->>Init: run init (timeout 10s)
Init-->>U: init_output
note over U: Extract status flags (timeout 1s)
U->>U: parse SKILLS_UPDATED / SKILLS_BEHIND
end
alt SKILLS_BEHIND
U->>U: compose status_message (behind)
else not behind
U->>U: status_message empty
end
rect rgba(220,255,220,0.3)
note over U: Discover skills (timeout 5s)
U->>FS: run find-skills
FS-->>U: skills list / data
end
rect rgba(255,240,200,0.3)
U->>U: Assemble EXTREMELY_IMPORTANT JSON context<br/>- using-skills<br/>- tool paths<br/>- skill repo path<br/>- find-skills output<br/>- optional status_message
U->>JQ: escape for JSON
JQ-->>U: escaped context
U->>Out: write JSON with additionalContext
U-->>Out: exit 0
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~55 minutes Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (1)
hooks/session-start.sh (1)
15-15: Drop the unusedskills_updatedassignment.Shellcheck is right:
skills_updatedis written but never read, so it’s dead state you can delete (or actually use) to keep the hook lean.
Based on static analysis
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
hooks/session-start.sh(1 hunks)
🧰 Additional context used
🪛 Shellcheck (0.11.0)
hooks/session-start.sh
[warning] 15-15: skills_updated appears unused. Verify use (or export if used externally).
(SC2034)
| skills_updated=$(timeout 1 bash -c "echo '$init_output' | grep 'SKILLS_UPDATED=true'" 2>/dev/null || echo "") | ||
| skills_behind=$(timeout 1 bash -c "echo '$init_output' | grep 'SKILLS_BEHIND=true'" 2>/dev/null || echo "") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix unsafe quoting when scanning init_output.
Embedding $init_output inside bash -c "echo '...'" breaks as soon as the init script emits a single quote (or other shell metacharacters), producing a syntax error and silently disabling the updated/behind detection. Use grep directly on the buffered output instead of re-invoking a shell.
Apply this diff:
-skills_updated=$(timeout 1 bash -c "echo '$init_output' | grep 'SKILLS_UPDATED=true'" 2>/dev/null || echo "")
-skills_behind=$(timeout 1 bash -c "echo '$init_output' | grep 'SKILLS_BEHIND=true'" 2>/dev/null || echo "")
+skills_updated=$(timeout 1 grep -F 'SKILLS_UPDATED=true' <<<"$init_output" || true)
+skills_behind=$(timeout 1 grep -F 'SKILLS_BEHIND=true' <<<"$init_output" || true)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| skills_updated=$(timeout 1 bash -c "echo '$init_output' | grep 'SKILLS_UPDATED=true'" 2>/dev/null || echo "") | |
| skills_behind=$(timeout 1 bash -c "echo '$init_output' | grep 'SKILLS_BEHIND=true'" 2>/dev/null || echo "") | |
| -skills_updated=$(timeout 1 bash -c "echo '$init_output' | grep 'SKILLS_UPDATED=true'" 2>/dev/null || echo "") | |
| skills_updated=$(timeout 1 grep -F 'SKILLS_UPDATED=true' <<<"$init_output" || true) | |
| skills_behind=$(timeout 1 grep -F 'SKILLS_BEHIND=true' <<<"$init_output" || true) |
🧰 Tools
🪛 Shellcheck (0.11.0)
[warning] 15-15: skills_updated appears unused. Verify use (or export if used externally).
(SC2034)
|
Thanks so much for these PRs. I won't get to prod until tomorrow, though I'd love reports from other users. Between the two PRs, which one do I want? |
|
I ended up running with PR #9 |
Problem
The session-start.sh hook was causing Claude Code to hang on startup, displaying "⏺ Plugin hook error:" and preventing the superpowers context from being injected.
Symptoms
/doctorreports context/memory issuesRoot Causes
sed/awk JSON escaping hang: The complex sed/awk pipeline used for JSON escaping would hang when processing ~5KB of text on some systems
grep command hang: The
grepcommands for extracting status flags would mysteriously hang when executed within command substitution in Claude's hook execution environment, even though they ran fine in a normal shellEnvironment Where Bug Was Triggered
/Volumes/T9/)The hang occurred specifically during Claude's hook execution context, where stdin/stdout/stderr handling differs from normal shell execution. This is likely why the issue didn't manifest in manual testing but only during Claude startup.
Solution
1. Replace sed/awk with jq for JSON escaping
Before:
escaped=$(echo "$text" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | awk '{printf "%s\\n", $0}')After:
escaped=$(echo "$text" | jq -Rs .)Benefits:
2. Wrap grep in timeout-protected subshells
Before:
skills_behind=$(echo "$init_output" | grep "SKILLS_BEHIND=true" || echo "")After:
skills_behind=$(timeout 1 bash -c "echo '$init_output' | grep 'SKILLS_BEHIND=true'" 2>/dev/null || echo "")This isolates the grep command and prevents hangs due to pipe buffering or process group issues in Claude's execution environment.
3. Add timeout protection to all external commands
How Superpowers Helped Debug This
This bug was tracked down using the skills/debugging/systematic-debugging approach:
The systematic debugging workflow helped avoid jumping to solutions and ensured we found the actual root cause rather than just treating symptoms.
Testing
Verified on the environment where the bug originally occurred:
✅ Hook completes in <3 seconds (was hanging indefinitely)
✅ No error messages on startup
✅ Full skills list successfully injected (~11KB of context)
✅ Debug log shows
Errors: 0✅
/doctorno longer reports issuesAdditional Notes
This fix maintains backward compatibility while adding robustness. Systems that weren't experiencing the hang will see improved performance from the jq optimization.
The timeout values are conservative and should work across different system speeds and network conditions for the git fetch operations in initialize-skills.
Summary by CodeRabbit
New Features
Bug Fixes
Refactor