[BUG] (lsp): Malformed file:// URI Generation (v2.1.1+) [RFC-8089]
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?
Product: Claude Code CLI
Affected Versions: 2.1.1, 2.1.0, 2.0.67
Platform: Multiple/all, incl. Windows 11 (native installation and via npm install @anthropic-ai/claude-code@2.1.1)
Component: LSP integration code in @anthropic-ai/claude-code/cli.js (affects at least clangd-lsp)
Severity: High - Prevents clangd LSP functionality
Status: Locally patched and verified 2026-01-07 17:29 EST
Relationship to Previous Versions
The bug persists across versions with different minified symbol names but identical root cause.
Problem Statement
The clangd-lsp plugin generates file URIs that violate RFC 8089 (The "file" URI Scheme) on Windows. This results in malformed URIs being sent to clangd via the Language Server Protocol, causing the error:
LSP request 'textDocument/documentSymbol' failed for server 'plugin:clangd-lsp:clangd': trying to get AST for non-added document
Technical Root Cause
The plugin uses JavaScript template literal string interpolation to construct file URIs instead of Node.js's built-in url.pathToFileURL() function.
Malformed output: file://F:\Projects\path\file.cpp
Correct output: file:///F:/Projects/path/file.cpp
Technical Analysis (v2.1.1)
Minified Symbol Cross-Reference
| Minified Symbol | Real Symbol | Module |
|-----------------|-------------|--------|
| cB7, v37, zG7 | pathToFileURL | url |
Multiple aliases exist for pathToFileURL in v2.1.1. Use whichever is in scope for the code section being patched.
Affected Code Patterns (Minified cli.js)
| Minified Line | Pattern (BROKEN) | Fix |
|---------------|------------------|-----|
| 866 | ` file://${aCB()}/ | cB7(aCB()).href + "/" |
| 2191 | file://${OQ()} | cB7(OQ()).href |
| 2199 | file://${A} | cB7(A).href |
| 2312 | file://${E} | cB7(E).href |
| 2340 | file://${J} | cB7(J).href |
| 2340 | file://${K} | cB7(K).href` |
Fix Implementation (v2.1.1)
Direct Minified Patching
No unminification required. Search and replace directly in cli.js:
# Find all occurrences
grep -oE ".{0,30}file://\\\$\{[^}]+\}.{0,30}" cli.js
Replacement Patterns
| Find | Replace |
|------|---------|
| ` file://${aCB()}/ | cB7(aCB()).href + "/" |
| file://${OQ()} | cB7(OQ()).href |
| file://${A} | cB7(A).href |
| file://${E} | cB7(E).href |
| file://${J} | cB7(J).href |
| file://${K} | cB7(K).href |GW1(
| file://${J}) | GW1(cB7(J).href) |GW1(
| file://${K}) | GW1(cB7(K).href)` |
Verification
After restarting Claude Code:
LSP documentSymbol returned 95 symbols from [redacted].cpp
Installation Path Note (fnm users)
For users with fnm (Fast Node Manager), the shell-specific paths like:
C:\Users\<user>\AppData\Local\fnm_multishells\<pid>_<timestamp>\
Are junctions pointing to:
C:\Users\<user>\AppData\Roaming\fnm\node-versions\v<version>\installation\
Patch the file at the real path, not the junction.
Files Referenced
clangd-wrapper.go- Go wrapper for logging LSP messages
clangd-wrapper.go Usage
- Close Claude Code
- Rename C:\Program Files\LLVM\bin\clangd.exe to C:\Program Files\LLVM\bin\clangd-real.exe
- Drop clangd-wrapper.exe as C:\Program Files\LLVM\bin\clangd.exe
- Launch Claude Code
- Execute LSP on a C++ target
- Examine C:\Users\<user>\.claude\debug\clangd-messages.log to confirm actual uris passed to clangd
- Ensure they are well formatted
Recommendations for Upstream Fix
- Search source code for all instances of `
file://${`` pattern - Replace with
pathToFileURL(...).hreffrom Node.jsurlmodule - Add Windows CI tests that validate URI format compliance
- Consider a shared utility function for file URI generation
References
- RFC 8089 - The "file" URI Scheme
- Node.js url.pathToFileURL() Documentation
- LSP Specification - Document URI
clangd-wrapper.go Source Code
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
)
func main() {
// Log file
homeDir, _ := os.UserHomeDir()
logFile := filepath.Join(homeDir, ".claude", "debug", "clangd-messages.log")
// Start real clangd
cmd := exec.Command(`C:\Program Files\LLVM\bin\clangd-real.exe`, os.Args[1:]...)
stdin, _ := cmd.StdinPipe()
stdout, _ := cmd.StdoutPipe()
cmd.Stderr = os.Stderr
cmd.Start()
// Forward stdin to clangd, intercepting messages
go func() {
buffer := make([]byte, 0)
readBuf := make([]byte, 4096)
for {
n, err := os.Stdin.Read(readBuf)
if err != nil {
break
}
chunk := readBuf[:n]
buffer = append(buffer, chunk...)
// Try to extract complete LSP messages
for {
msg, rest := extractLSPMessage(buffer)
if msg == nil {
break
}
buffer = rest
// Log interesting messages
if parsed := parseJSON(msg); parsed != nil {
if method, ok := parsed["method"].(string); ok {
if method == "textDocument/didOpen" || method == "textDocument/documentSymbol" {
logMessage(logFile, "TO_CLANGD", parsed)
}
}
}
}
// Forward all data to clangd
stdin.Write(chunk)
}
}()
// Forward clangd output to stdout
io.Copy(os.Stdout, stdout)
cmd.Wait()
}
func extractLSPMessage(data []byte) ([]byte, []byte) {
// Find header end
headerEnd := bytes.Index(data, []byte("\r\n\r\n"))
if headerEnd == -1 {
return nil, data
}
// Parse Content-Length
header := string(data[:headerEnd])
var contentLength int
for _, line := range strings.Split(header, "\r\n") {
if strings.HasPrefix(line, "Content-Length: ") {
contentLength, _ = strconv.Atoi(strings.TrimPrefix(line, "Content-Length: "))
break
}
}
if contentLength == 0 {
return nil, data[headerEnd+4:]
}
messageEnd := headerEnd + 4 + contentLength
if len(data) < messageEnd {
return nil, data
}
message := data[headerEnd+4 : messageEnd]
return message, data[messageEnd:]
}
func parseJSON(data []byte) map[string]interface{} {
var parsed map[string]interface{}
if err := json.Unmarshal(data, &parsed); err != nil {
return nil
}
return parsed
}
func logMessage(logFile string, direction string, message map[string]interface{}) {
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return
}
defer f.Close()
timestamp := time.Now().Format(time.RFC3339)
jsonBytes, _ := json.MarshalIndent(message, "", " ")
fmt.Fprintf(f, "\n%s [%s]\n%s\n", timestamp, direction, string(jsonBytes))
}
What Should Happen?
Code should use pathToFileURL() consistently to avoid passing corrupt file URIs to clangd.
Error Messages/Logs
LSP request 'textDocument/documentSymbol' failed for server 'plugin:clangd-lsp:clangd': trying to get AST for non-added document
Steps to Reproduce
Attempt to use LSP on a clangd-lsp with a valid (e.g., c++) project.
Claude Model
Opus
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.1 (Claude Code)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Windows Terminal
Additional Information
See go source code in bug report to create a clangd.exe proxy which logs malformed inputs and can confirm fix.
11 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Alternative Workaround: External URI-fixing Wrappers
Found the same root cause independently. Here's an alternative approach that doesn't require patching cli.js (survives Claude Code updates):
Approach: Node.js wrapper scripts that sit between Claude Code and the LSP servers, intercepting JSON-RPC messages and fixing malformed URIs before forwarding.
How it works:
uri,rootUri,documentUri, etc.)file://c:\path\→file:///C:/pathTested working with:
| LSP | Version |
|-----|---------|
| typescript-language-server | 5.1.3 |
| pyright-langserver | latest |
| clangd | latest |
| csharp-ls | 0.13.0 |
Setup:
~\.local\bin(must be first in PATH)Full guide + scripts: https://gist.github.com/dlprentice/20708af282490773f0dd28c15b86d90d
Trade-offs vs cli.js patching:
| | cli.js patch | External wrapper |
|---|---|---|
| Survives updates | ❌ | ✅ |
| Zero overhead | ✅ | ~negligible |
| Setup complexity | Find minified symbols | One-time shimgen setup |
Both approaches work. Sharing this as an option for those who prefer not to patch internals.
Confirming that workaround is good and 2.1.3 still impacted.
This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.
Still an issue for me. Claude Code v2.1.38
Please consider this bug as independent of Windows and
clangd. Any OS and LSP server are affected. For example, if a file or folder path contains a space (or another character needed escaping according to RFC-1738), it is sent as is. It happens on Mac OS and Linux too. See, for example, this:https://github.com/AdaCore/ada_language_server/issues/1294
RFC-1738 states:
To reproduce on Linux/Mac OS
mkdir "/tmp/hello world")claudein the folder and ask for LSP related informationPS Claude Code v2.1.52
Fair statement, I updated the bug title accordingly, though I kept (and uplifted) the reference to RFC 8089 which ~~supersedes~~ updates RFC 1738.
Additional findings: Pyright (Python LSP) — URI mismatch between didOpen and request params
Claude Code v2.1.73, Windows 11, Pyright 1.1.408
The bug manifests in a subtle way with Pyright that may not be immediately obvious:
Diagnostics work perfectly — because Pyright discovers files through its own workspace scan and generates correct URIs internally.
All query operations return empty (documentSymbol, hover, references, etc.) — but the error messages say "No symbols found" or "No hover information", NOT "No LSP server available". This makes it look like a Pyright issue rather than a URI issue.
Root cause: URI mismatch within Claude Code itself
The LSP tool handler (
NSYfunction) correctly usespathToFileURL()to build request URIs:But the LSP manager's
openFile/isFileOpen/closeFilemethods use broken template literals:This creates a URI mismatch:
textDocument/didOpenis sent withfile://C:\...buttextDocument/documentSymbolis sent withfile:///C:/.... Pyright doesn't associate them — it has the file content under one URI but receives the query for a different URI.Verified fix
Replacing all 5 instances of
file://${rc.resolve(X)}and 1 instance of `file://${W}` (workspace URI in server init) with proper Windows-safe URIs fixes all operations:After patching:
documentSymbolreturns 428 symbols,hoverreturns full type info and docstrings. Confirmed working.Proper fix suggestion
All 6 occurrences in the LSP manager should use
pathToFileURL(path).href(already imported asGSYin the tool handler module) instead of template literal string concatenation. This would also fix the space/special character issue mentioned by @reznikmm.Doing a little bump since this still seems to be a thing and the 'stale' GitHub tag concerns me.
Closing for now — inactive for too long. Please open a new issue if this is still relevant.
Why offer an LSP for c++ as a plugin if there is no plan to fix this. Proper implementation would reduce token usage which is also a win for Anthropic.