Summary
On Windows, the built-in Read tool fails to render PDFs with:
pdftoppm is not installed. Install poppler-utils (e.g. `brew install poppler` or `apt-get install poppler-utils`) to enable PDF page rendering.
— but only in interactive sessions spawned by the VS Code extension. The exact same native binary, run headless (claude -p) with the same environment, same cwd, and even the same stdio shape (piped stream-json in/out), reads the same PDF successfully. poppler is correctly installed and on PATH, and the session's own Bash/PowerShell tools run pdftoppm fine.
This looks like the Windows executable-discovery path (where.exe-based resolver) failing inside IDE-spawned interactive processes, compounded by the resolver caching the negative result for the lifetime of the process with no retry, so the session can never recover.
Environment
- Windows 10 Pro 10.0.19045
- VS Code extension
anthropic.claude-code-2.1.199-win32-x64 (native binary resources\native-binary\claude.exe, reports 2.1.199 (Claude Code))
- Also tested: npm global
@anthropic-ai/claude-code 2.1.197 (headless — works)
- poppler 26.02.0 (oschwartz10612/poppler-windows) at
%LOCALAPPDATA%\poppler\poppler-26.02.0\Library\bin, registered in HKCU user PATH
- Shell tools in the failing session resolve
pdftoppm fine (Git Bash and PowerShell)
- Corporate antivirus present (considered as a factor; see "Ruled out" — headless descendants of the very same process tree succeed)
What fails vs. what works (all on the same machine, same day, after reboot)
| Scenario |
Result |
| Read tool on a PDF, VS Code extension interactive session (fresh process, spawned by extension host) |
❌ pdftoppm is not installed — persistent, from the very first attempt |
| Same, in a second independent IDE session (also fresh post-reboot process) |
❌ same error |
pdftoppm -v via the session's Bash/PowerShell tools |
✅ exit 0, version on stderr |
Same extension binary run -p headless from a shell: claude.exe -p "Read page 1 of X.pdf" --allowedTools Read |
✅ Read succeeds, page rendered |
Same binary, -p --input-format stream-json --output-format stream-json with piped stdio and IDE-matching flags (--max-thinking-tokens 31999 --setting-sources=user,project,local --permission-mode acceptEdits) |
✅ Read succeeds |
| npm-installed 2.1.197 headless |
✅ Read succeeds |
Evidence that the failing process's runtime context is healthy
Verified against the live failing process (extension-spawned claude.exe, PID inspected from within the session):
- Process environment PATH contains the poppler directory (read directly from the process environment block — not from a child shell, so no profile/snapshot contamination).
- Process CWD is the workspace root on a different drive than poppler, so the resolver's "unsafe location (current directory)" filter cannot apply (candidate is not under cwd).
- Manual replay of every step of the discovery pipeline from the same machine succeeds:
C:\Windows\System32\where.exe pdftoppm → finds ...\poppler-26.02.0\Library\bin\pdftoppm.exe, exit 0; pdftoppm -v → exit 0, 147 bytes on stderr.
- No
where.exe (or pdftoppm.exe) process is created when the Read tool fails (observed with a WMI process-creation watcher polling at 30 ms while triggering Read). Combined with (5), this indicates the resolver returns a cached negative without re-probing.
- From reading the bundled resolver logic in the 2.1.199 binary: on Windows, bare command names are resolved by spawning
where.exe <cmd> with env: process.env, filtering results (lstat-exists, not-under-cwd, extension allowlist .com/.exe/.bat/.cmd), and storing a null result in an in-memory Map. The availability probe path calls the resolver with the "retry negatives" flag set to false, so once a negative is cached, every subsequent PDF read in that session fails instantly and permanently. A failed resolution is synthesized as exit code 127 with the message Command 'pdftoppm' not found or is in an unsafe location (current directory), which the PDF path maps to the generic "pdftoppm is not installed" error — masking the real cause.
Ruled out
- Stale PATH in the process (verified in the process env block, post-reboot).
- CWD-based "unsafe location" filter (cwd is on
E:, poppler on C:).
- Shell-profile-only PATH (poppler is in HKCU registry PATH; no PowerShell profile exists; bash profiles contain no poppler entries).
settings.json env blocks / claudeCode.environmentVariables overrides (none configured anywhere).
- Binary/version difference (the literally same
.exe file works headless).
- stdio shape (piped stream-json headless run also works).
- AV blocking the image chain (headless successes were grandchildren of the failing process itself: failing claude.exe → PowerShell tool → claude.exe headless → where.exe/pdftoppm all fine).
What I could not determine from outside is why the very first where.exe probe fails (or gets filtered) specifically in extension-spawned interactive processes — that needs visibility inside the process (logging around the resolver would answer it immediately).
Suggested fixes
- Don't cache negative resolutions for the process lifetime (or expire them / retry on each new tool invocation). A user who installs poppler mid-session, or hits any transient failure, is currently stuck until the session process restarts — and in the IDE that's opaque.
- Surface the underlying resolution error instead of collapsing everything into "pdftoppm is not installed" (distinguish: not on PATH / filtered as unsafe-location /
where.exe itself failed / spawn error).
- Consider an fs-based PATH+PATHEXT scan fallback when
where.exe yields nothing, and/or log the PATH actually used for the probe at debug level.
Workaround for affected users
Inside the affected IDE session, the Bash tool resolves PATH normally, so:
pdftoppm -png -r 150 -f 1 -l 1 file.pdf /tmp/out # then Read the PNG(s)
pdftotext -layout file.pdf out.txt # when only text is needed
Related
This investigation was performed by Claude Code itself (Fable 5) from inside the affected session — including reading the live process environment/CWD, replaying the discovery pipeline, bisecting IDE vs. headless spawn conditions, and inspecting the bundled resolver logic.
🤖 Generated with Claude Code
Summary
On Windows, the built-in Read tool fails to render PDFs with:
— but only in interactive sessions spawned by the VS Code extension. The exact same native binary, run headless (
claude -p) with the same environment, same cwd, and even the same stdio shape (pipedstream-jsonin/out), reads the same PDF successfully. poppler is correctly installed and on PATH, and the session's own Bash/PowerShell tools runpdftoppmfine.This looks like the Windows executable-discovery path (
where.exe-based resolver) failing inside IDE-spawned interactive processes, compounded by the resolver caching the negative result for the lifetime of the process with no retry, so the session can never recover.Environment
anthropic.claude-code-2.1.199-win32-x64(native binaryresources\native-binary\claude.exe, reports2.1.199 (Claude Code))@anthropic-ai/claude-code2.1.197 (headless — works)%LOCALAPPDATA%\poppler\poppler-26.02.0\Library\bin, registered in HKCU user PATHpdftoppmfine (Git Bash and PowerShell)What fails vs. what works (all on the same machine, same day, after reboot)
pdftoppm is not installed— persistent, from the very first attemptpdftoppm -vvia the session's Bash/PowerShell tools-pheadless from a shell:claude.exe -p "Read page 1 of X.pdf" --allowedTools Read-p --input-format stream-json --output-format stream-jsonwith piped stdio and IDE-matching flags (--max-thinking-tokens 31999 --setting-sources=user,project,local --permission-mode acceptEdits)Evidence that the failing process's runtime context is healthy
Verified against the live failing process (extension-spawned
claude.exe, PID inspected from within the session):C:\Windows\System32\where.exe pdftoppm→ finds...\poppler-26.02.0\Library\bin\pdftoppm.exe, exit 0;pdftoppm -v→ exit 0, 147 bytes on stderr.where.exe(orpdftoppm.exe) process is created when the Read tool fails (observed with a WMI process-creation watcher polling at 30 ms while triggering Read). Combined with (5), this indicates the resolver returns a cached negative without re-probing.where.exe <cmd>withenv: process.env, filtering results (lstat-exists, not-under-cwd, extension allowlist.com/.exe/.bat/.cmd), and storing anullresult in an in-memory Map. The availability probe path calls the resolver with the "retry negatives" flag set tofalse, so once a negative is cached, every subsequent PDF read in that session fails instantly and permanently. A failed resolution is synthesized as exit code 127 with the messageCommand 'pdftoppm' not found or is in an unsafe location (current directory), which the PDF path maps to the generic "pdftoppm is not installed" error — masking the real cause.Ruled out
E:, poppler onC:).settings.jsonenvblocks /claudeCode.environmentVariablesoverrides (none configured anywhere)..exefile works headless).What I could not determine from outside is why the very first
where.exeprobe fails (or gets filtered) specifically in extension-spawned interactive processes — that needs visibility inside the process (logging around the resolver would answer it immediately).Suggested fixes
where.exeitself failed / spawn error).where.exeyields nothing, and/or log the PATH actually used for the probe at debug level.Workaround for affected users
Inside the affected IDE session, the Bash tool resolves PATH normally, so:
Related
This investigation was performed by Claude Code itself (Fable 5) from inside the affected session — including reading the live process environment/CWD, replaying the discovery pipeline, bisecting IDE vs. headless spawn conditions, and inspecting the bundled resolver logic.
🤖 Generated with Claude Code