opencode's built-in LSP servers are repo-root gated (NearestRoot([...markers...]), walk stops at ctx.directory), but a custom lsp entry whose id is not a built-in gets root = ctx.directory ΓÇö i.e. it activates globally, with no marker file required. This behaviour is undocumented and there is no explicit way to request it.
Mechanism (source, dev)
packages/opencode/src/lsp/lsp.ts ΓÇö config entries are merged into the server map:
if (cfg.lsp !== true) {
for (const [name, item] of Object.entries(cfg.lsp)) {
const existing = servers[name]
if (item.disabled) { delete servers[name]; continue }
servers[name] = {
...existing,
id: name,
root: existing?.root ?? (async (_file, ctx) => ctx.directory), // <-- key line
extensions: item.extensions ?? existing?.extensions ?? [],
spawn: async (root) => ({ process: lspspawn(item.command[0], item.command.slice(1), { cwd: root, env: { ...process.env, ...item.env } }), initialization: item.initialization }),
}
}
}
- New id →
existing === undefined → root = ctx.directory (global; no markers).
- Built-in id (e.g.
biome, csharp) → inherits the built-in NearestRoot(...) → marker-gated.
The config schema (packages/core/src/config/lsp.ts) exposes only command, extensions, disabled, env, initialization ΓÇö there is no root or global field, and this behaviour is not in the docs (https://opencode.ai/docs/lsp/).
Verified globally (marker-free directory)
With ~/.config/opencode/opencode.jsonc entries using non-built-in ids:
In a bare temp dir with no lockfile/biome.json/.slnx anywhere on the walk path, writing broken files produced diagnostics (write-tool <diagnostics> block):
<diagnostics file="...\global-lsp-probe\probe2.py">
ERROR [1:6] "(" was not closed
ERROR [1:7] Expected parameter name
...
ERROR [1:37] Invalid expression term ';' # .cs
ERROR [1:19] Expected an expression, or an assignment but instead found ';'. # .ts
ERROR [1:11] Expected an expression, or an assignment but instead found ';'. # .js
(The same directory yields nothing for .sh and .json ΓÇö separate issues, not this request.)
Request
- Document the rule: "a custom server id that does not match a built-in is launched with the workspace directory as its root, so it is not marker-gated."
- Optionally expose it explicitly, e.g.
"root": "workspace" or "global": true on a server entry, so users don't have to discover a name-collision side effect.
- Consider the inverse too: a documented way to force marker-gating for a custom server.
Verified 2026-09-13 on opencode 1.18.30 (Windows 10 Pro 19045).
Environment (software)
| item |
value |
| OS |
Microsoft Windows 10 Pro — 10.0.19045 (Build 19045), 64-bit |
| Shell |
PowerShell 7.6.6 (+ Windows Terminal) |
| opencode |
1.18.30 |
| plugins |
oh-my-openagent 4.19.4 · @cortexkit/opencode-antigravity-auth 2.2.1 |
| runtimes |
Node v24.19.0 · npm 11.17.0 · Bun 1.4.2 |
| LSP binaries |
biome 2.5.13 · basedpyright 1.40.1 · roslyn-language-server 5.12.0-1.26426.8 · bash-language-server 5.6.0 · shellcheck 0.11.0 · vscode-langservers-extracted 4.10.0 · PowerShellEditorServices 4.7.0 · PSScriptAnalyzer 1.25.0 |
| logs |
~/.local/share/opencode/log/opencode.log · ~/.omo/agent/OmO-debug.log · ~/.omo/agent/logs/config-reload.log |
OS facts collected with:
Get-CimInstance Win32_OperatingSystem | Select-Object Caption,Version,BuildNumber,OSArchitecture
No hardware, hostname, username, or credentials included.
opencode's built-in LSP servers are repo-root gated (
NearestRoot([...markers...]), walk stops atctx.directory), but a customlspentry whose id is not a built-in getsroot = ctx.directoryΓÇö i.e. it activates globally, with no marker file required. This behaviour is undocumented and there is no explicit way to request it.Mechanism (source,
dev)packages/opencode/src/lsp/lsp.ts— config entries are merged into the server map:existing === undefined→root = ctx.directory(global; no markers).biome,csharp) → inherits the built-inNearestRoot(...)→ marker-gated.The config schema (
packages/core/src/config/lsp.ts) exposes onlycommand,extensions,disabled,env,initializationΓÇö there is norootorglobalfield, and this behaviour is not in the docs (https://opencode.ai/docs/lsp/).Verified globally (marker-free directory)
With
~/.config/opencode/opencode.jsoncentries using non-built-in ids:In a bare temp dir with no lockfile/biome.json/.slnx anywhere on the walk path, writing broken files produced diagnostics (write-tool
<diagnostics>block):(The same directory yields nothing for
.shand.jsonΓÇö separate issues, not this request.)Request
"root": "workspace"or"global": trueon a server entry, so users don't have to discover a name-collision side effect.Verified 2026-09-13 on opencode 1.18.30 (Windows 10 Pro 19045).
Environment (software)
~/.local/share/opencode/log/opencode.log·~/.omo/agent/OmO-debug.log·~/.omo/agent/logs/config-reload.logOS facts collected with:
No hardware, hostname, username, or credentials included.