[BUG] SessionStart hooks invoked twice per session causing race conditions

Resolved 💬 5 comments Opened Dec 21, 2025 by coygeek Closed Feb 14, 2026

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?

Claude Code is invoking SessionStart hooks twice per session start, with invocations occurring 82ms-1400ms apart. This causes race conditions in hooks that read-modify-write files, as both invocations run concurrently and see stale data.

In my case, this caused duplicate entries in a changelog file because both hook invocations detected the same "missing" commits and inserted them independently.

What Should Happen?

SessionStart hooks should be invoked exactly once per session start. A single, atomic invocation would prevent race conditions and duplicate work.

Error Messages/Logs

From `~/.claude/hooks/hooks.log`, showing the same hook invoked twice within milliseconds:


# Pattern: Same hook, same directory, two invocations within seconds

18:11:26.025 [INFO] [bootstrap-changelog] [pwd:generate-changelog-after-commit] starting
18:11:27.391 [INFO] [bootstrap-changelog] [pwd:generate-changelog-after-commit] starting
                                          ↑ 1366ms gap

18:42:14.321 [INFO] [bootstrap-changelog] [pwd:enforce-gitignore] starting
18:42:14.449 [INFO] [bootstrap-changelog] [pwd:enforce-gitignore] starting
                                          ↑ 128ms gap

18:57:53.479 [INFO] [bootstrap-changelog] [pwd:crawl4ai] starting
18:57:53.976 [INFO] [bootstrap-changelog] [pwd:crawl4ai] starting
                                          ↑ 497ms gap (this caused duplicate changelog entries!)

19:00:16.838 [INFO] [bootstrap-changelog] [pwd:generate-changelog-after-commit] starting
19:00:16.920 [INFO] [bootstrap-changelog] [pwd:generate-changelog-after-commit] starting
                                          ↑ 82ms gap

Steps to Reproduce

  1. Create a minimal SessionStart hook that logs timestamps:
#!/usr/bin/env python3
# Save as: /tmp/invocation_logger.py

import json
import os
import sys
from datetime import datetime
from pathlib import Path

LOG_FILE = Path("/tmp/hook-invocation.log")

def main():
    timestamp = datetime.now().isoformat(timespec="milliseconds")
    pid = os.getpid()

    try:
        input_data = json.load(sys.stdin)
        cwd = input_data.get("cwd", "unknown")
    except:
        cwd = "unknown"

    with LOG_FILE.open("a") as f:
        f.write(f"{timestamp} | PID: {pid} | cwd: {cwd}\n")

    sys.exit(0)

if __name__ == "__main__":
    main()
  1. Register the hook in ~/.claude/settings.json:
{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "python3 /tmp/invocation_logger.py",
            "timeout": 5
          }
        ]
      }
    ]
  }
}
  1. Clear any previous logs:
rm -f /tmp/hook-invocation.log
  1. Start a Claude Code session (CLI or VS Code extension)
  1. Check the log:
cat /tmp/hook-invocation.log

Expected: One log entry per session start
Actual: Two log entries with different PIDs, ~100-1400ms apart

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

2.0.75

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

Environment

  • Claude Code version: Latest (December 2025)
  • OS: macOS Darwin 25.1.0
  • Python: 3.14.1
  • Tested in: VS Code extension

Impact

  1. Race conditions: Hooks that read-modify-write files see stale data from each other
  2. Duplicate work: Hooks execute twice, wasting resources
  3. Data corruption: Files modified by hooks can end up with duplicate content

Workaround Implemented

I added deduplication checks to my hook to handle the race condition:

# Skip if commit already exists (prevents race condition from double invocation)
if commit_hash and f"[{commit_hash}]" in content:
    return False  # Already inserted by other invocation

Possible Causes

The two invocations appear to be from separate processes (different PIDs based on timing gaps). This suggests Claude Code may be spawning hook execution twice, possibly from:

  • Multiple initialization paths (UI + backend?)
  • Retry logic that doesn't check if first invocation succeeded
  • Parallel component initialization

Diagnostic Script

I created a diagnostic script to analyze existing hook logs for double-invocation patterns:

#!/usr/bin/env python3
# Analyzes ~/.claude/hooks/hooks.log for double invocations

import re
from collections import defaultdict
from pathlib import Path

log_file = Path.home() / ".claude/hooks/hooks.log"
pattern = re.compile(r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+).*\[(\w+-\w+)\].*\[pwd:([^\]]+)\].*starting")

entries = defaultdict(list)
for line in log_file.read_text().splitlines():
    if match := pattern.search(line):
        ts, hook, pwd = match.groups()
        key = f"{hook}:{pwd}"
        entries[key].append(ts)

# Find entries with multiple starts within 2 seconds
for key, timestamps in entries.items():
    for i in range(len(timestamps) - 1):
        # Check if consecutive timestamps are within 2 seconds
        # (simplified check - production would parse timestamps)
        if timestamps[i][:17] == timestamps[i+1][:17]:  # Same minute
            print(f"Double invocation: {key}")
            print(f"  {timestamps[i]}")
            print(f"  {timestamps[i+1]}")

View original on GitHub ↗

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