Skip to content

CLI LSP setup

The Pullminder CLI ships with a built-in Language Server Protocol server. Once configured, your editor surfaces Pullminder rule violations as inline diagnostics — squiggly underlines, hover descriptions, and quick-fix actions — the same way it shows compiler errors and lint warnings.

Prerequisites:

The server runs over stdio by default, which is what every editor on this page expects. If you need to run it over TCP (remote development, dev containers), see the TCP transport section at the bottom.


Create a local extension — no marketplace publish needed. VS Code loads extensions from a user-level directory, so the snippet below works on any machine without a publisher ID.

Terminal window
# macOS / Linux
mkdir -p ~/.vscode/extensions/pullminder-lsp
# Windows (PowerShell)
New-Item -ItemType Directory -Force "$env:USERPROFILE\.vscode\extensions\pullminder-lsp"

Save this as ~/.vscode/extensions/pullminder-lsp/package.json:

{
"name": "pullminder-lsp",
"displayName": "Pullminder LSP",
"description": "Pullminder language server — inline diagnostics for security, compliance, and code quality rules.",
"version": "0.1.0",
"publisher": "local",
"license": "MIT",
"engines": {
"vscode": "^1.75.0"
},
"activationEvents": [
"onLanguage:yaml",
"onLanguage:json",
"onLanguage:markdown",
"onLanguage:go",
"onLanguage:typescript",
"onLanguage:javascript",
"onLanguage:python",
"onLanguage:dockerfile",
"onLanguage:terraform",
"onLanguage:plaintext"
],
"main": "./extension.js",
"contributes": {
"languages": [
{
"id": "yaml",
"extensions": [".yml", ".yaml"]
}
]
}
}

activationEvents lists the language IDs the server wakes up for. Add or remove entries to match the file types in your projects. VS Code language identifiers are listed in the VS Code docs.

Save this as ~/.vscode/extensions/pullminder-lsp/extension.js:

const { LanguageClient, TransportKind } = require("vscode-languageclient/node");
/** @type {LanguageClient} */
let client;
/** @param {import("vscode").ExtensionContext} context */
function activate(context) {
const serverOptions = {
command: "pullminder",
args: ["lsp"],
transport: TransportKind.stdio,
};
const clientOptions = {
documentSelector: [
{ scheme: "file", language: "yaml" },
{ scheme: "file", language: "json" },
{ scheme: "file", language: "markdown" },
{ scheme: "file", language: "go" },
{ scheme: "file", language: "typescript" },
{ scheme: "file", language: "javascript" },
{ scheme: "file", language: "python" },
{ scheme: "file", language: "dockerfile" },
{ scheme: "file", language: "terraform" },
{ scheme: "file", language: "plaintext" },
],
};
client = new LanguageClient(
"pullminder-lsp",
"Pullminder LSP",
serverOptions,
clientOptions,
);
context.subscriptions.push(client.start());
}
function deactivate() {
if (client) {
return client.stop();
}
}
module.exports = { activate, deactivate };
Terminal window
cd ~/.vscode/extensions/pullminder-lsp
npm init -y
npm install vscode-languageclient@9

Run the Developer: Reload Window command from the command palette (Ctrl+Shift+P / Cmd+Shift+P). Open any file type listed in documentSelector — Pullminder diagnostics appear inline.

Troubleshooting: Check the extension host log at View → Output and select Pullminder LSP from the dropdown. If pullminder: command not found appears, VS Code’s PATH doesn’t include the binary. Either start VS Code from a terminal where pullminder resolves, or set the absolute path in serverOptions.command (run which pullminder to find it).


Two recipes: the built-in LSP client (Neovim 0.10+, no plugins) and nvim-lspconfig (broader compatibility).

Add this to your init.lua (or a file sourced from it). The snippet creates an autocmd that starts the Pullminder server for common file types:

-- Requires Neovim 0.10+
local function start_pullminder(bufnr)
vim.lsp.start({
name = "pullminder-lsp",
cmd = { "pullminder", "lsp" },
root_dir = vim.fs.dirname(vim.fs.find(".git", { upward = true, path = vim.fn.getcwd() })[1]),
filetypes = {
"yaml",
"json",
"markdown",
"go",
"typescript",
"javascript",
"python",
"dockerfile",
"terraform",
"sh",
"bash",
"zsh",
},
}, { bufnr = bufnr })
end
vim.api.nvim_create_autocmd("FileType", {
pattern = {
"yaml",
"json",
"markdown",
"go",
"typescript",
"javascript",
"python",
"dockerfile",
"terraform",
"sh",
"bash",
"zsh",
},
callback = function(args)
-- avoid attaching twice when multiple buffers share a filetype
local clients = vim.lsp.get_clients({ name = "pullminder-lsp", bufnr = args.buf })
if #clients == 0 then
start_pullminder(args.buf)
end
end,
})

