claude -p --no-session-persistence still runs SessionStart hooks from CWD project, causing hangs

Resolved 💬 3 comments Opened Apr 2, 2026 by zenprocess Closed Apr 2, 2026

Summary

claude -p --no-session-persistence loads and executes all SessionStart hooks defined in the CWD's .claude/settings.json. When a project has slow or blocking SessionStart hooks (e.g., draining an observation queue via HTTP, checking infrastructure health), claude -p hangs for the duration of those hooks before processing the prompt. This makes claude -p unusable for programmatic dispatch from projects with rich hook configurations.

Reproduction

# From a project directory with SessionStart hooks:
cd ~/my-project  # has .claude/settings.json with SessionStart hooks
time claude -p "reply ok" --no-session-persistence --output-format text
# Hangs for 30-60 seconds (sum of hook timeouts), or indefinitely if a hook hangs

# From a directory WITHOUT hooks:
cd /tmp
time claude -p "reply ok" --no-session-persistence --output-format text
# Returns immediately (~2 seconds)

Root Cause Analysis

When claude -p starts from a project directory:

  1. CWD-based project detection: Claude Code detects .claude/settings.json in the CWD and loads the project configuration, including all hooks
  2. SessionStart hooks fire: All SessionStart hooks execute sequentially before the prompt is processed
  3. --no-session-persistence does not skip hooks: The flag controls conversation persistence, not hook execution. Hooks still fire.
  4. Blocking hooks block the entire invocation: If any SessionStart hook hangs (e.g., HTTP call to a slow service), claude -p blocks for the hook's timeout duration

Measured Impact (real project with 7 SessionStart hooks)

| Hook | Timeout | Actual Duration | Status |
|------|---------|-----------------|--------|
| session-health-gate.sh | 30s | 1.4s | OK |
| observation-capture.py | 10s | 30s (hangs) | Blocks — drains 349 queued observations via HTTP to local LLM |
| memory-inject-l1.py | 8s | 34ms | OK |
| drain-notifications.py | 3s | 26ms | OK |
| wave-summary-bridge.py | 2s | 32ms | OK |
| memory-forgetting-trigger.py | 30s | 70ms | OK |
| auto-dream-trigger.py | 60s | 63ms | OK |
| Total worst case | 143s | ~32s | |

A single blocking hook (observation-capture.py) makes every claude -p invocation wait 30 seconds. With --no-session-persistence, the user expects a fast, stateless invocation — they're explicitly opting out of session state.

Why This Matters

Multi-agent dispatch systems (like Switchyard) invoke claude -p programmatically from the project directory to run headless agents. When SessionStart hooks fire on each invocation, dispatch throughput drops from seconds to minutes. A 5-agent parallel dispatch that should take 2 minutes takes 10+ minutes due to hook overhead.

Expected Behavior

claude -p --no-session-persistence should:

  1. Skip all SessionStart hooks — the user is requesting a stateless, one-shot invocation
  2. OR provide a --skip-hooks / --no-hooks flag to explicitly opt out of hook execution
  3. OR at minimum, run SessionStart hooks with a hard 2-second cap per hook when in print mode (-p)

Environment

  • Claude Code version: 2.1.90
  • OS: Linux (OrbStack)
  • Node.js: v22.x
  • Project: zendev-lite (7 SessionStart hooks, 6 PreToolUse hooks, 7 PostToolUse hooks)

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