[BUG] --resume in REPL mode fires duplicate SessionStart hook with wrong session_id (spurious startup event)

Resolved 💬 3 comments Opened Feb 7, 2026 by alvarolb Closed Feb 11, 2026

Bug Description

When using claude --resume <session-id> in interactive REPL mode, two SessionStart hook events are fired instead of one. One has the correct session_id and source: "resume", but the other has a new random UUID and source: "startup", with a transcript_path pointing to a nonexistent/empty file.

This breaks integrations that use hooks to track sessions by session_id.

Reproduction Steps

  1. Create a SessionStart hook that logs the full JSON input to a file:
#!/bin/bash
INPUT=$(cat)
echo "$INPUT" | python3 -m json.tool >> /tmp/session-start-debug.log
  1. Start a session, send a message, then exit
  2. Resume with: claude --resume <session-id>
  3. Send a message
  4. Check the log file

Observed Behavior

Two SessionStart events arrive (same timestamp):

{
    "session_id": "431cad5d-8eed-4182-9ed9-2c9fddd569e5",
    "transcript_path": "...431cad5d-8eed-4182-9ed9-2c9fddd569e5.jsonl",
    "hook_event_name": "SessionStart",
    "source": "resume"
}
{
    "session_id": "42345f1a-34b2-4808-9f8b-9908b5a52d0d",
    "transcript_path": "...42345f1a-34b2-4808-9f8b-9908b5a52d0d.jsonl",
    "hook_event_name": "SessionStart",
    "source": "startup",
    "model": "claude-opus-4-6"
}
  • The startup event has a random new UUID (the process's internal session_id) and its transcript_path points to an empty/nonexistent file.
  • The arrival order is non-deterministic — sometimes startup arrives first, sometimes resume.
  • All subsequent hooks (UserPromptSubmit, Stop, etc.) correctly use the original session_id.
  • The transcript file is also correctly written to the original session's file.

Expected Behavior

Only one SessionStart event should fire with source: "resume" and the original session_id. The spurious startup event should not be emitted during --resume.

Root Cause (from source analysis)

In cli.js, the lj("startup") call (which fires SessionStart with source: "startup") is guarded by:

let gq = p || B || r || _1 ? null : lj("startup", { agentType: ..., model: ... });

Where p = initOnly, B = init, r = maintenance, _1 = !isInteractive.

The --resume and --continue flags are not included in this guard condition, so lj("startup") fires as a background promise even during resume. Meanwhile, lj("resume") is correctly called inside the is() function with the proper session_id.

The fix would be adding the resume/continue flags to the guard:

let gq = p || B || r || _1 || H.resume || H.continue ? null : lj("startup", { agentType: ..., model: ... });

Impact

  • Integrations that capture session_id from the first SessionStart event may capture the wrong (ephemeral) ID
  • The spurious transcript_path points to an empty file, breaking transcript watchers
  • Non-deterministic arrival order makes it impossible to reliably distinguish the correct event without inspecting the source field

Note

This does not affect --print mode — claude -p --resume <id> correctly fires only one SessionStart with the original session_id.

Environment

  • Claude Code version: 2.1.34
  • macOS (arm64)
  • Related issues: #8069, #12235, #10806 (these track a different but related problem — session_id mismatch — which may now be partially caused by this duplicate event)

View original on GitHub ↗

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