Summary
The SessionStart and Stop hooks (scripts/claude-obsidian.py hook session-start / hook stop, 5s timeout in hooks/hooks.json) time out on a large fraction of runs in real usage, even though the hook logic itself (hook_adapter.session_start_context / stop_status) is trivial — a couple of bounded file reads.
Evidence
Scanned my own Claude Code session transcripts over 13 days / 48 sessions:
| Hook |
Runs |
Timed out |
SessionStart:startup |
20 |
19 (95%), up to ~21s |
SessionStart:resume |
3 |
3 (100%), up to ~11s |
Stop |
4 |
3 (75%), up to ~10s |
Running the exact hook command in isolation (no concurrent load) takes a consistent ~0.9-1.0s — under the 5s timeout, but still slow for what the hook actually does. Notably, user+sys CPU time for that run is only ~0.02-0.08s of the ~1s wall clock — the process spends nearly all its time waiting on something external (process launch / file-read interception), not computing.
Suspected causes
sys.dont_write_bytecode = True in scripts/claude-obsidian.py disables Python's .pyc cache, so every hook invocation recompiles all imported modules from source instead of reusing a cache after the first run.
claude_obsidian/cli.py eagerly imports ~13 submodules at module level (capture, checkpoint, contracts, extensions, gates, ledgers, lint_engine, mode_config, package_validation, release, transaction, vault_ops, plus hook_adapter) regardless of which subcommand is invoked. The hook session-start/hook stop path only needs hook_adapter — the other ~12 are dead weight on this path.
- On Windows specifically,
python3 commonly resolves to the Store app-execution-alias stub (...\AppData\Local\Microsoft\WindowsApps\python3.exe) ahead of a real interpreter on PATH, which adds its own launch overhead before Python even starts. Confirmed this resolution order on my machine via both where python3 and Get-Command python3 -All.
None of these alone fully explains the 5-21s real-world timeouts (my isolated runs stayed under 1.5s), so I suspect contention: at actual session start, this hook runs concurrently with Claude Code connecting MCP servers and loading other plugins/hooks. If antivirus real-time scanning is involved (plausible given the near-zero CPU time relative to wall time, and that .pyc caching is disabled so every file gets freshly read every time), that contention would compound significantly.
Suggested fixes
- Remove
sys.dont_write_bytecode = True (or scope it to only the commands that need it) so repeated invocations benefit from Python's normal bytecode cache.
- Make the imports in
cli.py lazy per-subcommand, or give the hook subcommand its own thin entry point that only imports hook_adapter, so hook session-start/hook stop don't pull in lint/transaction/release/vault_ops machinery they never touch.
- Consider raising the default hook timeout, or documenting that Windows users may want to point
hooks.json's command at a specific interpreter path rather than bare python3, to avoid the Store alias stub.
Environment
- OS: Windows 11 Pro
- Claude Code: 2.1.233 (native install)
- Plugin version: claude-obsidian 2.1.0
- Python: 3.14.7 (multiple interpreters on PATH;
python3 resolves to the WindowsApps alias first)
Summary
The
SessionStartandStophooks (scripts/claude-obsidian.py hook session-start/hook stop, 5s timeout inhooks/hooks.json) time out on a large fraction of runs in real usage, even though the hook logic itself (hook_adapter.session_start_context/stop_status) is trivial — a couple of bounded file reads.Evidence
Scanned my own Claude Code session transcripts over 13 days / 48 sessions:
SessionStart:startupSessionStart:resumeStopRunning the exact hook command in isolation (no concurrent load) takes a consistent ~0.9-1.0s — under the 5s timeout, but still slow for what the hook actually does. Notably,
user+sysCPU time for that run is only ~0.02-0.08s of the ~1s wall clock — the process spends nearly all its time waiting on something external (process launch / file-read interception), not computing.Suspected causes
sys.dont_write_bytecode = Trueinscripts/claude-obsidian.pydisables Python's.pyccache, so every hook invocation recompiles all imported modules from source instead of reusing a cache after the first run.claude_obsidian/cli.pyeagerly imports ~13 submodules at module level (capture,checkpoint,contracts,extensions,gates,ledgers,lint_engine,mode_config,package_validation,release,transaction,vault_ops, plushook_adapter) regardless of which subcommand is invoked. Thehook session-start/hook stoppath only needshook_adapter— the other ~12 are dead weight on this path.python3commonly resolves to the Store app-execution-alias stub (...\AppData\Local\Microsoft\WindowsApps\python3.exe) ahead of a real interpreter on PATH, which adds its own launch overhead before Python even starts. Confirmed this resolution order on my machine via bothwhere python3andGet-Command python3 -All.None of these alone fully explains the 5-21s real-world timeouts (my isolated runs stayed under 1.5s), so I suspect contention: at actual session start, this hook runs concurrently with Claude Code connecting MCP servers and loading other plugins/hooks. If antivirus real-time scanning is involved (plausible given the near-zero CPU time relative to wall time, and that
.pyccaching is disabled so every file gets freshly read every time), that contention would compound significantly.Suggested fixes
sys.dont_write_bytecode = True(or scope it to only the commands that need it) so repeated invocations benefit from Python's normal bytecode cache.cli.pylazy per-subcommand, or give thehooksubcommand its own thin entry point that only importshook_adapter, sohook session-start/hook stopdon't pull in lint/transaction/release/vault_ops machinery they never touch.hooks.json'scommandat a specific interpreter path rather than barepython3, to avoid the Store alias stub.Environment
python3resolves to the WindowsApps alias first)