LSP and linter support
zut exposes a built-in lsp tool for code intelligence and diagnostics. It
communicates with stdio language servers through JSON-RPC and can also run
configured command-line linters without a shell.
Configuration
zut loads JSON configuration from the following layers, in increasing
precedence:
$ZUT_HOME/lsp.json
<cwd>/.pi/lsp.json
<cwd>/.omp/lsp.json
<cwd>/lsp.json, <cwd>/.lsp.json, and <cwd>/.zut/lsp.json
A file may put providers below servers, lspServers, or linters, use a
flat provider map, or use the providers array:
{
"servers": {
"gopls": {
"command": "gopls",
"fileTypes": [".go"],
"rootMarkers": ["go.mod", "go.work"]
},
"eslint": {
"kind": "cli",
"command": "eslint",
"args": ["--format", "json"],
"parser": "eslint-json",
"mode": "files",
"fileTypes": [".js", ".jsx", ".ts", ".tsx"],
"rootMarkers": ["eslint.config.js", ".eslintrc"]
}
}
}
The providers array accepts built-in names and objects with id, kind,
command, args, selectors, root markers, and nested lsp/cli options.
Unknown provider metadata is ignored. Set autoDetect to false to
keep only providers named or defined by the configuration file.
Server overrides merge onto zut's built-in definitions. Built-ins cover common
Go, Rust, TypeScript/JavaScript, Python, C/C++, YAML, JSON, and shell servers,
plus ESLint, golangci-lint, and Ruff CLI providers. A provider is selected
when it is enabled and its file type/root markers match the workspace. Command
lookup prefers project-local bin directories and then PATH; missing commands
are reported by status or the request result. Set disabled to true to
turn off a provider.
Useful server fields are:
| Field |
Meaning |
kind |
lsp (default) or cli |
command, args |
Executable and arguments |
fileTypes |
Extensions, basenames, or language IDs handled by the provider |
languageId |
Optional LSP language ID sent on textDocument/didOpen; inferred when omitted |
rootMarkers |
Files identifying a workspace root |
isLinter |
LSP provider used for diagnostics, not navigation/refactors |
parser |
CLI output parser: eslint-json, golangci-lint-json, ruff-json, sarif, or generic |
mode |
CLI provider scope: files or workspace |
settings |
Values returned for workspace/configuration requests |
initializationOptions |
Values sent during LSP initialization |
env |
Extra environment variables for the child process |
CLI providers may use {files} as an argument or inside an argument. It is
expanded to project-relative paths touched by the diagnostics request; an
embedded placeholder produces one argument per path. CLI output is capped
before parsing.
The model calls lsp with an action and optional arguments such as path,
line, column, server, or query. Every action has an overall time budget:
timeout_ms defaults to 30,000 milliseconds and is capped at 120,000 milliseconds.
diagnostics — merge LSP and CLI diagnostics for a file, glob, or the whole workspace. run_cli:false skips command-line linters.
definition, type_definition, implementation, references, hover — query a language server at a 1-based line and column.
symbols — return document symbols, or workspace symbols when query is supplied and path is omitted or *.
rename — request a WorkspaceEdit; applies it by default, or previews it with apply:false.
code_actions — list actions, or apply returned edits with apply:true.
status — show configured providers, running state, and diagnostic counts without starting servers.
reload — stop cached LSP processes and reload configuration.
capabilities — inspect initialize capabilities for one or all servers.
request — send a raw JSON-RPC method with JSON params.
--no-lsp disables the tool for one run. --tools lsp selects it when an
explicit tool list is used.
Diagnostics and context limits
Diagnostics are deduplicated across providers using path, severity, code,
start position, and normalized message. Results are ordered by severity and
location. Repeated
identical messages are represented as one occurrence group with example paths
or lines. Cascades beneath missing-module errors are grouped as SECONDARY
items so the root import problem remains visible without repeating a large
error fan-out.
The model-facing summary has a hard diagnostic cap (50 by default, with a
smaller eight-item summary used after writes and edits). Messages are
whitespace-compacted and clipped before formatting. Slow write-time checks are
bounded to a short two-second budget; a successful file write or edit is never
turned into a failed mutation because an LSP or linter is unavailable.
Write and edit diagnostics are enabled by default when LSP is enabled. Applied
LSP rename, code-action, and AST rewrite edits also run the same bounded checks
for up to three modified files within one shared two-second budget. The persisted configuration keys are
lsp_diagnostics_on_write and lsp_diagnostics_on_edit when an embedding
needs to override the native file-tool defaults.
Repeated post-write diagnostics are tracked per file by severity/code/message
identity, so a line shift does not replay the same error. Clearing a file's
diagnostics allows a later recurrence to be surfaced again.
Settings
/settings provides two independent switches, both enabled by default:
- lsp in main session (
lsp_enabled) controls the main agent and applies
immediately to the live tool registry.
- lsp in sub-agents (
subagent_lsp_enabled) controls newly spawned subagent
child processes.
The sub-agent setting does not grant access to a child whose profile or
explicit tool list omits lsp. Both settings can also be controlled through
$ZUT_HOME/config.json.
Safety and lifecycle
Language servers and linters run as local child processes. Project and
global LSP configuration is executable local configuration: review configured
commands, arguments, environment variables, and workspace-local binaries before
using it. zut does not add a separate trust prompt or sandbox around those child
processes; it does avoid shell expansion and keeps returned workspace edits
inside the session workspace. The lsp tool never executes configured commands
through a shell. Workspace edits returned by
rename and code actions are restricted to the session workspace, reject
resource operations, validate UTF-16 LSP positions, and write through a
temporary file before replacement. Server-initiated workspace/applyEdit
requests use the same checks.
LSP clients are cached per workspace and provider for the lifetime of the zut
process. reload and process shutdown terminate cached children. Server
stderr is kept out of model-facing tool results; diagnostics and protocol
errors are returned through the established tool result channel.
source
1# LSP and linter support
2
3zut exposes a built-in `lsp` tool for code intelligence and diagnostics. It
4communicates with stdio language servers through JSON-RPC and can also run
5configured command-line linters without a shell.
6
7## Configuration
8
9zut loads JSON configuration from the following layers, in increasing
10precedence:
11
12- `$ZUT_HOME/lsp.json`
13- `<cwd>/.pi/lsp.json`
14- `<cwd>/.omp/lsp.json`
15- `<cwd>/lsp.json`, `<cwd>/.lsp.json`, and `<cwd>/.zut/lsp.json`
16
17A file may put providers below `servers`, `lspServers`, or `linters`, use a
18flat provider map, or use the `providers` array:
19
20```json
21{
22 "servers": {
23 "gopls": {
24 "command": "gopls",
25 "fileTypes": [".go"],
26 "rootMarkers": ["go.mod", "go.work"]
27 },
28 "eslint": {
29 "kind": "cli",
30 "command": "eslint",
31 "args": ["--format", "json"],
32 "parser": "eslint-json",
33 "mode": "files",
34 "fileTypes": [".js", ".jsx", ".ts", ".tsx"],
35 "rootMarkers": ["eslint.config.js", ".eslintrc"]
36 }
37 }
38}
39```
40
41The `providers` array accepts built-in names and objects with `id`, `kind`,
42`command`, `args`, selectors, root markers, and nested `lsp`/`cli` options.
43Unknown provider metadata is ignored. Set `autoDetect` to `false` to
44keep only providers named or defined by the configuration file.
45
46Server overrides merge onto zut's built-in definitions. Built-ins cover common
47Go, Rust, TypeScript/JavaScript, Python, C/C++, YAML, JSON, and shell servers,
48plus ESLint, golangci-lint, and Ruff CLI providers. A provider is selected
49when it is enabled and its file type/root markers match the workspace. Command
50lookup prefers project-local bin directories and then `PATH`; missing commands
51are reported by `status` or the request result. Set `disabled` to `true` to
52turn off a provider.
53
54Useful server fields are:
55
56| Field | Meaning |
57| --- | --- |
58| `kind` | `lsp` (default) or `cli` |
59| `command`, `args` | Executable and arguments |
60| `fileTypes` | Extensions, basenames, or language IDs handled by the provider |
61| `languageId` | Optional LSP language ID sent on `textDocument/didOpen`; inferred when omitted |
62| `rootMarkers` | Files identifying a workspace root |
63| `isLinter` | LSP provider used for diagnostics, not navigation/refactors |
64| `parser` | CLI output parser: `eslint-json`, `golangci-lint-json`, `ruff-json`, `sarif`, or generic |
65| `mode` | CLI provider scope: `files` or `workspace` |
66| `settings` | Values returned for `workspace/configuration` requests |
67| `initializationOptions` | Values sent during LSP initialization |
68| `env` | Extra environment variables for the child process |
69
70CLI providers may use `{files}` as an argument or inside an argument. It is
71expanded to project-relative paths touched by the diagnostics request; an
72embedded placeholder produces one argument per path. CLI output is capped
73before parsing.
74
75## Tool actions
76
77The model calls `lsp` with an `action` and optional arguments such as `path`,
78`line`, `column`, `server`, or `query`. Every action has an overall time budget:
79`timeout_ms` defaults to 30,000 milliseconds and is capped at 120,000 milliseconds.
80
81- `diagnostics` — merge LSP and CLI diagnostics for a file, glob, or the whole workspace. `run_cli:false` skips command-line linters.
82- `definition`, `type_definition`, `implementation`, `references`, `hover` — query a language server at a 1-based line and column.
83- `symbols` — return document symbols, or workspace symbols when `query` is supplied and `path` is omitted or `*`.
84- `rename` — request a `WorkspaceEdit`; applies it by default, or previews it with `apply:false`.
85- `code_actions` — list actions, or apply returned edits with `apply:true`.
86- `status` — show configured providers, running state, and diagnostic counts without starting servers.
87- `reload` — stop cached LSP processes and reload configuration.
88- `capabilities` — inspect initialize capabilities for one or all servers.
89- `request` — send a raw JSON-RPC method with JSON `params`.
90
91`--no-lsp` disables the tool for one run. `--tools lsp` selects it when an
92explicit tool list is used.
93
94## Diagnostics and context limits
95
96Diagnostics are deduplicated across providers using path, severity, code,
97start position, and normalized message. Results are ordered by severity and
98location. Repeated
99identical messages are represented as one occurrence group with example paths
100or lines. Cascades beneath missing-module errors are grouped as `SECONDARY`
101items so the root import problem remains visible without repeating a large
102error fan-out.
103
104The model-facing summary has a hard diagnostic cap (50 by default, with a
105smaller eight-item summary used after writes and edits). Messages are
106whitespace-compacted and clipped before formatting. Slow write-time checks are
107bounded to a short two-second budget; a successful file write or edit is never
108turned into a failed mutation because an LSP or linter is unavailable.
109
110Write and edit diagnostics are enabled by default when LSP is enabled. Applied
111LSP rename, code-action, and AST rewrite edits also run the same bounded checks
112for up to three modified files within one shared two-second budget. The persisted configuration keys are
113`lsp_diagnostics_on_write` and `lsp_diagnostics_on_edit` when an embedding
114needs to override the native file-tool defaults.
115Repeated post-write diagnostics are tracked per file by severity/code/message
116identity, so a line shift does not replay the same error. Clearing a file's
117diagnostics allows a later recurrence to be surfaced again.
118
119## Settings
120
121`/settings` provides two independent switches, both enabled by default:
122
123- **lsp in main session** (`lsp_enabled`) controls the main agent and applies
124 immediately to the live tool registry.
125- **lsp in sub-agents** (`subagent_lsp_enabled`) controls newly spawned subagent
126 child processes.
127
128The sub-agent setting does not grant access to a child whose profile or
129explicit tool list omits `lsp`. Both settings can also be controlled through
130`$ZUT_HOME/config.json`.
131
132## Safety and lifecycle
133
134Language servers and linters run as local child processes. Project and
135global LSP configuration is executable local configuration: review configured
136commands, arguments, environment variables, and workspace-local binaries before
137using it. zut does not add a separate trust prompt or sandbox around those child
138processes; it does avoid shell expansion and keeps returned workspace edits
139inside the session workspace. The `lsp` tool never executes configured commands
140through a shell. Workspace edits returned by
141rename and code actions are restricted to the session workspace, reject
142resource operations, validate UTF-16 LSP positions, and write through a
143temporary file before replacement. Server-initiated `workspace/applyEdit`
144requests use the same checks.
145
146LSP clients are cached per workspace and provider for the lifetime of the zut
147process. `reload` and process shutdown terminate cached children. Server
148stderr is kept out of model-facing tool results; diagnostics and protocol
149errors are returned through the established tool result channel.