Skip to content

security-guidance: count Windows sessions whose Python is behind a link (measurement only) - #6920

Draft
mhegazy wants to merge 1 commit into
mainfrom
mhegazy/sg-interp-reparse-telemetry
Draft

mhegazy wants to merge 1 commit into
mainfrom
mhegazy/sg-interp-reparse-telemetry

Conversation

@mhegazy

@mhegazy mhegazy commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Before: nothing in the plugin's metrics says whether the Python that runs its hooks sits behind a symbolic link or junction on Windows. anthropics/claude-code#96870 reports that inside the packaged Claude Desktop app Windows leaks kernel memory on every file opened through such a link under AppData (a uv-managed Python is one), and every hook starts an interpreter, so the plugin's hooks add to that leak. How many users are in that situation is unknown.

After: the SessionStart row carries one more key on Windows, interp_reparse, that says so. No behaviour changes.

Measurement only: no hook does anything different, no process is started, and no other row changes.

How

  • interp_reparse_metric() in hooks/_base.py walks the folders of sys.executable and sys.base_prefix with one lstat each, up to the first symbolic link or junction, and reports bits: 1 executable path through a link, 2 standard library path through a link, 4 that path is under %APPDATA% or %LOCALAPPDATA% as spelled, 8 a path could not be checked (including Python older than 3.8, which has no reparse tag).
  • App execution aliases (...\Microsoft\WindowsApps) and volumes mounted at a folder are not counted as links.
  • 0 is sent too, so a Windows row without the key is a row that lost it, not a "no".
  • ensure_agent_sdk.py appends it as the last key of the SessionStart row, so it is the first key a row over the key cap loses and never displaces another. A healthy row stays within 10 keys.
  • Other systems return before looking at anything.

Implications

  • One more key on each Windows SessionStart row, and about 10 extra lstat calls once per session on Windows.
  • It sizes sessions and users, not hook starts: only rows the plugin already sends carry metrics, and most hook runs send none.
  • Not covered: where a link points (a link outside AppData whose target is inside reads as "not AppData"), 8.3 short path spellings, and links on the plugin's own folders.

Tests

  • tests/test_interp_reparse_metric.py: 21 tests with the interpreter paths and file attributes passed in (regular install, junction and symlink under AppData, link elsewhere, only the standard library or only the executable behind a link, AppData itself moved by a link, app execution alias, mounted volume, unreadable folders, failures on one path, other systems, the SessionStart row and its key order).
  • GitHub-hosted Windows runners (Server 2025 x64 and Windows 11 ARM64, Python 3.12), running the function from this branch under real interpreters:
interpreter value
regular install 0
junction under %APPDATA% to that install 7
directory symlink under %LOCALAPPDATA% 7
junction outside AppData 3
venv of a regular install 0
venv of the junctioned install 6
uv-managed Python through uv's minor-version junction (cpython-3.12-windows-...-none) 7
the same uv Python through its real folder 0
venv made by uv from that junction 6
  • Not run: a real Claude Code session on Windows carrying the key end to end (the function was called directly), a mounted volume on real Windows, Python 3.7 on Windows, and anything inside the packaged Desktop app.

No version bump.

Human: @mhegazy


Generated by Claude Code

Measurement only. On Windows the SessionStart row gains one key,
interp_reparse, saying whether the interpreter that runs the hooks is
reached through a symbolic link or junction: 1 executable path, 2
standard library path, 4 that path is under AppData, 8 could not be
checked; 0 when neither is.

Inside the packaged Claude Desktop app Windows leaks kernel memory on
every file opened through such a link under AppData (a uv-managed
Python is one), and every hook starts an interpreter. This sizes how
many users that is. See anthropics/claude-code#96870.

No process is started, no hook behaves differently, and no other row
changes. The key is the last one of its row, so it never displaces
another.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant