SessionStart hooks not working for new conversations
Bug Report: SessionStart Hooks Not Working for New Conversations
Environment
- Claude Code Version: 2.0.27
- Platform: macOS (Darwin 24.6.0)
- Node.js Version: v23.11.0
- Installation Type: npm-global (via Homebrew)
Summary
SessionStart hooks execute but their output is never processed or injected into Claude's context when starting new conversations. The hooks work correctly for /clear, /compact, and URL resume operations, but fail silently for brand new sessions.
Expected Behavior
When a user starts a new Claude Code conversation, SessionStart hooks should:
- Execute successfully
- Have their stdout parsed for
hookSpecificOutput.additionalContext - Create
hook_additional_contextattachments - Inject that context into Claude's conversation
According to the documentation, "For UserPromptSubmit and SessionStart, stdout is added as context."
Actual Behavior
- SessionStart hooks do execute (verified by file logging)
- Hook stdout is never processed by the
qz()function - The
hook_additional_contextattachments are never created - Context is not injected into new conversations
- No error messages are shown to the user
Root Cause Analysis
Code Investigation
The qz() function in cli.js is responsible for:
- Executing SessionStart hooks
- Collecting their stdout
- Parsing JSON output for
hookSpecificOutput.additionalContext - Creating
hook_additional_contextattachments - Returning those attachments for injection into the conversation
Problem: qz() is only called in 3 places in the entire codebase:
- Line 1798:
await qz("compact")- during/compactcommand ✅ - Line 3723:
await qz("startup")- during URL resume (--resumewith URL) ✅ - Line 3726:
await qz("startup")- during/clearcommand ✅
For brand new interactive conversations, the wm6() function (line 3717) only replays old hook responses from message history:
let H=FZ2(G),w=[],N=!1;
for(let g of H)if(g.type==="system"&&g.subtype==="hook_response"&&g.hook_event==="SessionStart")w.push(g);
It never calls qz("startup") for new sessions, so SessionStart hooks execute but their output is discarded.
Verification Steps Taken
- Confirmed hooks execute: Added logging to SessionStart hook - timestamps written to
/tmp/claude-session-start.logproved hooks ran - Confirmed no output injection: Created test hook outputting secret word - Claude had no knowledge of it in new sessions
- Added debug logging: Instrumented
qz(),Gf2(), and hook processing functions - confirmedqz()never called for new sessions - Source code review: Examined
cli.js- found only 3 call sites forqz(), none for new interactive sessions
Reproduction Steps
1. Create a test SessionStart hook
#!/bin/bash
cat <<'EOF'
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "The secret word is \"bamboozle\"."
}
}
EOF
exit 0
2. Configure in .claude/settings.local.json
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "/absolute/path/to/test_hook.sh"
}
]
}
]
}
}
3. Start a brand new conversation
claude
4. Ask Claude
> what is the secret word?
Expected: Claude responds "bamboozle"
Actual: Claude has no knowledge of any secret word
5. Verify hook executed
cat /tmp/hook-executed.log # If hook logs timestamps
Hook timestamps confirm execution, but context was not injected.
The Fix
Add qz("startup") call inside the wm6 function's async helper where hooks are processed:
File: /opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js
Location: Line ~3717, inside the _=async()=>{} function
Original code:
_=async()=>{
if(V=!0,!N){
N=!0;
for(let c of w)E.enqueue(c)
}
// ... rest of function
Fixed code:
_=async()=>{
if(V=!0,!N){
N=!0;
if(w.length===0&&H.length===0){
let s=await qz("startup");
w.push(...s);
}
for(let c of w)E.enqueue(c)
}
// ... rest of function
Explanation: This checks if there are no hooks from history (w.length===0) and no messages (H.length===0), indicating a new session, then calls qz("startup") to process SessionStart hooks before enqueuing them.
Verification After Fix
After applying the fix:
- Started new conversation
- SessionStart hook output appeared:
````
SessionStart:startup hook succeeded: The secret word is "bamboozle".
- Asked Claude for secret word
- Claude correctly responded with "bamboozle"
Impact
This bug affects all users who configure SessionStart hooks for:
- Injecting project-specific rules/context
- Setting up session-specific configuration
- Loading workspace information
Workaround: Users must manually run /clear at the start of each session to trigger SessionStart hooks, which is not intuitive.
Additional Notes
- PreToolUse and PostToolUse hooks work correctly
- UserPromptSubmit hooks work correctly
- The UI correctly hides
hook_additional_contextattachments (line 1818 incli.js) - The bug only affects the interactive CLI mode, not
--printmode (which callsqz("startup")at line 3726)
Files for Reference
- Main implementation:
cli.jsline 3717 (wm6function) - Hook processing:
cli.jsline ~1798 (qzfunction) - Context extraction:
cli.js(Gf2function) - UI rendering:
cli.jsline 1818 (hides hook_additional_context)
Proposed Solution
Apply the fix shown above, or alternatively refactor to ensure qz("startup") is called for all new sessions, not just URL resumes and command-triggered operations.
23 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Not a duplicate:
Confirmed this issue. Setup:
Result: On fresh session start (via /new command), no hook output appears in context. Hook appears to execute silently or output is discarded.
Workaround attempted: None successful without manually running the script.
Config:
This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.
how is this still open ... this is in your docs...
+1 it doesn't work
+1 same here
Title: SessionStart hooks don't fire reliably when opening new VSCode window with multiple windows already open
Description:
When multiple VSCode windows are running Claude Code and I open a new window, plugins that rely on SessionStart hooks fail to load properly. Commands appear in the slash menu but return "Unknown skill" when invoked.
Other plugins (beads, pr-review-toolkit) that register skills statically work fine
~/.claude/plugins/cache/temp_git_* directories accumulate with .orphaned_at markers (225+ in my case)
Running /clear fixes it by re-triggering SessionStart
Reinstalling the plugin also fixes it temporarily
Affected plugin: superpowers (but likely affects any plugin using SessionStart hooks)
Confirmed same issue. Claude Code led me here in troubleshooting it.
I can also confirm that this is broken for me. (Startup hook is ignored.)
Also just tried to create a session startup hook. Same problem. Please fix.
Facing the same issue.
Also experiencing this. As a workaround, I tried moving context injection to
UserPromptSubmithooks instead ofSessionStart, but that has its own issues - theadditionalContextfromUserPromptSubmithooks also doesn't get injected into Claude's context (related to #12151).So currently there's no reliable way to inject dynamic context at either:
This makes it impossible to build automation that feeds external information into Claude conversations. Would really appreciate a fix for the hook → context injection pipeline.
Facing the same issue
Wonderful, another broken configuration. I'm here after trying to write a hook to work around user permissions being ignored to copy user permissions to project permissions. OF COURSE that doesn't work either. Why host a github repo for something if you're going to ignore fundamental issues people are filing?
Reporting empirical findings from Claude Code VSCode extension (March 2026):
startupmatcher is now firing correctly in VSCode — both on Reload and on/newcommand.Observed behavior (session-start-context-load log, 2026-03-03):
/newcommand: single fire observed (1 data point; pattern vs Reload needs further observation)This suggests the root cause described in this issue has been resolved in recent versions. Environment: Windows 11, VSCode extension, Claude Code v2.1.6x.
This was working for me until very recently and now seems to have stopped.
Opus is already a bit flakey about following instruction reliably so these hooks are the only way to inject a bit of determinism.
Sad state of affairs.
Experiencing this on v2.1.19 (Linux).
/clearThe intermittent nature is puzzling - no identified variable changed between working and non-working state.
Also experiencing this intermittently on macOS (CLI, not VSCode). I built Tacklebox, a hook server for session coordination and context persistence, and ran into this early on. My workaround is a dual-path injection: SessionStart builds and returns context normally, but for startup sessions we deliberately skip setting an internal context_injected flag. This lets UserPromptSubmit on the first prompt act as a fallback, if SessionStart was silently discarded, UserPromptSubmit catches it and injects the same context. Either way, context is delivered reliably.
Noting that TanakaSeiji's findings above match what we've seen: resume, compact, and clear sources work consistently; it's only startup that's unreliable. The behavior seems environment-dependent and can come and go between versions without any config changes on our side.
When Claude refers to "the issue" it means this issue.
This is annoying, still happening, please fix.
Cloud-session confirmation, 2026-08-24 (Claude Code on the Web, Ubuntu 24.04 cloud environment) — this still reproduces roughly ten months on, on a different surface from the original macOS CLI report.
Across five cloud sessions, including ones where the
SessionStarthook demonstrably ran and did substantial work (installed a toolchain, hydrated 187 Git LFS objects, initialized a submodule), no hook output was delivered to the session's context. The only trace is one debug line:The hook's stdout is neither shown to the agent nor preserved in the log beyond that line — the text itself is discarded. We only learned what the hook had done by having it
teeits own output to a file and reading that file back.One data point that may narrow the analysis in the original report: that debug line proves something inspected the hook's stdout at session start and decided it was not JSON. So an inspection path does run for a brand-new cloud conversation, even though
qz("startup")reportedly is not called for new sessions. Whether thehookSpecificOutput.additionalContextbranch would fire if the output were JSON is what we cannot determine from outside the binary — we have just switched our hook to emit JSON and will report back either way.Context: we hit this while diagnosing a separate cloud-session bug, #89215 — this issue is what kept that one invisible for four test rounds.
Practical impact: a repository cannot use a
SessionStarthook to tell the session anything — including that environment setup failed. The session discovers the failure minutes later, at its first red build, with no pointer to the cause. Preserving plain-text hook output in the debug log, even if it is not injected as context, would have saved us several diagnostic rounds.Follow-up to my comment above — I said we had switched our
SessionStarthook to emit JSON and would report back either way. Result, from a brand-new Claude Code on the Web cloud session (2026-08-24):hookSpecificOutput.additionalContextIS injected on a brand-new session on this surface. The hook's summary arrived as a system message before the session's first tool call. To keep the evidence uncontaminated, the session was asked to answer from context alone before inspecting any file or log; it quoted:The same hook, on the same surface, with plain-text output produced nothing in context across five earlier sessions — only the
Hook output does not start with {, treating as plain textdebug line. So the difference is purely JSON vs. plain text, not whether the hook ran or what it did.What this does and doesn't say about this issue:
additionalContextprocessing path does run for a new (non-/clear, non-/compact, non-resume) conversation, at least on Claude Code on the Web at this build. That is contrary to theqz("startup")analysis in the original report, so either the cloud surface takes a different path, or the new-session case has been fixed since October and the remaining gap is narrower than the title suggests.The part that still bites, and may be the more durable half of this report: plain-text hook stdout is silently discarded — not shown to the session, not preserved in the debug log beyond that one line. A hook that prints a human-readable failure gets no signal to anyone. Emitting JSON is the workaround, but nothing in the failure mode points a developer toward it.
Independent confirmation of the intermittent "hook never invoked for brand-new sessions" behavior, via direct session-transcript comparison (Claude Code v2.1.259, VSCode native extension host, Windows 11 Pro 10.0.26200), project-level
.claude/settings.jsonwith a singleSessionStarthook (nomatcher).Comparing recently-created session
.jsonltranscripts under~/.claude/projects/<project>/:type:"attachment"record withattachment.type:"hook_success"(hookName:"SessionStart:startup", containing the hook's raw stdout) immediately followed byattachment.type:"hook_additional_context"— and the injected<system-reminder>is visible in the very next turn's context.queue-operation/session-init lines to the first realuser/attachment:environmententries — nohook_successorhook_additional_contextrecord anywhere, not even a failure/error record. Not a rendering gap — the hook was simply never invoked for that session.Both session types originated the same way (opening the extension panel and sending a first message), within the same ~20-minute window, same settings file, same hook script — so this isn't a stale-config or caching issue on my end; it's nondeterministic per-session, consistent with
qz("startup")only being reached via/clear//compact/URL-resume and not on the plain new-conversation path described in this issue.Workaround in use: running
/clearimmediately after opening a new session reliably triggers the hook (matches the root-cause note that/cleardoes callqz("startup")).