[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.
This issue has 11 comments on GitHub. Read the full discussion on GitHub ↗