-
-
Save Zamua/f7ca58ce5dd9ba61279ea195a01b190c to your computer and use it in GitHub Desktop.
| #!/bin/bash | |
| # | |
| # Claude Code LSP Fix | |
| # ==================== | |
| # Fixes the LSP plugin bug: https://github.com/anthropics/claude-code/issues/13952 | |
| # | |
| # THE BUG: | |
| # Claude Code's LSP manager has an empty initialize() function that should | |
| # load and register LSP servers from plugins, but instead does nothing. | |
| # This causes "No LSP server available for file type" errors. | |
| # | |
| # THE FIX: | |
| # This script patches the empty initialize() function to actually: | |
| # 1. Load LSP server configs from enabled plugins | |
| # 2. Create server instances for each config | |
| # 3. Register them so Claude Code can use them | |
| # | |
| # HOW IT WORKS: | |
| # Uses the acorn JavaScript parser to find the right functions by their | |
| # structure and string contents (not minified names, which vary between builds). | |
| # This makes the fix reliable across different installations. | |
| # | |
| # USAGE: | |
| # ./apply-claude-code-2.0.76-lsp-fix.sh # Apply the fix | |
| # ./apply-claude-code-2.0.76-lsp-fix.sh --check # Check if fix is needed | |
| # ./apply-claude-code-2.0.76-lsp-fix.sh --restore # Restore from backup | |
| # ./apply-claude-code-2.0.76-lsp-fix.sh --fix-plugins # Fix plugin configs only | |
| # ./apply-claude-code-2.0.76-lsp-fix.sh --help # Show this help | |
| # | |
| # REQUIREMENTS: | |
| # - Node.js (already installed if you have Claude Code) | |
| # - Internet connection (downloads acorn parser on first run) | |
| # - npm-based Claude Code installation (not the standalone binary) | |
| # The official install script creates a Mach-O binary that can't be patched. | |
| # Install via npm instead: npm install -g @anthropic-ai/claude-code | |
| # | |
| # NOTE: | |
| # This patch will be overwritten when Claude Code updates. | |
| # Re-run this script after updates if LSP stops working. | |
| # | |
| set -e | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m' | |
| NC='\033[0m' | |
| print_status() { echo -e "${GREEN}β${NC} $1"; } | |
| print_warning() { echo -e "${YELLOW}!${NC} $1"; } | |
| print_error() { echo -e "${RED}β${NC} $1"; } | |
| print_info() { echo -e "${BLUE}β${NC} $1"; } | |
| # Fix plugin marketplace configs (removes unsupported fields like startupTimeout) | |
| fix_plugin_configs() { | |
| local marketplace_json="$HOME/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json" | |
| if [ ! -f "$marketplace_json" ]; then | |
| return 0 | |
| fi | |
| # Use Node.js for proper JSON manipulation | |
| node -e " | |
| const fs = require('fs'); | |
| const path = '$marketplace_json'; | |
| let data; | |
| try { | |
| data = JSON.parse(fs.readFileSync(path, 'utf8')); | |
| } catch (e) { | |
| console.log('\x1b[33m!\x1b[0m Plugin config JSON is invalid, skipping'); | |
| process.exit(0); | |
| } | |
| let fixes = []; | |
| const unsupportedFields = ['startupTimeout', 'shutdownTimeout']; | |
| if (data.plugins) { | |
| for (const plugin of data.plugins) { | |
| if (plugin.lspServers) { | |
| for (const [serverName, config] of Object.entries(plugin.lspServers)) { | |
| for (const field of unsupportedFields) { | |
| if (config[field] !== undefined) { | |
| delete config[field]; | |
| fixes.push({ plugin: plugin.name, server: serverName, field }); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| if (fixes.length > 0) { | |
| // Backup and write | |
| fs.copyFileSync(path, path + '.backup'); | |
| fs.writeFileSync(path, JSON.stringify(data, null, 2)); | |
| for (const fix of fixes) { | |
| console.log('\x1b[32mβ\x1b[0m Fixed ' + fix.plugin + ': removed unsupported \"' + fix.field + '\" from ' + fix.server); | |
| } | |
| } else { | |
| console.log('\x1b[32mβ\x1b[0m Plugin configs already clean'); | |
| } | |
| " | |
| } | |
| # Show help | |
| if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then | |
| sed -n '3,39p' "$0" | sed 's/^#//' | sed 's/^ //' | |
| exit 0 | |
| fi | |
| # Handle --fix-plugins (standalone) | |
| if [ "$1" = "--fix-plugins" ]; then | |
| echo "Fixing plugin configs..." | |
| fix_plugin_configs | |
| exit 0 | |
| fi | |
| # Find Claude Code cli.js in common installation locations | |
| find_cli_path() { | |
| local locations=( | |
| "$HOME/.claude/local/node_modules/@anthropic-ai/claude-code/cli.js" | |
| "/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js" | |
| "/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js" | |
| "$(npm root -g 2>/dev/null)/@anthropic-ai/claude-code/cli.js" | |
| ) | |
| for path in "${locations[@]}"; do | |
| if [ -f "$path" ]; then | |
| echo "$path" | |
| return 0 | |
| fi | |
| done | |
| return 1 | |
| } | |
| CLI_PATH=$(find_cli_path) || true | |
| if [ -z "$CLI_PATH" ]; then | |
| print_error "Claude Code cli.js not found" | |
| echo "" | |
| echo "Searched:" | |
| echo " ~/.claude/local/node_modules/@anthropic-ai/claude-code/cli.js" | |
| echo " /usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js" | |
| echo " /usr/lib/node_modules/@anthropic-ai/claude-code/cli.js" | |
| echo " \$(npm root -g)/@anthropic-ai/claude-code/cli.js" | |
| echo "" | |
| echo "This script only works with npm-based installations." | |
| echo "If you installed via the official install script (standalone binary)," | |
| echo "reinstall using: npm install -g @anthropic-ai/claude-code" | |
| exit 1 | |
| fi | |
| # Handle --restore | |
| if [ "$1" = "--restore" ]; then | |
| restored=0 | |
| # Restore cli.js | |
| LATEST_BACKUP=$(ls -t "${CLI_PATH}.backup-"* 2>/dev/null | head -1) | |
| if [ -n "$LATEST_BACKUP" ]; then | |
| cp "$LATEST_BACKUP" "$CLI_PATH" | |
| print_status "Restored cli.js from: $LATEST_BACKUP" | |
| restored=1 | |
| fi | |
| # Restore plugin config | |
| MARKETPLACE_JSON="$HOME/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json" | |
| if [ -f "${MARKETPLACE_JSON}.backup" ]; then | |
| cp "${MARKETPLACE_JSON}.backup" "$MARKETPLACE_JSON" | |
| print_status "Restored plugin config from backup" | |
| restored=1 | |
| fi | |
| if [ $restored -eq 0 ]; then | |
| print_error "No backups found" | |
| exit 1 | |
| fi | |
| exit 0 | |
| fi | |
| print_info "Found Claude Code at: $CLI_PATH" | |
| echo "" | |
| # Download acorn JS parser if needed (cached in /tmp) | |
| ACORN_PATH="/tmp/acorn-claude-fix.js" | |
| if [ ! -f "$ACORN_PATH" ]; then | |
| print_info "Downloading acorn parser..." | |
| if ! curl -sf https://unpkg.com/acorn@8.14.0/dist/acorn.js -o "$ACORN_PATH"; then | |
| print_error "Failed to download acorn parser" | |
| exit 1 | |
| fi | |
| fi | |
| # Create the Node.js patch script | |
| PATCH_SCRIPT="/tmp/claude-lsp-patch-$$.js" | |
| cat > "$PATCH_SCRIPT" << 'NODESCRIPT' | |
| const fs = require('fs'); | |
| const acorn = require('/tmp/acorn-claude-fix.js'); | |
| const cliPath = process.argv[2]; | |
| const checkOnly = process.argv[3] === '--check'; | |
| let code = fs.readFileSync(cliPath, 'utf-8'); | |
| // Strip shebang for parsing (will restore later) | |
| let shebang = ''; | |
| if (code.startsWith('#!')) { | |
| const idx = code.indexOf('\n'); | |
| shebang = code.slice(0, idx + 1); | |
| code = code.slice(idx + 1); | |
| } | |
| // Check if already patched (look for our unique variable names) | |
| if (code.includes('let{servers:_S}=await') && code.includes('.set(_N,_I)')) { | |
| console.log('\x1b[32mβ\x1b[0m Already patched'); | |
| process.exit(2); // Exit 2 = already patched, no changes | |
| } | |
| // Parse the JavaScript | |
| let ast; | |
| try { | |
| ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: 'module' }); | |
| } catch (e) { | |
| console.error('\x1b[31mβ\x1b[0m Failed to parse cli.js:', e.message); | |
| process.exit(1); | |
| } | |
| // --- AST Helpers --- | |
| // Get source text for a node | |
| const src = (node) => code.slice(node.start, node.end); | |
| // Recursively find all nodes matching a predicate | |
| function findNodes(node, predicate, results = []) { | |
| if (!node || typeof node !== 'object') return results; | |
| if (predicate(node)) results.push(node); | |
| for (const key in node) { | |
| if (node[key] && typeof node[key] === 'object') { | |
| if (Array.isArray(node[key])) { | |
| node[key].forEach(child => findNodes(child, predicate, results)); | |
| } else { | |
| findNodes(node[key], predicate, results); | |
| } | |
| } | |
| } | |
| return results; | |
| } | |
| // Check if node contains a string literal matching text | |
| function containsString(node, text) { | |
| const strings = findNodes(node, n => n.type === 'Literal' && typeof n.value === 'string'); | |
| return strings.some(s => s.value.includes(text)); | |
| } | |
| // Check if node contains a template literal matching text | |
| function containsTemplate(node, text) { | |
| const templates = findNodes(node, n => n.type === 'TemplateLiteral'); | |
| return templates.some(t => t.quasis.map(q => q.value.raw).join('').includes(text)); | |
| } | |
| // --- Find the functions we need to patch --- | |
| const allFunctions = findNodes(ast, n => | |
| n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression' | |
| ); | |
| // 1. Find createLspServer() - contains "Starting LSP server instance" | |
| let createServerFunc = null; | |
| for (const fn of allFunctions) { | |
| if (containsString(fn, 'Starting LSP server instance') || containsTemplate(fn, 'Starting LSP server instance')) { | |
| createServerFunc = fn; | |
| break; | |
| } | |
| } | |
| if (!createServerFunc) { | |
| console.error('\x1b[31mβ\x1b[0m Could not find createLspServer function'); | |
| console.error(' (looking for function containing "Starting LSP server instance")'); | |
| process.exit(1); | |
| } | |
| const createServerName = createServerFunc.id?.name; | |
| console.log('\x1b[34mβ\x1b[0m Found createLspServer:', createServerName); | |
| // 2. Find loadLspServersFromPlugins() - contains "Loaded" + "LSP server" log | |
| let loadServersFunc = null; | |
| for (const fn of allFunctions) { | |
| const hasLoaded = containsString(fn, 'Loaded') || containsTemplate(fn, 'Loaded'); | |
| const hasLsp = containsString(fn, 'LSP server') || containsTemplate(fn, 'LSP server'); | |
| if (hasLoaded && hasLsp) { | |
| loadServersFunc = fn; | |
| break; | |
| } | |
| } | |
| if (!loadServersFunc) { | |
| console.error('\x1b[31mβ\x1b[0m Could not find loadLspServersFromPlugins function'); | |
| console.error(' (looking for function containing "Loaded" + "LSP server")'); | |
| process.exit(1); | |
| } | |
| const loadServersName = loadServersFunc.id?.name; | |
| console.log('\x1b[34mβ\x1b[0m Found loadLspServersFromPlugins:', loadServersName); | |
| // 3. Find LSP manager factory - has 3 Maps and an empty async initialize() | |
| let lspManagerFunc = null; | |
| let emptyInitFunc = null; | |
| let mapVars = []; | |
| for (const fn of allFunctions) { | |
| // Look for "new Map()" variable declarations | |
| const varDecls = findNodes(fn, n => n.type === 'VariableDeclaration'); | |
| const mapInits = []; | |
| for (const decl of varDecls) { | |
| for (const d of decl.declarations) { | |
| if (d.init?.type === 'NewExpression' && d.init.callee?.name === 'Map') { | |
| mapInits.push(d.id.name); | |
| } | |
| } | |
| } | |
| // LSP manager has 3+ Maps | |
| if (mapInits.length >= 3) { | |
| // Find empty async function inside (the buggy initialize) | |
| const asyncFuncs = findNodes(fn, n => n.type === 'FunctionDeclaration' && n.async); | |
| for (const inner of asyncFuncs) { | |
| const body = inner.body?.body; | |
| // Empty body or just "return" with no value | |
| if (body?.length === 0 || | |
| (body?.length === 1 && body[0].type === 'ReturnStatement' && !body[0].argument)) { | |
| lspManagerFunc = fn; | |
| emptyInitFunc = inner; | |
| mapVars = mapInits; | |
| break; | |
| } | |
| } | |
| } | |
| if (lspManagerFunc) break; | |
| } | |
| if (!lspManagerFunc || !emptyInitFunc) { | |
| console.error('\x1b[31mβ\x1b[0m Could not find LSP manager with empty initialize()'); | |
| console.error(' (looking for function with 3 Maps + empty async function)'); | |
| process.exit(1); | |
| } | |
| const initFuncName = emptyInitFunc.id?.name; | |
| const serverMap = mapVars[0]; // First map stores servers | |
| const extMap = mapVars[1]; // Second map stores extension->server mappings | |
| console.log('\x1b[34mβ\x1b[0m Found empty initialize():', initFuncName); | |
| console.log('\x1b[34mβ\x1b[0m Server registry map:', serverMap); | |
| console.log('\x1b[34mβ\x1b[0m Extension map:', extMap); | |
| if (checkOnly) { | |
| console.log(''); | |
| console.log('\x1b[33m!\x1b[0m Patch needed - run without --check to apply'); | |
| process.exit(1); | |
| } | |
| // --- Build and apply the patch --- | |
| // The fix: make initialize() actually load and register LSP servers | |
| const newInitBody = `async function ${initFuncName}(){` + | |
| `let{servers:_S}=await ${loadServersName}();` + | |
| `for(let[_N,_C]of Object.entries(_S)){` + | |
| `let _I=${createServerName}(_N,_C);` + | |
| `${serverMap}.set(_N,_I);` + | |
| `for(let[_E,_L]of Object.entries(_C.extensionToLanguage||{})){` + | |
| `let _M=${extMap}.get(_E)||[];` + | |
| `_M.push(_N);` + | |
| `${extMap}.set(_E,_M)` + | |
| `}` + | |
| `}` + | |
| `}`; | |
| // Apply patch (preserve shebang) | |
| const newCode = shebang + code.slice(0, emptyInitFunc.start) + newInitBody + code.slice(emptyInitFunc.end); | |
| // Backup original | |
| const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); | |
| const backupPath = cliPath + '.backup-' + timestamp; | |
| fs.copyFileSync(cliPath, backupPath); | |
| console.log(''); | |
| console.log('Backup:', backupPath); | |
| // Write patched file | |
| fs.writeFileSync(cliPath, newCode); | |
| // Verify | |
| if (fs.readFileSync(cliPath, 'utf-8').includes(newInitBody)) { | |
| console.log(''); | |
| console.log('\x1b[32mβ\x1b[0m Fix applied successfully!'); | |
| } else { | |
| console.error('\x1b[31mβ\x1b[0m Verification failed, restoring backup...'); | |
| fs.copyFileSync(backupPath, cliPath); | |
| process.exit(1); | |
| } | |
| NODESCRIPT | |
| # Run the patch script | |
| node "$PATCH_SCRIPT" "$CLI_PATH" "$1" | |
| EXIT_CODE=$? | |
| # Cleanup temp script | |
| rm -f "$PATCH_SCRIPT" | |
| # Exit codes: 0 = patched, 1 = error/check-needed, 2 = already patched | |
| if [ $EXIT_CODE -eq 0 ] && [ "$1" != "--check" ]; then | |
| # Newly patched - fix plugins and show restart message | |
| echo "" | |
| fix_plugin_configs | |
| echo "" | |
| print_warning "Restart Claude Code for changes to take effect" | |
| elif [ $EXIT_CODE -eq 2 ]; then | |
| # Already patched - just exit cleanly | |
| exit 0 | |
| fi | |
| exit $EXIT_CODE |
do you mind sharing your entire cli.js file with me? you can share it however you like (gist, pastebin, etc)
Here's my cli.js (2.0.76, installed via asdf/npm):
https://gist.github.com/mkreyman/e2b026059891b98e60429060dc8023e4
thanks! i updated the script; do you mind trying again @mkreyman ?
Updated script works! π
Found all functions and patched successfully:
- createLspServer: T52
- loadLspServersFromPlugins: v52
- empty initialize(): G
- Server registry map: A
- Extension map: Q
After restart, the elixir-lsp plugin is now functional - LSP documentSymbol returns proper results. The skills from the elixir plugin are also available.
Thanks for the quick iteration!
π₯³
Is this related to the script or some Claude code issue?
LSP server 'plugin:jdtls-lsp:jdtls': startupTimeout is not yet implemented. Remove this field from the configuration.
@52617365 hmmm that error message comes from claude code. it seems to be a bug because the startupTimeout config is only present for the jdtls plugin, but the claude code cli has explicit validation for not supporting this config with the message you saw. i was able to reproduce it locally
i got it fixed by updating my config for that plugin. can you try updating ~/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json (or whatever path yours is at) to remove startupTimeout": 120000? this is the patch
diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index c11e644..0a4755d 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -204,7 +204,7 @@
"extensionToLanguage": {
".java": "java"
},
- "startupTimeout": 120000
+
}
}
},
i can see about updating the script to auto-fix this as well
That fixed it, thank you a lot!
π₯³
Iβm sorry to ask but how do I use this?
Iβve installed PHP LSP via the /plugin command.
Iβm sorry to ask but how do I use this?
Iβve installed PHP LSP via the /plugin command.
not a problem, copy the script either by copy/paste or git clone, give it execute permissions, run it. here's an example on my computer
β― git clone https://gist.github.com/Zamua/f7ca58ce5dd9ba61279ea195a01b190c
Cloning into 'f7ca58ce5dd9ba61279ea195a01b190c'...
remote: Enumerating objects: 21, done.
remote: Counting objects: 100% (21/21), done.
remote: Compressing objects: 100% (21/21), done.
remote: Total 21 (delta 5), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (21/21), 8.51 KiB | 8.51 MiB/s, done.
Resolving deltas: 100% (5/5), done.
β― cd f7ca58ce5dd9ba61279ea195a01b190c
β― chmod +x apply-claude-code-2.0.76-lsp-fix.sh
β― ./apply-claude-code-2.0.76-lsp-fix.sh
β Found Claude Code at: /Users/zamua/.claude/local/node_modules/@anthropic-ai/claude-code/cli.js
β Already patched
Thanks for your fix!
I got this issue, not sure if anyone else got the similar one
> I started another session and got this when I run doctor command
Plugin Errors
β 1 plugin error(s) detected:
β lsp-manager: LSP server 'plugin:jdtls-lsp:jdtls': startupTimeout is not yet
implemented. Remove this field from the configuration.
βΊ The marketplace config was likely refreshed. Let me fix the startupTimeout field again:
βΊ Bash(/tmp/apply-claude-code-lsp-fix.sh --fix-plugins)
βΏ Β Fixing plugin configs...
β Fixed jdtls-lsp: removed unsupported "startupTimeout" from jdtls
βΊ Fixed. The startupTimeout field was restored because the marketplace config gets refreshed periodically.
β
Insight βββββββββββββββββββββββββββββββββββββ
Why this keeps happening:
1. Claude Code refreshes marketplace configs from the remote repository
2. The upstream marketplace.json still contains startupTimeout: 120000
3. Each refresh overwrites the local fix
Workaround options:
- Run --fix-plugins after marketplace refreshes
- Or add a hook to auto-fix on session start
βββββββββββββββββββββββββββββββββββββββββββββββββ
Would you like me to create a session-start hook that automatically removes unsupported LSP config fields?
hmmmmmm ok, so are you saying the script worked, but then it reverted back to being broken again after a while? i don't have much more info than claude does there. not sure what would cause it to recent the plugin config, but if re-running the script fixes it, i guess you'll need to just keep doing that when it breaks π
let me know if i'm misunderstanding your issue
Hi @Zamua, thanks for this helpful script!
I encountered an issue: the script exits silently without any output on my machine.
After debugging with bash -x, I found the cause: I installed Claude Code via the official installation script (not npm), which installs a standalone Mach-O binary instead of a Node.js package.
$ which claude
/Users/xxx/.local/bin/claude
$ file /Users/xxx/.local/bin/claude
/Users/xxx/.local/bin/claude: Mach-O 64-bit executable arm64Since there's no cli.js file in this installation method, the find_cli_path() function returns 1, and set -e causes the script to exit immediately before printing the error message.
Suggestions:
- Maybe add a note in the script header that this only works for npm installations
- Or change
CLI_PATH=$(find_cli_path)toCLI_PATH=$(find_cli_path) || trueso the friendly error message can be displayed
Hope this helps others who might encounter the same issue!
@Viacol0916 ahhh thanks for that info! i've updated the script
hmmmmmm ok, so are you saying the script worked, but then it reverted back to being broken again after a while? i don't have much more info than claude does there. not sure what would cause it to recent the plugin config, but if re-running the script fixes it, i guess you'll need to just keep doing that when it breaks π
let me know if i'm misunderstanding your issue
yeah, it works but have to run the script whenever I start a new CC session in the different folder.
i am in windows system , error message is as follows
$ sh apply-claude-code-2.0.76-lsp-fix.sh
β Found Claude Code at: C:
vm4w
odejs
ode_modules/@anthropic-ai/claude-code/cli.js
β Downloading acorn parser...
node:internal/modules/cjs/loader:1228
throw err;
^
Error: Cannot find module '/tmp/acorn-claude-fix.js'
Require stack:
- C:\Users\SUNJIE
1\AppData\Local\Temp\claude-lsp-patch-2250.js1\AppData\Local\Temp\claude-lsp-patch-2250.js:2:15)
at Function._resolveFilename (node:internal/modules/cjs/loader:1225:15)
at Function._load (node:internal/modules/cjs/loader:1055:27)
at TracingChannel.traceSync (node:diagnostics_channel:322:14)
at wrapModuleLoad (node:internal/modules/cjs/loader:220:24)
at Module.require (node:internal/modules/cjs/loader:1311:12)
at require (node:internal/modules/helpers:136:16)
at Object. (C:\Users\SUNJIE
at Module._compile (node:internal/modules/cjs/loader:1554:14)
at Object..js (node:internal/modules/cjs/loader:1706:10)
at Module.load (node:internal/modules/cjs/loader:1289:32) {
code: 'MODULE_NOT_FOUND',
requireStack: [
'C:\Users\SUNJIE~1\AppData\Local\Temp\claude-lsp-patch-2250.js'
]
}
Node.js v22.14.0
Here you go - script that works if you installed with bunjs
#!/bin/bash
#
# Claude Code LSP Fix
# ====================
# Fixes the LSP plugin bug: https://github.com/anthropics/claude-code/issues/13952
#
# THE BUG:
# Claude Code's LSP manager has an empty initialize() function that should
# load and register LSP servers from plugins, but instead does nothing.
# This causes "No LSP server available for file type" errors.
#
# THE FIX:
# This script patches the empty initialize() function to actually:
# 1. Load LSP server configs from enabled plugins
# 2. Create server instances for each config
# 3. Register them so Claude Code can use them
#
# HOW IT WORKS:
# Uses the acorn JavaScript parser to find the right functions by their
# structure and string contents (not minified names, which vary between builds).
# This makes the fix reliable across different installations.
#
# USAGE:
# ./apply-claude-code-2.0.76-lsp-fix.sh # Apply the fix
# ./apply-claude-code-2.0.76-lsp-fix.sh --check # Check if fix is needed
# ./apply-claude-code-2.0.76-lsp-fix.sh --restore # Restore from backup
# ./apply-claude-code-2.0.76-lsp-fix.sh --fix-plugins # Fix plugin configs only
# ./apply-claude-code-2.0.76-lsp-fix.sh --help # Show this help
#
# REQUIREMENTS:
# - Node.js (already installed if you have Claude Code)
# - Internet connection (downloads acorn parser on first run)
# - npm or bun-based Claude Code installation (not the standalone binary)
# The official install script creates a Mach-O binary that can't be patched.
# Install via bun instead: bun add -g @anthropic-ai/claude-code
#
# NOTE:
# This patch will be overwritten when Claude Code updates.
# Re-run this script after updates if LSP stops working.
#
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${GREEN}β${NC} $1"; }
print_warning() { echo -e "${YELLOW}!${NC} $1"; }
print_error() { echo -e "${RED}β${NC} $1"; }
print_info() { echo -e "${BLUE}β${NC} $1"; }
# Fix plugin marketplace configs (removes unsupported fields like startupTimeout)
fix_plugin_configs() {
local marketplace_json="$HOME/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json"
if [ ! -f "$marketplace_json" ]; then
return 0
fi
# Use Node.js for proper JSON manipulation
node -e "
const fs = require('fs');
const path = '$marketplace_json';
let data;
try {
data = JSON.parse(fs.readFileSync(path, 'utf8'));
} catch (e) {
console.log('\x1b[33m!\x1b[0m Plugin config JSON is invalid, skipping');
process.exit(0);
}
let fixes = [];
const unsupportedFields = ['startupTimeout', 'shutdownTimeout'];
if (data.plugins) {
for (const plugin of data.plugins) {
if (plugin.lspServers) {
for (const [serverName, config] of Object.entries(plugin.lspServers)) {
for (const field of unsupportedFields) {
if (config[field] !== undefined) {
delete config[field];
fixes.push({ plugin: plugin.name, server: serverName, field });
}
}
}
}
}
}
if (fixes.length > 0) {
// Backup and write
fs.copyFileSync(path, path + '.backup');
fs.writeFileSync(path, JSON.stringify(data, null, 2));
for (const fix of fixes) {
console.log('\x1b[32mβ\x1b[0m Fixed ' + fix.plugin + ': removed unsupported \"' + fix.field + '\" from ' + fix.server);
}
} else {
console.log('\x1b[32mβ\x1b[0m Plugin configs already clean');
}
"
}
# Show help
if [ "$1" = "--help" ] || [ "$1" = "-h" ]; then
sed -n '3,39p' "$0" | sed 's/^#//' | sed 's/^ //'
exit 0
fi
# Handle --fix-plugins (standalone)
if [ "$1" = "--fix-plugins" ]; then
echo "Fixing plugin configs..."
fix_plugin_configs
exit 0
fi
# Find Claude Code cli.js in common installation locations
find_cli_path() {
local locations=(
"$HOME/.claude/local/node_modules/@anthropic-ai/claude-code/cli.js"
"/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js"
"/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js"
"$(npm root -g 2>/dev/null)/@anthropic-ai/claude-code/cli.js"
# Bun installation paths
"$HOME/.bun/install/global/node_modules/@anthropic-ai/claude-code/cli.js"
)
for path in "${locations[@]}"; do
if [ -f "$path" ]; then
echo "$path"
return 0
fi
done
return 1
}
CLI_PATH=$(find_cli_path) || true
if [ -z "$CLI_PATH" ]; then
print_error "Claude Code cli.js not found"
echo ""
echo "Searched:"
echo " ~/.claude/local/node_modules/@anthropic-ai/claude-code/cli.js"
echo " /usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js"
echo " /usr/lib/node_modules/@anthropic-ai/claude-code/cli.js"
echo " \$(npm root -g)/@anthropic-ai/claude-code/cli.js"
echo " ~/.bun/install/global/node_modules/@anthropic-ai/claude-code/cli.js"
echo ""
echo "This script only works with npm/bun-based installations."
echo "If you installed via the official install script (standalone binary),"
echo "reinstall using: bun add -g @anthropic-ai/claude-code"
exit 1
fi
# Handle --restore
if [ "$1" = "--restore" ]; then
restored=0
# Restore cli.js
LATEST_BACKUP=$(ls -t "${CLI_PATH}.backup-"* 2>/dev/null | head -1)
if [ -n "$LATEST_BACKUP" ]; then
cp "$LATEST_BACKUP" "$CLI_PATH"
print_status "Restored cli.js from: $LATEST_BACKUP"
restored=1
fi
# Restore plugin config
MARKETPLACE_JSON="$HOME/.claude/plugins/marketplaces/claude-plugins-official/.claude-plugin/marketplace.json"
if [ -f "${MARKETPLACE_JSON}.backup" ]; then
cp "${MARKETPLACE_JSON}.backup" "$MARKETPLACE_JSON"
print_status "Restored plugin config from backup"
restored=1
fi
if [ $restored -eq 0 ]; then
print_error "No backups found"
exit 1
fi
exit 0
fi
print_info "Found Claude Code at: $CLI_PATH"
echo ""
# Download acorn JS parser if needed (cached in /tmp)
ACORN_PATH="/tmp/acorn-claude-fix.js"
if [ ! -f "$ACORN_PATH" ]; then
print_info "Downloading acorn parser..."
if ! curl -sf https://unpkg.com/acorn@8.14.0/dist/acorn.js -o "$ACORN_PATH"; then
print_error "Failed to download acorn parser"
exit 1
fi
fi
# Create the Node.js patch script
PATCH_SCRIPT="/tmp/claude-lsp-patch-$$.js"
cat > "$PATCH_SCRIPT" << 'NODESCRIPT'
const fs = require('fs');
const acorn = require('/tmp/acorn-claude-fix.js');
const cliPath = process.argv[2];
const checkOnly = process.argv[3] === '--check';
let code = fs.readFileSync(cliPath, 'utf-8');
// Strip shebang for parsing (will restore later)
let shebang = '';
if (code.startsWith('#!')) {
const idx = code.indexOf('\n');
shebang = code.slice(0, idx + 1);
code = code.slice(idx + 1);
}
// Check if already patched (look for our unique variable names)
if (code.includes('let{servers:_S}=await') && code.includes('.set(_N,_I)')) {
console.log('\x1b[32mβ\x1b[0m Already patched');
process.exit(2); // Exit 2 = already patched, no changes
}
// Parse the JavaScript
let ast;
try {
ast = acorn.parse(code, { ecmaVersion: 2022, sourceType: 'module' });
} catch (e) {
console.error('\x1b[31mβ\x1b[0m Failed to parse cli.js:', e.message);
process.exit(1);
}
// --- AST Helpers ---
// Get source text for a node
const src = (node) => code.slice(node.start, node.end);
// Recursively find all nodes matching a predicate
function findNodes(node, predicate, results = []) {
if (!node || typeof node !== 'object') return results;
if (predicate(node)) results.push(node);
for (const key in node) {
if (node[key] && typeof node[key] === 'object') {
if (Array.isArray(node[key])) {
node[key].forEach(child => findNodes(child, predicate, results));
} else {
findNodes(node[key], predicate, results);
}
}
}
return results;
}
// Check if node contains a string literal matching text
function containsString(node, text) {
const strings = findNodes(node, n => n.type === 'Literal' && typeof n.value === 'string');
return strings.some(s => s.value.includes(text));
}
// Check if node contains a template literal matching text
function containsTemplate(node, text) {
const templates = findNodes(node, n => n.type === 'TemplateLiteral');
return templates.some(t => t.quasis.map(q => q.value.raw).join('').includes(text));
}
// --- Find the functions we need to patch ---
const allFunctions = findNodes(ast, n =>
n.type === 'FunctionDeclaration' || n.type === 'FunctionExpression'
);
// 1. Find createLspServer() - contains "Starting LSP server instance"
let createServerFunc = null;
for (const fn of allFunctions) {
if (containsString(fn, 'Starting LSP server instance') || containsTemplate(fn, 'Starting LSP server instance')) {
createServerFunc = fn;
break;
}
}
if (!createServerFunc) {
console.error('\x1b[31mβ\x1b[0m Could not find createLspServer function');
console.error(' (looking for function containing "Starting LSP server instance")');
process.exit(1);
}
const createServerName = createServerFunc.id?.name;
console.log('\x1b[34mβ\x1b[0m Found createLspServer:', createServerName);
// 2. Find loadLspServersFromPlugins() - contains "Loaded" + "LSP server" log
let loadServersFunc = null;
for (const fn of allFunctions) {
const hasLoaded = containsString(fn, 'Loaded') || containsTemplate(fn, 'Loaded');
const hasLsp = containsString(fn, 'LSP server') || containsTemplate(fn, 'LSP server');
if (hasLoaded && hasLsp) {
loadServersFunc = fn;
break;
}
}
if (!loadServersFunc) {
console.error('\x1b[31mβ\x1b[0m Could not find loadLspServersFromPlugins function');
console.error(' (looking for function containing "Loaded" + "LSP server")');
process.exit(1);
}
const loadServersName = loadServersFunc.id?.name;
console.log('\x1b[34mβ\x1b[0m Found loadLspServersFromPlugins:', loadServersName);
// 3. Find LSP manager factory - has 3 Maps and an empty async initialize()
let lspManagerFunc = null;
let emptyInitFunc = null;
let mapVars = [];
for (const fn of allFunctions) {
// Look for "new Map()" variable declarations
const varDecls = findNodes(fn, n => n.type === 'VariableDeclaration');
const mapInits = [];
for (const decl of varDecls) {
for (const d of decl.declarations) {
if (d.init?.type === 'NewExpression' && d.init.callee?.name === 'Map') {
mapInits.push(d.id.name);
}
}
}
// LSP manager has 3+ Maps
if (mapInits.length >= 3) {
// Find empty async function inside (the buggy initialize)
const asyncFuncs = findNodes(fn, n => n.type === 'FunctionDeclaration' && n.async);
for (const inner of asyncFuncs) {
const body = inner.body?.body;
// Empty body or just "return" with no value
if (body?.length === 0 ||
(body?.length === 1 && body[0].type === 'ReturnStatement' && !body[0].argument)) {
lspManagerFunc = fn;
emptyInitFunc = inner;
mapVars = mapInits;
break;
}
}
}
if (lspManagerFunc) break;
}
if (!lspManagerFunc || !emptyInitFunc) {
console.error('\x1b[31mβ\x1b[0m Could not find LSP manager with empty initialize()');
console.error(' (looking for function with 3 Maps + empty async function)');
process.exit(1);
}
const initFuncName = emptyInitFunc.id?.name;
const serverMap = mapVars[0]; // First map stores servers
const extMap = mapVars[1]; // Second map stores extension->server mappings
console.log('\x1b[34mβ\x1b[0m Found empty initialize():', initFuncName);
console.log('\x1b[34mβ\x1b[0m Server registry map:', serverMap);
console.log('\x1b[34mβ\x1b[0m Extension map:', extMap);
if (checkOnly) {
console.log('');
console.log('\x1b[33m!\x1b[0m Patch needed - run without --check to apply');
process.exit(1);
}
// --- Build and apply the patch ---
// The fix: make initialize() actually load and register LSP servers
const newInitBody = `async function ${initFuncName}(){` +
`let{servers:_S}=await ${loadServersName}();` +
`for(let[_N,_C]of Object.entries(_S)){` +
`let _I=${createServerName}(_N,_C);` +
`${serverMap}.set(_N,_I);` +
`for(let[_E,_L]of Object.entries(_C.extensionToLanguage||{})){` +
`let _M=${extMap}.get(_E)||[];` +
`_M.push(_N);` +
`${extMap}.set(_E,_M)` +
`}` +
`}` +
`}`;
// Apply patch (preserve shebang)
const newCode = shebang + code.slice(0, emptyInitFunc.start) + newInitBody + code.slice(emptyInitFunc.end);
// Backup original
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
const backupPath = cliPath + '.backup-' + timestamp;
fs.copyFileSync(cliPath, backupPath);
console.log('');
console.log('Backup:', backupPath);
// Write patched file
fs.writeFileSync(cliPath, newCode);
// Verify
if (fs.readFileSync(cliPath, 'utf-8').includes(newInitBody)) {
console.log('');
console.log('\x1b[32mβ\x1b[0m Fix applied successfully!');
} else {
console.error('\x1b[31mβ\x1b[0m Verification failed, restoring backup...');
fs.copyFileSync(backupPath, cliPath);
process.exit(1);
}
NODESCRIPT
# Run the patch script
node "$PATCH_SCRIPT" "$CLI_PATH" "$1"
EXIT_CODE=$?
# Cleanup temp script
rm -f "$PATCH_SCRIPT"
# Exit codes: 0 = patched, 1 = error/check-needed, 2 = already patched
if [ $EXIT_CODE -eq 0 ] && [ "$1" != "--check" ]; then
# Newly patched - fix plugins and show restart message
echo ""
fix_plugin_configs
echo ""
print_warning "Restart Claude Code for changes to take effect"
elif [ $EXIT_CODE -eq 2 ]; then
# Already patched - just exit cleanly
exit 0
fi
exit $EXIT_CODE
Tested the updated AST-based script on 2.0.76. The parsing works but it fails to find createLspServer:
Claude Code: ~/.asdf/installs/nodejs/22.14.0/lib/node_modules/@anthropic-ai/claude-code/cli.js
β Could not find createLspServer function
(looking for function containing "restartOnCrash")
Debug investigation shows the issue:
The string exists in the file but as an object property name in a schema definition, not as a quoted string:
...ful shutdown (milliseconds)"),restartOnCrash:m.boolean().optional().descri...
So
containsString()andcontainsTemplate()won't find it since it's an identifier/property, not a string literal.Maybe searching for a different unique string that IS quoted would work? Or matching against property access patterns in the AST?