[BUG] (lsp): Malformed file:// URI Generation (v2.1.1+) [RFC-8089]

Status Closed — not planned
Reported on v2.1.1
Maintainer reply None cached
Activity 11 comments · opened Jan 7, 2026 · closed May 16, 2026

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

  1. Close Claude Code
  2. Rename C:\Program Files\LLVM\bin\clangd.exe to C:\Program Files\LLVM\bin\clangd-real.exe
  3. Drop clangd-wrapper.exe as C:\Program Files\LLVM\bin\clangd.exe
  4. Launch Claude Code
  5. Execute LSP on a C++ target
  6. Examine C:\Users\<user>\.claude\debug\clangd-messages.log to confirm actual uris passed to clangd
  7. Ensure they are well formatted

Recommendations for Upstream Fix

  1. Search source code for all instances of ` file://${`` pattern
  2. Replace with pathToFileURL(...).href from Node.js url module
  3. Add Windows CI tests that validate URI format compliance
  4. Consider a shared utility function for file URI generation

References

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.

View original on GitHub ↗

11 Comments

github-actions[bot] · 7 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/3381
  2. https://github.com/anthropics/claude-code/issues/1522
  3. https://github.com/anthropics/claude-code/issues/15914

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

dlprentice · 7 months ago

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:

  1. Wrapper receives LSP messages from Claude Code
  2. Recursively finds URI fields (uri, rootUri, documentUri, etc.)
  3. Fixes file://c:\path\file:///C:/path
  4. Forwards corrected messages to real LSP server
  5. Passes responses back unchanged

Tested working with:

| LSP | Version |
|-----|---------|
| typescript-language-server | 5.1.3 |
| pyright-langserver | latest |
| clangd | latest |
| csharp-ls | 0.13.0 |

Setup:

  • Requires Node.js + Chocolatey (for shimgen to create .exe shims)
  • Automated setup script included
  • Shims go in ~\.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.

pedropaulovc · 7 months ago

Confirming that workaround is good and 2.1.3 still impacted.

github-actions[bot] · 6 months ago

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.

justcfx2u · 6 months ago
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

reznikmm · 6 months ago

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:

Octets must be encoded if they have no corresponding graphic character within the US-ASCII coded character set, if the use of the corresponding character is unsafe, or if the corresponding character is reserved for some other interpretation within the particular URL scheme.

To reproduce on Linux/Mac OS

  • create a folder containing a space in its name (like mkdir "/tmp/hello world")
  • launch claude in the folder and ask for LSP related information
  • see plain space in URLs (in LSP trace/log) like this:
{
 "jsonrpc":"2.0",
 "id":0,
 "method":"initialize",
 "params":{
  "rootUri":"file:///tmp/hello world"
...

PS Claude Code v2.1.52

justcfx2u · 6 months ago
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: AdaCore/ada_language_server#1294 RFC-1738 states:

Fair statement, I updated the bug title accordingly, though I kept (and uplifted) the reference to RFC 8089 which ~~supersedes~~ updates RFC 1738.

f1rstmann · 5 months ago

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 (NSY function) correctly uses pathToFileURL() to build request URIs:

let K = GSY(q).href;  // file:///C:/Projects/... (CORRECT)

But the LSP manager's openFile/isFileOpen/closeFile methods use broken template literals:

let Z = `file://${rc.resolve(X)}`;  // file://C:\Projects\... (WRONG)

This creates a URI mismatch: textDocument/didOpen is sent with file://C:\... but textDocument/documentSymbol is sent with file:///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:

Before: file://${rc.resolve(X)}
After:  file:///${rc.resolve(X).split("\\").join("/")}

After patching: documentSymbol returns 428 symbols, hover returns full type info and docstrings. Confirmed working.

Proper fix suggestion

All 6 occurrences in the LSP manager should use pathToFileURL(path).href (already imported as GSY in the tool handler module) instead of template literal string concatenation. This would also fix the space/special character issue mentioned by @reznikmm.

justcfx2u · 4 months ago

Doing a little bump since this still seems to be a thing and the 'stale' GitHub tag concerns me.

github-actions[bot] · 3 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

mattmcde78 · 2 months ago

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.