Skip to main content

Command Palette

Search for a command to run...

Ruby-lsp keeps crashing in your monorepo? Here's exactly how to fix it in Zed

Updated
4 min readView as Markdown
Ruby-lsp keeps crashing in your monorepo? Here's exactly how to fix it in Zed

Ruby-lsp crashes in monorepos because Zed launches it from the wrong directory. Your Gemfile is one level down, bundle can't find it, and the server loops restarting. Same root cause breaks your test runner tasks.

Layout: Rails API in server/, React frontend in client/. No Gemfile at the root.

monorepo/
├── client/
├── server/          # Gemfile + spec/ live here
│   ├── Gemfile
│   └── spec/
└── .git/

The gems you need

Zed's Ruby extension expects a language server from your project gems. Add these to server/Gemfile's :development group:

gem 'ruby-lsp', require: false
gem 'ruby-lsp-rails', '~> 0.4'
gem 'ruby-lsp-rspec', require: false
Gem What it adds
ruby-lsp Go-to-definition, hover, completion, diagnostics
ruby-lsp-rails Schema columns, routes, associations
ruby-lsp-rspec Test discovery, "run test" code lens

Why bundle exec fails from the worktree root

Zed runs language servers from the worktree root. When you open monorepo/:

cd /path/to/monorepo
bundle exec ruby-lsp   # no Gemfile here

bundle walks up looking for a Gemfile, finds nothing, and exits. Zed restarts the server, it dies again, and you see Server reset the connection on repeat.

This isn't a ruby-lsp bug. It's a working-directory mismatch. The tool assumes a single-project layout, but the Ruby project is in a subdirectory.

Ruby-lsp logs are silent when it crashes this way. The reset notification is your only clue. The first time I hit this I spent an hour debugging my Ruby version before I thought to check where bundle was running.

The mise problem

Version managers (mise, rbenv, asdf) activate via shell config. Zed may not source your shell, so ruby/bundle aren't on PATH for the language server process. (Mise docs on shims explain why.)

Use the version manager's shims by absolute path. They resolve the correct Ruby per-directory without shell activation:

$ ls ~/.local/share/mise/shims/
bundle  ruby  ...

Three ways to point ruby-lsp at your Gemfile

1. Open the subproject directly

zed ~/dev/monorepo/server

Worktree root is now server/. Gemfile is right there. ruby-lsp starts cleanly.

Trade-off: Lose the single-window view of the whole repo.

2. Add server/ as a second worktree

Cmd/Ctrl-Shift-P"Add Folder to Project" → pick server/.

Zed runs language servers per worktree. The server worktree gets its own ruby-lsp from a directory with a Gemfile. Both sides visible in one window.

Trade-off: Per-machine UI action. Can't commit and share.

3. A wrapper script (committable)

This lives in the repo and works for everyone.

.zed/ruby-lsp.sh:

#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/../server"
exec "$HOME/.local/share/mise/shims/bundle" exec ruby-lsp "$@"

cd "$(dirname "$0")/../server" resolves relative to the script's location, not the caller's cwd. exec with the mise shim by absolute path sidesteps PATH gaps. (rbenv? ~/.rbenv/shims/bundle. asdf? ~/.asdf/shims/bundle.)

Make it executable, then route the binary via .zed/settings.json (Zed's LSP binary config):

{
  "lsp": {
    "ruby-lsp": {
      "binary": { "path": ".zed/ruby-lsp.sh" }
    }
  }
}

Smoke test:

$ timeout 5 .zed/ruby-lsp.sh < /dev/null
# (no output, hangs; success)

Could not locate Gemfile → wrong cd path. command not found → wrong shim path. Restart: "editor: restart language server".

Same problem, same fix for tasks

Zed tasks launch from the worktree root too. A naive RSpec task hits the same wall:

⏵ Task `test server/spec/.../organizations_spec.rb` finished with exit code: 10

Broken server/.zed/tasks.json:

[{
  "label": "test $ZED_RELATIVE_FILE",
  "command": "bundle",
  "args": ["exec", "rspec", "\"$ZED_RELATIVE_FILE\""],
  "cwd": "$ZED_WORKTREE_ROOT",
  "tags": ["ruby-test"]
}]

Two bugs:

  1. cwd is the repo root and there's no Gemfile there.

  2. $ZED_RELATIVE_FILE is server/spec/.... Fix cwd to server/ and that path becomes server/server/spec/..., which doesn't exist.

Fix:

[{
  "label": "test $ZED_RELATIVE_FILE",
  "command": "bundle",
  "args": ["exec", "rspec", "\"$ZED_FILE\""],
  "cwd": "$ZED_WORKTREE_ROOT/server",
  "tags": ["ruby-test"]
}]
Field Before After Why
cwd worktree root .../server So bundle finds the Gemfile
file arg $ZED_RELATIVE_FILE $ZED_FILE Absolute path, immune to cwd change

Run a single example with $ZED_ROW:

[{
  "label": "test (line) $ZED_RELATIVE_FILE:$ZED_ROW",
  "command": "bundle",
  "args": ["exec", "rspec", "\"$ZED_FILE:$ZED_ROW\""],
  "cwd": "$ZED_WORKTREE_ROOT/server",
  "tags": ["ruby-test"]
}]

Verify by hand:

$ cd ~/dev/monorepo/server
$ ~/.local/share/mise/shims/bundle exec rspec \
    "/home/me/dev/monorepo/server/spec/requests/api/super_admin/organizations_spec.rb" \
    --dry-run

--dry-run loads everything and runs nothing. Fast path confirmation.

What to commit

monorepo/
├── .zed/
│   ├── ruby-lsp.sh
│   └── settings.json
├── client/
└── server/
    ├── .zed/
    │   └── tasks.json
    ├── Gemfile
    └── spec/

No more server resets, no more failing tasks. Commit .zed/ and the next person who clones gets a working setup on open.