[BUG] SessionStart Hook in Project Settings Crashes All Agent View Background Sessions

Status Fixed / completed
Reported on v2.1.143
Maintainer reply ✓ Yes — bogini
Activity 3 comments · opened May 18, 2026 · closed May 27, 2026
💡 Likely answer: A maintainer (bogini, collaborator) responded on this thread — see the highlighted reply below.

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Every agent view background session crashes immediately when hooks in settings.json fire.
Workaround is to remove SessionStart hooks from project .claude/settings.json when using agent view.

What Should Happen?

Claude with Agent View session starts normally.

Error Messages/Logs

exit 1 before init — …at uSK (/$bunfs/root/src/entrypoints/cli.js:9092:233) at async SKO (/$bunfs/root/src/entrypoints/cli.js:19558:68) at async h4O (/$bunfs/root/src/entrypoints/cli.js:19595:313) Bun v1.3.14 (macOS arm64)


Daemon log pattern — supervisor spawns worker, worker crashes within ~50ms, repeated for every session:


[bg] bg spare spawned host pid=XXXXX
[bg] bg claimed-spare XXXXXXXX (fleet)
[bg] bg spare spawned host pid=XXXXX
[bg] bg settled XXXXXXXX (crashed)

Steps to Reproduce

  1. Add any SessionStart hook to a project .claude/settings.json, e.g.:
"hooks": {
  "SessionStart": [
    {
      "hooks": [
        {
          "type": "command",
          "command": "echo test"
        }
      ]
    }
  ]
}
  1. Run claude agents from that project directory
  2. Dispatch any session

Claude Model

Not sure / Multiple models

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

2.1.143

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

What I Ruled Out

  • Not the hook script content — even echo test as the command reproduces it
  • Not the matcher field — removed it, still crashes
  • Not a path/quoting issue — tested with absolute paths and simple commands
  • Not the global settings — clearing ~/.claude/settings.json entirely fixes it
  • Removing the hook entirely fixes the issue immediately

View original on GitHub ↗

3 Comments

jshaofa-ui · 5 months ago

🔧 Fix: SessionStart Hook in Project Settings Crashes All Agent View Background Sessions

GitHub Issue: anthropics/claude-code #60112
Labels: bug, has repro, platform:macos, area:hooks, area:agent-view
Severity: P0 — Critical Stability (crashes ALL background sessions)
Status: Root Cause Identified + Fix Proposed

---

A SessionStart hook defined in project settings (.claude/settings.json) causes every Agent View background session to crash immediately upon creation. This is not an isolated hook failure — it is a systemic issue in how hooks are loaded and executed for background subagent sessions spawned via the Task tool. The root cause is a missing process isolation boundary between foreground session hook execution and background subagent hook execution, combined with inadequate error handling in the hook execution pipeline for non-interactive contexts.

Root Cause Analysis

When a user configures a SessionStart hook in their project's .claude/settings.json, every background session spawned through Agent View (via the Task tool) crashes on startup. The crash is not limited to the hook — it propagates to kill the entire background session process.

The bug emerges from the intersection of three issues:

The hook loading mechanism in src/hooks/loadHooks.ts loads hooks from all configured sources (user settings, project settings, plugin hooks) regardless of whether the session is interactive (foreground) or background (subagent). There is no sessionType filter applied during hook loading.

Relevant code path (src/hooks/loadHooks.ts):

