[BUG] SessionStart hooks invoked twice per session causing race conditions
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
- 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()
- Register the hook in
~/.claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "python3 /tmp/invocation_logger.py",
"timeout": 5
}
]
}
]
}
}
- Clear any previous logs:
rm -f /tmp/hook-invocation.log
- Start a Claude Code session (CLI or VS Code extension)
- 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
- Race conditions: Hooks that read-modify-write files see stale data from each other
- Duplicate work: Hooks execute twice, wasting resources
- 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]}")This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