What root_dir does: The snippet walks upward from the file’s directory until it finds a .git folder, then uses that directory as the workspace root. The Pullminder LSP reads .pullminder.yml from this directory. If your repos live outside .git trees or you use a monorepo layout, replace root_dir with vim.fn.getcwd() to always use the current working directory.

For users who already have nvim-lspconfig installed:

local lspconfig = require("lspconfig")
lspconfig.pullminder_lsp = {
default_config = {
name = "pullminder-lsp",
cmd = { "pullminder", "lsp" },
filetypes = {
"yaml",
"json",
"markdown",
"go",
"typescript",
"javascript",
"python",
"dockerfile",
"terraform",
"sh",
"bash",
"zsh",
},
},
}
lspconfig.pullminder_lsp.setup({})

Because Pullminder is not a built-in lspconfig server, the snippet registers it manually before calling .setup({}). This works with any lspconfig version.

  1. Open a file that matches one of the configured filetypes in a repository that has a .pullminder.yml.
  2. Run :LspInfo — you should see pullminder-lsp in the attached client list.
  3. Introduce a known-bad pattern (e.g. a hardcoded secret in a YAML file) — Pullminder diagnostics appear as virtual text and in the :LspLog.

Add a pullminder language server entry in your languages.toml (typically ~/.config/helix/languages.toml):

[language-server.pullminder-lsp]
command = "pullminder"
args = ["lsp"]
# Register the server for every language you want Pullminder to scan.
# Helix requires an explicit language entry per file type.
[[language]]
name = "yaml"
language-servers = ["pullminder-lsp"]
[[language]]
name = "json"
language-servers = ["pullminder-lsp"]
[[language]]
name = "markdown"
language-servers = ["pullminder-lsp"]
[[language]]
name = "go"
language-servers = ["pullminder-lsp"]
[[language]]
name = "typescript"
language-servers = ["pullminder-lsp"]
[[language]]
name = "javascript"
language-servers = ["pullminder-lsp"]
[[language]]
name = "python"
language-servers = ["pullminder-lsp"]
[[language]]
name = "dockerfile"
language-servers = ["pullminder-lsp"]
[[language]]
name = "hcl"
language-servers = ["pullminder-lsp"]
[[language]]
name = "bash"
language-servers = ["pullminder-lsp"]

Helix does not support wildcard or “all” language entries — repeat a [[language]] block for each file type you want scanned. The name values must match Helix language identifiers (run helix --health to list available languages).

Restart Helix after editing languages.toml. Open any configured file type to see diagnostics.


Zed uses JSON extension manifests to define language servers. Create ~/.local/share/zed/extensions/pullminder/extension.json (macOS/Linux) or %LOCALAPPDATA%\Zed\extensions\pullminder\extension.json (Windows):

{
"name": "Pullminder LSP",
"version": "0.1.0",
"language_servers": {
"pullminder-lsp": {
"name": "pullminder-lsp",
"command": "pullminder",
"args": ["lsp"],
"languages": [
"YAML",
"JSON",
"Markdown",
"Go",
"TypeScript",
"JavaScript",
"Python",
"Dockerfile",
"HCL",
"Shell Script"
]
}
}
}

Zed language identifiers are case-sensitive display names. A full list is available at Zed Languages.

This is a sketch — Zed’s extension system is still evolving. For the latest extension manifest format, check the Zed extension docs.


The default stdio transport works for local development. If your editor runs in a separate container or remote machine, use TCP mode instead:

Terminal window
pullminder lsp --tcp --port 9099

Then configure your editor to connect to localhost:9099 instead of launching the pullminder lsp command:

Neovim (vim.lsp.start):

vim.lsp.start({
name = "pullminder-lsp",
cmd = { "pullminder", "lsp", "--tcp", "--port", "9099" },
-- ... same root_dir and filetypes as above
})

Helix (languages.toml):

[language-server.pullminder-lsp]
command = "pullminder"
args = ["lsp", "--tcp", "--port", "9099"]

The --listen flag gives you finer control over the bind address:

Terminal window
pullminder lsp --listen 127.0.0.1:9099 # localhost only
pullminder lsp --listen :9099 # all interfaces

Only one editor can connect at a time over TCP. For multi-editor setups, start separate servers on different ports.