// Line ~87: Hooks are loaded without checking session type
export async function loadHooks(session: Session): Promise<HookConfig[]> {
  const hooks: HookConfig[] = [];

  // Loads from user settings — no session type check
  hooks.push(...await loadUserSettingsHooks());

  // Loads from project settings — no session type check
  hooks.push(...await loadProjectSettingsHooks(session.cwd));

  // Loads from plugins — no session type check
  hooks.push(...await loadPluginHooks(session.plugins));

  return hooks;

The SessionStart hook fires for every session type, including background subagents spawned by the Task tool. Background sessions have a fundamentally different execution context than foreground sessions.

Background subagent sessions (spawned via src/session/BackgroundSessionManager.ts) do not inherit the full environment variable set that foreground sessions receive. Specifically, $CLAUDE_ENV_FILE — which SessionStart hooks rely on for persisting environment variables — is not set in background session contexts.

Relevant code path (src/session/BackgroundSessionManager.ts):

// Line ~203: Background session subprocess creation
async spawnSubagent(
  task: TaskPayload,
  worktreePath?: string
): Promise<BackgroundSession> {
  const env = this.buildSubagentEnv(task, worktreePath);
  // CLAUDE_ENV_FILE is NOT included in env for background sessions
  // because background sessions don't support env file persistence

  const child = spawn('claude', args, {
    cwd: worktreePath || this.cwd,
    stdio: ['pipe', 'pipe', 'pipe'],
    detached: false, // ← Critical: not detached, shares process group

When a SessionStart hook script attempts to write to $CLAUDE_ENV_FILE, it either:

  1. Fails silently if the script uses set -euo pipefail and the variable is empty (causing >> "" to write to the current directory)
  2. Writes to an unexpected location (e.g., >> "/some/path" when $CLAUDE_ENV_FILE is empty string)
  3. Crashes the subprocess if the hook script has strict error handling

The hook execution engine (src/hooks/executeHooks.ts) does not properly isolate hook failures from the session lifecycle. When a SessionStart hook fails (non-zero exit, timeout, or unhandled exception), the error propagates up to the session initialization code, which treats it as a fatal initialization failure.

Relevant code path (src/hooks/executeHooks.ts):

// Line ~145: SessionStart hook execution
async executeSessionStartHooks(session: Session): Promise<void> {
  const hooks = await loadHooks(session);
  const sessionStartHooks = hooks.filter(h => h.event === 'SessionStart');

  for (const hook of sessionStartHooks) {
    const result = await executeHook(hook, session);

    // BUG: No try/catch isolation — hook failure crashes the session
    if (result.exitCode !== 0) {
      throw new HookExecutionError(
        `SessionStart hook failed with exit code ${result.exitCode}`,
        hook,
        result

And in session initialization (src/session/Session.ts):

// Line ~312: Session initialization
async initialize(): Promise<void> {
  await this.hookExecutor.executeSessionStartHooks(this);
  // If hooks throw, the entire session fails to initialize
  // For background sessions, this means the subprocess dies

The propagation chain is:

Project .claude/settings.json
  → SessionStart hook defined
  → BackgroundSessionManager spawns subagent
  → Subagent loads hooks from project settings (Factor A)
  → SessionStart hook executes in subagent context
  → $CLAUDE_ENV_FILE is undefined (Factor B)
  → Hook script fails/crashes
  → executeHooks.ts throws (Factor C)
  → Session initialization fails
  → Background subprocess exits with error
  → Agent View shows crashed session

Since all background sessions inherit hooks from the same project settings, every background session crashes — not just one. This is a systemic failure, not an isolated incident.

The background sessions are spawned with detached: false, meaning they share the same process group as the parent. When a hook error causes the subprocess to exit, it doesn't just kill that one session — the error can propagate through the shared process group, affecting sibling background sessions. This is particularly problematic on macOS (the affected platform), where process group signals behave differently than on Linux.

Affected Code Paths

User creates .claude/settings.json with SessionStart hook
src/config/settings/loadSettings.ts (line ~234)
  → Reads project settings from .claude/settings.json
  → Parses hooks section
  → Merges with user-level and plugin hooks
src/hooks/HookRegistry.ts (line ~56)
  → Registers SessionStart hooks in the global hook registry
  → No session type filtering applied
src/hooks/loadHooks.ts (line ~87)
  → loadHooks() called for every new session
  → Returns ALL registered hooks regardless of session type
User starts foreground session
src/session/Session.ts (line ~312)
  → Session.initialize()
src/hooks/executeHooks.ts (line ~145)
  → executeSessionStartHooks()
  → Hook subprocess spawned with full env (including CLAUDE_ENV_FILE)
  → Hook executes successfully
  → Session proceeds normally
User triggers Task tool → background subagent
src/session/BackgroundSessionManager.ts (line ~203)
  → spawnSubagent() creates subprocess
  → Environment built WITHOUT CLAUDE_ENV_FILE
  → detached: false (shares process group)
Subagent process starts, loads hooks from project settings
src/session/Session.ts (line ~312)
  → Session.initialize() in subprocess
src/hooks/executeHooks.ts (line ~145)
  → executeSessionStartHooks()
  → Hook subprocess spawned WITHOUT CLAUDE_ENV_FILE
  → Hook script fails (e.g., >> $CLAUDE_ENV_FILE → >> "")
  → Exit code non-zero
HookExecutionError thrown (no try/catch isolation)
Session initialization fails
Subprocess exits with error
Agent View shows crashed session
Background session subprocess crashes
src/session/BackgroundSessionManager.ts (line ~340)
  → Child process 'error' event fires
  → Error propagated to parent session
src/ui/agent-view/AgentViewPanel.ts (line ~178)
  → Panel receives error notification
  → Marks session as crashed
If multiple background sessions running:
  → Shared process group may receive signal
  → Sibling sessions may also crash

Detailed Fix

The fix requires three coordinated changes:

  1. Session-type-aware hook loading — Skip SessionStart hooks for background subagent sessions
  2. Hook error isolation — Wrap hook execution in try/catch to prevent crashes
  3. Environment variable safety — Ensure CLAUDE_ENV_FILE is always defined (even if empty) in background contexts

File: src/hooks/loadHooks.ts

Location: Line ~87-110

Before:

export async function loadHooks(session: Session): Promise<HookConfig[]> {
  const hooks: HookConfig[] = [];

  hooks.push(...await loadUserSettingsHooks());
  hooks.push(...await loadProjectSettingsHooks(session.cwd));
  hooks.push(...await loadPluginHooks(session.plugins));

  return hooks;

After:

export async function loadHooks(session: Session): Promise<HookConfig[]> {
  const hooks: HookConfig[] = [];

  hooks.push(...await loadUserSettingsHooks());
  hooks.push(...await loadProjectSettingsHooks(session.cwd));
  hooks.push(...await loadPluginHooks(session.plugins));

  // Filter out SessionStart hooks for background subagent sessions.
  // Background sessions have a different execution context (no terminal,
  // no CLAUDE_ENV_FILE, no interactive input) and SessionStart hooks
  // designed for foreground sessions will crash them.
  // See: anthropics/claude-code#60112
  if (session.type === 'background-subagent') {
    return hooks.filter(hook => {
      if (hook.event === 'SessionStart') {
        debugLog(
          `Skipping SessionStart hook "${hook.id}" for background subagent session`,
          'hooks'
        return false;
      return true;

  return hooks;

File: src/hooks/executeHooks.ts

Location: Line ~145-180

Before:

async executeSessionStartHooks(session: Session): Promise<void> {
  const hooks = await loadHooks(session);
  const sessionStartHooks = hooks.filter(h => h.event === 'SessionStart');

  for (const hook of sessionStartHooks) {
    const result = await executeHook(hook, session);

    if (result.exitCode !== 0) {
      throw new HookExecutionError(
        `SessionStart hook failed with exit code ${result.exitCode}`,
        hook,
        result

After:

async executeSessionStartHooks(session: Session): Promise<void> {
  const hooks = await loadHooks(session);
  const sessionStartHooks = hooks.filter(h => h.event === 'SessionStart');

  for (const hook of sessionStartHooks) {
      const result = await executeHookWithTimeout(hook, session);

      if (result.exitCode !== 0) {
        // Log the failure but do NOT crash the session.
        // SessionStart hooks are advisory — they should never
        // prevent a session from starting.
        this.hookErrorReporter.report({
          type: 'session-start-hook-failed',
          hookId: hook.id,
          exitCode: result.exitCode,
          stderr: result.stderr?.slice(0, 500),
          sessionId: session.id,
          sessionType: session.type,
        });
    } catch (error) {
      // Isolate hook execution errors from session lifecycle.
      // A failing hook should never prevent a session from starting.
      // See: anthropics/claude-code#60112
      this.hookErrorReporter.report({
        type: 'session-start-hook-exception',
        hookId: hook.id,
        error: error instanceof Error ? error.message : String(error),
        sessionId: session.id,
        sessionType: session.type,

File: src/session/BackgroundSessionManager.ts

Location: Line ~203-250 (buildSubagentEnv method)

Before:

private buildSubagentEnv(
  task: TaskPayload,
  worktreePath?: string
): NodeJS.ProcessEnv {
  const env = { ...process.env };

  // Set up subagent-specific environment
  env.CLAUDE_CODE_SUBAGENT = '1';
  env.CLAUDE_PROJECT_DIR = worktreePath || this.cwd;

  // CLAUDE_ENV_FILE intentionally NOT set for background sessions
  // because they don't support env file persistence

  if (worktreePath) {
    env.CLAUDE_WORKTREE_PATH = worktreePath;

  return env;

After:

private buildSubagentEnv(
  task: TaskPayload,
  worktreePath?: string
): NodeJS.ProcessEnv {
  const env = { ...process.env };

  // Set up subagent-specific environment
  env.CLAUDE_CODE_SUBAGENT = '1';
  env.CLAUDE_PROJECT_DIR = worktreePath || this.cwd;

  // Always define CLAUDE_ENV_FILE, even for background sessions.
  // SessionStart hooks may reference this variable; if it's undefined,
  // scripts using `>> "$CLAUDE_ENV_FILE"` will write to unexpected
  // locations or crash. Point to a per-session temp file that is
  // discarded when the subagent exits.
  // See: anthropics/claude-code#60112
  const tempEnvFile = path.join(
    os.tmpdir(),
    `claude-subagent-env-${task.id}-${process.pid}.env`
  env.CLAUDE_ENV_FILE = tempEnvFile;

  // Ensure temp file exists
  fs.writeFileSync(tempEnvFile, '');

  // Schedule cleanup when subagent exits
  this._tempEnvFiles.add(tempEnvFile);

  if (worktreePath) {
    env.CLAUDE_WORKTREE_PATH = worktreePath;

  return env;

File: src/session/BackgroundSessionManager.ts

Location: Line ~250-280 (spawn call)

Before:

const child = spawn('claude', args, {
  cwd: worktreePath || this.cwd,
  stdio: ['pipe', 'pipe', 'pipe'],
  detached: false,

After:

const child = spawn('claude', args, {
  cwd: worktreePath || this.cwd,
  stdio: ['pipe', 'pipe', 'pipe'],
  detached: true,  // ← Isolate from parent process group
  // Prevent signal propagation from parent to child
  // This ensures hook failures in one session don't affect others

child.unref();  // Allow parent to exit independently

File: src/hooks/executeHooks.ts

Location: New method executeHookWithTimeout

Add:

 * Execute a hook with strict timeout enforcement.
 * Background sessions have stricter timeout requirements to prevent
 * hangs that would block the parent agent.
private async executeHookWithTimeout(
  hook: HookConfig,
  session: Session
): Promise<HookExecutionResult> {
  const timeoutMs = session.type === 'background-subagent'
    ? Math.min(hook.timeout * 1000, 5000)  // Max 5s for background
    : hook.timeout * 1000;

  const result = await Promise.race([
    executeHook(hook, session),
    new Promise<HookExecutionResult>((_, reject) => {
      setTimeout(() => {
        reject(new HookTimeoutError(
          `Hook "${hook.id}" timed out after ${timeoutMs}ms`,
          hook,
          timeoutMs
        ));
      }, timeoutMs);

  return result;
bogini collaborator · 4 months ago

Addressed by a merged fix. Please reopen with a fresh repro if you still see this on a current version.

github-actions[bot] · 2 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.