Environment: Windows 11 Pro (build 26200), Python 3.13.13, cortex plugin 4.13.0 installed through the marketplace. Same box as #91–#95. Found while diagnosing the SessionStart charmap crash (#96).
Setup that arms the bug
The 4.13.0 plugin cache's active deps dir (~\.claude\plugins\cache\cortex-plugins\cortex\4.13.0\deps) came up with the base stack (~200 top-level entries) but without the ML stack (no sentence_transformers / torch / flashrank). Since session_start is the one entry point that calls _ensure_all_deps (scripts/launcher.py:303-304), every session start walks into _pip_install(deps_dir, ["sentence-transformers==5.4.1", "flashrank==0.2.10"]).
The bug
_pip_install's commit step (scripts/launcher.py:246-252) moves each top-level entry from the temp target into deps/:
for entry in os.listdir(tmp_dir):
dest = os.path.join(deps_dir, entry)
if os.path.isdir(dest):
shutil.rmtree(dest, ignore_errors=True)
elif os.path.exists(dest):
os.remove(dest)
os.replace(os.path.join(tmp_dir, entry), dest)
pip resolves numpy as a transitive dep of sentence-transformers, so tmp_dir contains a numpy entry whose dest already exists. Meanwhile the MCP server — spawned in parallel by Claude Code at the same session start — has numpy imported, so its .pyd files are mapped and locked. On Windows that means:
shutil.rmtree(dest, ignore_errors=True) deletes everything in deps/numpy except the locked .pyd files (silent, ignore_errors=True),
os.replace fails: PermissionError: [WinError 5] Access is denied: '...\deps.tmp-11640\numpy' -> '...\deps\numpy',
- the exception propagates and the
finally: shutil.rmtree(tmp_dir, ignore_errors=True) (lines 253-254) deletes the freshly installed copy too.
Net result on my box: deps/numpy reduced to a husk — only _core/ and linalg/ (the dirs holding the locked .pyds) survived; numpy-2.5.1.dist-info and numpy.libs only survived because the loop crashed before reaching them alphabetically.
File "...\cortex\4.13.0\scripts\launcher.py", line 304, in main
_ensure_all_deps(deps_dir)
File "...\cortex\4.13.0\scripts\launcher.py", line 262, in _ensure_all_deps
_pip_install(
File "...\cortex\4.13.0\scripts\launcher.py", line 252, in _pip_install
os.replace(os.path.join(tmp_dir, entry), dest)
PermissionError: [WinError 5] Access is denied: '...\deps.tmp-11640\numpy' -> '...\deps\numpy'
Aftermath / blast radius
- Every subsequent hook spawn finds
numpy as a namespace husk; _importable's husk eviction can't fully delete it either (same locks), so each hook invocation re-attempts a multi-hundred-MB pip install and re-fails at the same os.replace — per prompt, for the rest of the session.
- Entries alphabetically before
numpy (e.g. flashrank, huggingface_hub, …) get silently replaced with whatever pip just resolved before the crash — partial, unversioned drift of shared deps.
- Whether a given session start corrupts or succeeds is a race against the server importing numpy, so the failure is intermittent and easy to misattribute.
Suggested fixes (any subset)
- Idempotence guard: skip an entry when
dest exists and its *.dist-info already satisfies the pin — shared transitive deps like numpy should never be touched at all.
- Non-destructive commit: copy-merge (no-clobber) instead of
rmtree + os.replace, or rename dest aside and roll back on failure; and don't delete tmp_dir in finally when the commit failed — leaving it is exactly what makes the current failure unrecoverable.
- Take the ML-stack ensure out of the SessionStart hot path: a one-time bootstrap with a success stamp + file lock can't race the server boot.
- Cheaper presence probe:
_importable("sentence_transformers", ...) imports torch inside the hook process just to answer "is it installed"; a dist-info check is enough.
Workaround used
Out-of-band pip install --target <staging> of numpy==2.5.1 sentence-transformers==5.4.1 flashrank==0.2.10, then a no-clobber copy into deps/ (locked files are same-version, so skipping them is safe). With the ML stack present, _ensure_all_deps is a no-op and the destructive path is unreachable.
Environment: Windows 11 Pro (build 26200), Python 3.13.13, cortex plugin 4.13.0 installed through the marketplace. Same box as #91–#95. Found while diagnosing the SessionStart charmap crash (#96).
Setup that arms the bug
The 4.13.0 plugin cache's active deps dir (
~\.claude\plugins\cache\cortex-plugins\cortex\4.13.0\deps) came up with the base stack (~200 top-level entries) but without the ML stack (nosentence_transformers/torch/flashrank). Sincesession_startis the one entry point that calls_ensure_all_deps(scripts/launcher.py:303-304), every session start walks into_pip_install(deps_dir, ["sentence-transformers==5.4.1", "flashrank==0.2.10"]).The bug
_pip_install's commit step (scripts/launcher.py:246-252) moves each top-level entry from the temp target intodeps/:pip resolves numpy as a transitive dep of sentence-transformers, so
tmp_dircontains anumpyentry whosedestalready exists. Meanwhile the MCP server — spawned in parallel by Claude Code at the same session start — has numpy imported, so its.pydfiles are mapped and locked. On Windows that means:shutil.rmtree(dest, ignore_errors=True)deletes everything indeps/numpyexcept the locked.pydfiles (silent,ignore_errors=True),os.replacefails:PermissionError: [WinError 5] Access is denied: '...\deps.tmp-11640\numpy' -> '...\deps\numpy',finally: shutil.rmtree(tmp_dir, ignore_errors=True)(lines 253-254) deletes the freshly installed copy too.Net result on my box:
deps/numpyreduced to a husk — only_core/andlinalg/(the dirs holding the locked.pyds) survived;numpy-2.5.1.dist-infoandnumpy.libsonly survived because the loop crashed before reaching them alphabetically.Aftermath / blast radius
numpyas a namespace husk;_importable's husk eviction can't fully delete it either (same locks), so each hook invocation re-attempts a multi-hundred-MB pip install and re-fails at the sameos.replace— per prompt, for the rest of the session.numpy(e.g.flashrank,huggingface_hub, …) get silently replaced with whatever pip just resolved before the crash — partial, unversioned drift of shared deps.Suggested fixes (any subset)
destexists and its*.dist-infoalready satisfies the pin — shared transitive deps like numpy should never be touched at all.rmtree + os.replace, or renamedestaside and roll back on failure; and don't deletetmp_dirinfinallywhen the commit failed — leaving it is exactly what makes the current failure unrecoverable._importable("sentence_transformers", ...)imports torch inside the hook process just to answer "is it installed"; a dist-info check is enough.Workaround used
Out-of-band
pip install --target <staging>ofnumpy==2.5.1 sentence-transformers==5.4.1 flashrank==0.2.10, then a no-clobber copy intodeps/(locked files are same-version, so skipping them is safe). With the ML stack present,_ensure_all_depsis a no-op and the destructive path is unreachable.