SessionStart hooks not working for new conversations

Status Open
Maintainer reply None cached
Activity 23 comments · opened Oct 26, 2025

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:

  1. Execute successfully
  2. Have their stdout parsed for hookSpecificOutput.additionalContext
  3. Create hook_additional_context attachments
  4. 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_context attachments 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:

  1. Executing SessionStart hooks
  2. Collecting their stdout
  3. Parsing JSON output for hookSpecificOutput.additionalContext
  4. Creating hook_additional_context attachments
  5. Returning those attachments for injection into the conversation

Problem: qz() is only called in 3 places in the entire codebase:

  1. Line 1798: await qz("compact") - during /compact command ✅
  2. Line 3723: await qz("startup") - during URL resume (--resume with URL) ✅
  3. Line 3726: await qz("startup") - during /clear command ✅

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

  1. Confirmed hooks execute: Added logging to SessionStart hook - timestamps written to /tmp/claude-session-start.log proved hooks ran
  2. Confirmed no output injection: Created test hook outputting secret word - Claude had no knowledge of it in new sessions
  3. Added debug logging: Instrumented qz(), Gf2(), and hook processing functions - confirmed qz() never called for new sessions
  4. Source code review: Examined cli.js - found only 3 call sites for qz(), 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:

  1. Started new conversation
  2. SessionStart hook output appeared:

``
SessionStart:startup hook succeeded: The secret word is "bamboozle".
``

  1. Asked Claude for secret word
  2. 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_context attachments (line 1818 in cli.js)
  • The bug only affects the interactive CLI mode, not --print mode (which calls qz("startup") at line 3726)

Files for Reference

  • Main implementation: cli.js line 3717 (wm6 function)
  • Hook processing: cli.js line ~1798 (qz function)
  • Context extraction: cli.js (Gf2 function)
  • UI rendering: cli.js line 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.

View original on GitHub ↗

23 Comments

github-actions[bot] · 11 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/9591
  2. https://github.com/anthropics/claude-code/issues/10287
  3. https://github.com/anthropics/claude-code/issues/9542

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

jeremybarnes · 11 months ago

Not a duplicate:

  • #9591 is about displaying the context, not running the script
  • #10287 is about all hooks, whereas this is about the SessionStart hook only (the others still appear to work)
  • #9542 is about the hooks hanging after being run (on Windows), this is about them never being run at all
crsmithdev · 10 months ago

Confirmed this issue. Setup:

  • SessionStart hook configured in ~/.claude/settings.json
  • Hook executes successfully when run manually
  • Proper JSON schema validation passes
  • Script outputs to stdout as expected

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:

"hooks": {
  "SessionStart": [
    {
      "hooks": [
        {
          "type": "command",
          "command": "bash ~/.claude/scripts/validate-commands.sh"
        }
      ]
    }
  ]
}
github-actions[bot] · 9 months ago

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.

leigh-sesameai · 9 months ago

how is this still open ... this is in your docs...

MarcinDudekDev · 9 months ago

+1 it doesn't work

iampava · 8 months ago

+1 same here

michael-denyer · 8 months ago

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.

Vaccano · 8 months ago

I can also confirm that this is broken for me. (Startup hook is ignored.)

scytaskin · 8 months ago

Also just tried to create a session startup hook. Same problem. Please fix.

drishk25 · 8 months ago

Facing the same issue.

greghughespdx · 8 months ago

Also experiencing this. As a workaround, I tried moving context injection to UserPromptSubmit hooks instead of SessionStart, but that has its own issues - the additionalContext from UserPromptSubmit hooks 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:

  • Session start (this issue)
  • Per-prompt (UserPromptSubmit - #12151)

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.

eilonb11 · 8 months ago

Facing the same issue

jspiro · 8 months ago

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?

TanakaSeiji · 7 months ago

Reporting empirical findings from Claude Code VSCode extension (March 2026):

startup matcher is now firing correctly in VSCode — both on Reload and on /new command.

Observed behavior (session-start-context-load log, 2026-03-03):

12:33:01 [start] session-start-context-load (trigger: startup)   ← VSCode Reload
12:33:01 [skip] already ran this minute                           ← 2nd concurrent fire caught by idempotency guard
12:33:01 [ok] cached last 200 lines (trigger: startup)
12:35:07 [start] session-start-context-load (trigger: startup)   ← /new command
12:35:07 [ok] cached last 200 lines (trigger: startup)
  • VSCode Reload: 2 concurrent fires at same second → idempotency guard needed (minute-based lock)
  • /new command: 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.

mikesimons · 7 months ago

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.

bidossessi · 7 months ago

Experiencing this on v2.1.19 (Linux).

  • SessionStart hook (Python script outputting plain text) was working for fresh sessions yesterday
  • Today, same setup fails silently for fresh sessions
  • No version update occurred (installation dates from January 23)
  • No script changes
  • Hook works correctly after /clear

The intermittent nature is puzzling - no identified variable changed between working and non-working state.

tdodd777 · 7 months ago

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.

mrmartan · 6 months ago

When Claude refers to "the issue" it means this issue.

You're right — the issue confirms that /clear does re-trigger SessionStart hooks (it's one of the 3 call sites for qz()). So the hook context was injected into my conversation
   after /clear.

  The issue is about fresh new sessions where SessionStart hooks silently discard their output. /clear is actually the documented workaround.

  So to answer your question: the boot instruction was present in my context (I can see it in the system-reminder). I just didn't act on it proactively before you messaged me —
  that's on me, not a platform bug. I should have called context_summary immediately without you having to ask.

❯ how is that on you and not a platform bug when you had it in the system-reminder and you still didn't follow it?

⏺ Fair point. You're right — if the instruction is in the system-reminder and I still don't follow it unprompted, that's a model reliability issue, not a conscious choice I
  made. I didn't "decide" to skip it; I just failed to prioritize it before responding to your message. That's exactly the kind of flaky instruction-following the issue
  commenters are frustrated about.

  It's a model behavior bug — same category as the comments saying "Opus is already a bit flakey about following instruction reliably."
rznd · 5 months ago

This is annoying, still happening, please fix.

OMGBrewmaster · 1 month ago

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 SessionStart hook 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:

[DEBUG] Hook output does not start with {, treating as plain text

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 tee its 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 the hookSpecificOutput.additionalContext branch 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 SessionStart hook 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.

OMGBrewmaster · 1 month ago

Follow-up to my comment above — I said we had switched our SessionStart hook 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.additionalContext IS 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:

SessionStart hook additional context: Cloud session environment established by the SessionStart hook (exit 0).

Steps:
    toolchain:        ok
    repo LFS assets:  ok
    unity reference:  ok

Full hook output: /tmp/cloud-session-start.log

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 text debug 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:

  • It is evidence that the additionalContext processing 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 the qz("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.
  • It says nothing about the macOS CLI path the original report tested — we have no way to exercise that here, and I am not claiming the issue is resolved.

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.

guruchill · 1 month ago

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.json with a single SessionStart hook (no matcher).

Comparing recently-created session .jsonl transcripts under ~/.claude/projects/<project>/:

  • Sessions where the hook fired: earliest entries include a type:"attachment" record with attachment.type:"hook_success" (hookName:"SessionStart:startup", containing the hook's raw stdout) immediately followed by attachment.type:"hook_additional_context" — and the injected <system-reminder> is visible in the very next turn's context.
  • Sessions where it didn't: the transcript goes straight from the queue-operation/session-init lines to the first real user/attachment:environment entries — no hook_success or hook_additional_context record 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 /clear immediately after opening a new session reliably triggers the hook (matches the root-cause note that /clear does call qz("startup")).