Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ Thumbs.db
*.log
.idea/
.vscode/
.claude/settings.local.json
zig-cache/
zig-out/
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@

1. **Plain text output** - No emojis, no ASCII art. Use `[OK]`, `[ERROR]`, `[WARN]`, `[CRITICAL]` for status markers.
2. **No unnecessary files** - Don't create summary files, plan files, audit files, or temp docs.
3. **Task is not done until CI passes** - agnix workflow must be green before merge.

Check warning on line 21 in AGENTS.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'must' at 40 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
4. **Create PRs for non-trivial changes** - No direct pushes to main.
5. **Always run git hooks** - Never bypass pre-commit or pre-push hooks.

Check warning on line 23 in AGENTS.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'Always' at 44 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
6. **Use single dash for em-dashes** - In prose, use ` - ` (single dash with spaces), never ` -- `.

Check warning on line 24 in AGENTS.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'never' at 46 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
7. **Mirror CLAUDE.md and AGENTS.md byte-identically** - When editing one, update the other in the same commit.
8. **Token efficiency** - Save tokens over decorations.

## Plugin shape - non-negotiables

1. **LSP server config lives in `marketplace.json`'s `lspServers` field**, inline on the plugin entry. NOT in a separate `.lsp.json` file at the plugin root - that mechanism does not exist in Claude Code's loader. Verified by inspecting the 12 official LSP plugins under `claude-plugins-official/.claude-plugin/marketplace.json`; their plugin source dirs ship only README + LICENSE.
1. **For third-party LSP plugins, ship `.lsp.json` at the plugin root** (`plugins/zig-lsp/.lsp.json`). This is the file the runtime loader reads from the user's cached install. We also keep `lspServers` inline in `marketplace.json` for marketplace-UI metadata and as a backstop; both are required because official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) load via a *separate* hardcoded path that doesn't apply to third parties. Empirically verified on Claude Code v2.1.119: with `.lsp.json` present in the cached plugin dir, `zig-lsp` registers as `Total LSP servers loaded: 3`; without it, the loader silently skips the entry even when `marketplace.json` has identical content.

Check warning on line 30 in AGENTS.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'required' at 58 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
2. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves.
3. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - the package manifest needs LSP support.
4. Bumping `zls` defaults in `lspServers` is a user-facing change - update CHANGELOG and bump the plugin `version` in `plugin.json`.
4. Bumping `zls` defaults: keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal. Update CHANGELOG and bump the plugin `version` in `plugin.json`.

## Validation

Expand Down
8 changes: 4 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Fixed
- **LSP server config now actually registers with Claude Code.** Empirical inspection of the 12 working official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) showed the LSP loader reads `lspServers` *inline on the marketplace.json plugin entry* — not from a separate `.lsp.json` at the plugin root, as the research doc claimed. Moved the ZLS server config into `.claude-plugin/marketplace.json`'s zig-lsp entry; deleted `plugins/zig-lsp/.lsp.json`. Same content, correct location.
- **`.lsp.json` restored at plugin root.** Empirical testing on Claude Code v2.1.119 showed third-party LSP plugins need both: an `lspServers` block in `marketplace.json` (matches official-plugin shape) AND a `.lsp.json` file at the plugin root (this is what the runtime loader actually reads from the cached install directory). The previous round of changes deleted `.lsp.json` based on an incorrect inference from the official LSP plugins' minimal source layout — those plugins use a separate hardcoded loader path that doesn't apply to third parties. With `.lsp.json` shipped, `claude --debug` logs `Loaded 1 LSP server(s) from plugin: zig-lsp`. Without it, the loader silently skips the entry.

### Changed
- `agent-knowledge/claude-code-lsp-plugin-contribution.md` carries a prominent correction block at the top documenting the loader-mechanism discovery; original sections are kept as historical record.
- `CLAUDE.md` / `AGENTS.md` / `CONTRIBUTING.md` "non-negotiables" rewritten to reflect the marketplace-level `lspServers` mechanism.
- Top-level `README.md` repo layout updated.
- `agent-knowledge/claude-code-lsp-plugin-contribution.md` correction block expanded with the two-path picture (official hardcoded vs third-party `.lsp.json`) and the empirical evidence table.
- `CLAUDE.md` / `AGENTS.md` / `CONTRIBUTING.md` "non-negotiables" updated: `.lsp.json` and marketplace `lspServers` are both required, kept byte-equivalent on server config.
- Top-level `README.md` repo layout reflects `.lsp.json` back in the plugin source.

## [0.1.0] - 2026-04-26

Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@

1. **Plain text output** - No emojis, no ASCII art. Use `[OK]`, `[ERROR]`, `[WARN]`, `[CRITICAL]` for status markers.
2. **No unnecessary files** - Don't create summary files, plan files, audit files, or temp docs.
3. **Task is not done until CI passes** - agnix workflow must be green before merge.

Check warning on line 21 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'must' at 40 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
4. **Create PRs for non-trivial changes** - No direct pushes to main.
5. **Always run git hooks** - Never bypass pre-commit or pre-push hooks.

Check warning on line 23 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'Always' at 44 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
6. **Use single dash for em-dashes** - In prose, use ` - ` (single dash with spaces), never ` -- `.

Check warning on line 24 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'never' at 46 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]
7. **Mirror CLAUDE.md and AGENTS.md byte-identically** - When editing one, update the other in the same commit.
8. **Token efficiency** - Save tokens over decorations.

## Plugin shape - non-negotiables

1. **LSP server config lives in `marketplace.json`'s `lspServers` field**, inline on the plugin entry. NOT in a separate `.lsp.json` file at the plugin root - that mechanism does not exist in Claude Code's loader. Verified by inspecting the 12 official LSP plugins under `claude-plugins-official/.claude-plugin/marketplace.json`; their plugin source dirs ship only README + LICENSE.
1. **For third-party LSP plugins, ship `.lsp.json` at the plugin root** (`plugins/zig-lsp/.lsp.json`). This is the file the runtime loader reads from the user's cached install. We also keep `lspServers` inline in `marketplace.json` for marketplace-UI metadata and as a backstop; both are required because official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) load via a *separate* hardcoded path that doesn't apply to third parties. Empirically verified on Claude Code v2.1.119: with `.lsp.json` present in the cached plugin dir, `zig-lsp` registers as `Total LSP servers loaded: 3`; without it, the loader silently skips the entry even when `marketplace.json` has identical content.

Check warning on line 30 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'required' at 58 percent of document (40-60 percent is the 'lost in the middle' zone) [PE-001]

Check warning on line 30 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Critical keyword 'required' at 58 percent of document (middle zone) [CC-MEM-008]
2. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves.
3. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - the package manifest needs LSP support.

Check warning on line 32 in CLAUDE.md

View workflow job for this annotation

GitHub Actions / Validate plugin & marketplace

Negative instruction 'Don't' without positive alternative [CC-MEM-006]
4. Bumping `zls` defaults in `lspServers` is a user-facing change - update CHANGELOG and bump the plugin `version` in `plugin.json`.
4. Bumping `zls` defaults: keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal. Update CHANGELOG and bump the plugin `version` in `plugin.json`.

## Validation

Expand Down
5 changes: 3 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,11 @@ Update `CHANGELOG.md` in the same PR. Keep entries under the `## [Unreleased]` h

## Plugin shape - non-negotiables

1. **LSP server config is inline in `marketplace.json`'s `lspServers` field**, on the plugin entry. NOT in a separate `.lsp.json` file - that mechanism doesn't exist in Claude Code's loader. The 12 official LSP plugins (typescript-lsp, rust-analyzer-lsp, etc.) all follow this pattern: their plugin source dirs ship only README + LICENSE; the `lspServers` block lives next to `description`/`version` in the marketplace entry.
1. **Ship `.lsp.json` at the plugin root** (`plugins/zig-lsp/.lsp.json`) AND keep an `lspServers` block inline in `.claude-plugin/marketplace.json`. Both are required for a third-party LSP plugin: the runtime loader reads `.lsp.json` from the user's cached install (this is what makes the LSP register on Claude Code v2.1.119); the marketplace `lspServers` field is metadata for the marketplace UI and matches what official plugins ship. Official LSP plugins (typescript-lsp, rust-analyzer-lsp) load via a separate hardcoded code path inside the Claude Code binary and don't ship `.lsp.json` themselves; that path doesn't apply to third parties.
2. The `command` field is `"zls"` - a PATH-resolved binary name. Never embed an absolute path; users install the binary themselves.
3. `extensionToLanguage` maps both `.zig` and `.zon` to language ID `"zig"`. Don't drop `.zon` - `build.zig.zon` needs LSP support.
4. `CLAUDE.md` and `AGENTS.md` are mirrored byte-identically. When editing one, update the other in the same commit.
4. Keep `.lsp.json` and the `lspServers` block in `marketplace.json` byte-equal on the server config. If they drift, behavior depends on which file the loader picks up first.
5. `CLAUDE.md` and `AGENTS.md` are mirrored byte-identically. When editing one, update the other in the same commit.

## Style

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ Claude Code's `LSP` tool dispatches to language-specific plugins. There are 11 o
│ └── zig-lsp/
│ ├── .claude-plugin/
│ │ └── plugin.json # Plugin identity (name, version, license)
│ ├── .lsp.json # ZLS server config — copied to user's
│ │ cache on install; this is what the
│ │ LSP runtime loader actually reads
│ └── README.md # User-facing setup + troubleshooting
├── agent-knowledge/ # Research notes for contributors
├── .agnix.toml # agnix config (target: claude-code)
Expand Down
33 changes: 19 additions & 14 deletions agent-knowledge/claude-code-lsp-plugin-contribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,32 @@
**Sources**: 24 resources analyzed
**Depth**: medium

> ## ⚠ CORRECTION — original research doc was wrong about the loader mechanism
> ## ⚠ CORRECTION — empirical investigation reshaped these facts twice
>
> The original "Key facts" said the plugin needs `.lsp.json` at the plugin root, citing PR #378. **That is not how Claude Code's LSP loader actually works.** Verified empirically by inspecting the 12 working official LSP plugins under `~/.claude/plugins/marketplaces/claude-plugins-official/`:
> The original "Key facts" said the plugin needs `.lsp.json` at the plugin root, citing PR #378. After empirical testing on Claude Code v2.1.119 we now have the *actual* picture, which differs from both the research doc AND the official Anthropic docs in subtle ways. Two paths exist, and you need both for a third-party LSP plugin to work:
>
> - Their plugin source dirs (`plugins/typescript-lsp/`, `plugins/rust-analyzer-lsp/`, etc.) ship **only `README.md` + `LICENSE`** — no `.claude-plugin/`, no `.lsp.json`.
> - The actual LSP server config lives **inline** in `marketplace.json`'s plugin entry, in a top-level `lspServers` field — alongside `name`, `description`, `version`, `source`.
> - The `lspServers` schema is identical to what the original research doc described (server name → `command` / `args` / `extensionToLanguage` / `initializationOptions` / `startupTimeout` / etc.). It just lives in a different file.
> - PR #378 may have been an aspirational change that was never merged or was rolled back. Don't trust the research doc on the `.lsp.json` claim.
> **The official LSP plugins** (typescript-lsp, rust-analyzer-lsp, etc., 12 of them in `claude-plugins-official`):
> - Plugin source dirs ship only `README.md` + `LICENSE` — no `.claude-plugin/plugin.json`, no `.lsp.json`.
> - `marketplace.json`'s plugin entry has `lspServers` inline alongside `name`/`description`/`version`/`source`.
> - These plugins load via a hardcoded code path inside the Claude Code binary that's wired specifically for the official marketplace name.
>
> **What this means for a contributor:**
> **Third-party LSP plugins** (anything not under `claude-plugins-official`):
> - Must ship `.lsp.json` at the plugin root in the source repo. This file gets copied into the user's `~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/` on install.
> - The runtime LSP loader reads `.lsp.json` from that cached path. **That's what actually registers the server.**
> - `lspServers` in marketplace.json is metadata for the marketplace UI / future-proofing — empirically not what triggers loading for non-`claude-plugins-official` plugins. Keep it anyway, identical to the `.lsp.json` content.
> - Open Anthropic issues describing related bugs and dead-code paths: [#15521](https://github.com/anthropics/claude-code/issues/15521), [#31468](https://github.com/anthropics/claude-code/issues/31468). The picture there is incomplete — `.lsp.json` empirically does work for third parties on v2.1.119.
>
> | Old (wrong) claim | Reality |
> **Empirical evidence (this repo, this session, v2.1.119):**
>
> | State | Result |
> | --- | --- |
> | Ship `.lsp.json` at plugin root | Don't ship `.lsp.json` at all — Claude Code ignores it |
> | Plugin needs `.claude-plugin/plugin.json` | Optional — official LSP plugins don't ship it. We keep it for identity metadata only |
> | LSP config is plugin-local | LSP config is marketplace-local (lives in marketplace.json `lspServers`) |
> | Test with `claude --plugin-dir <plugin>` | Test by registering a marketplace and installing — `--plugin-dir` may not exercise the marketplace-side `lspServers` loader |
> | Plugin in `agent-sh` marketplace, only `lspServers` in marketplace.json (no `.lsp.json`) | Silently skipped. `Checking plugin zig-lsp` appears in debug log, no `Loaded ... LSP server(s)` line follows. |
> | Same plugin replicated under `claude-plugins-official` marketplace cache | Loads. Hardcoded path. |
> | Plugin in `agent-sh` marketplace WITH `.lsp.json` at plugin root → cached in install dir | **Loads.** `Loaded 1 LSP server(s) from plugin: zig-lsp`. |
>
> **Updated key facts**: A working LSP plugin is 1 marketplace-level file (`.claude-plugin/marketplace.json` with `lspServers` inline on the plugin entry) + at minimum `README.md` in the plugin source dir. Binary name is `zls`. Extensions: `.zig` and `.zon` → language `"zig"`. Official marketplace submission is form-gated (not PR-based).
> **Updated key facts (canonical):** A working third-party LSP plugin ships `.lsp.json` at the plugin root + an `lspServers` block in `.claude-plugin/marketplace.json` (kept byte-equivalent on server config) + a `plugin.json` for identity metadata + `README.md`. Binary name `zls`. Extensions `.zig` and `.zon` → language `"zig"`. Submission to the official marketplace is form-gated at <https://clau.de/plugin-directory-submission>.
>
> The sections below are kept verbatim with the original `.lsp.json` framing as historical record. Refer to `CONTRIBUTING.md` and `CLAUDE.md` in this repo for the corrected, currently-accurate guidance.
> The sections below are kept verbatim with the original `.lsp.json`-only framing as historical record. Refer to `CONTRIBUTING.md` and `CLAUDE.md` in this repo for the corrected, currently-accurate guidance.

---

Expand Down
22 changes: 22 additions & 0 deletions plugins/zig-lsp/.lsp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"zig": {
"command": "zls",
"extensionToLanguage": {
".zig": "zig",
".zon": "zig"
},
"initializationOptions": {
"enable_snippets": true,
"enable_argument_placeholders": true,
"enable_build_on_save": true,
"build_on_save_args": [],
"semantic_tokens": "full",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The semantic_tokens key with the value "full" appears to be incorrect for ZLS. According to the ZLS configuration schema, the relevant option is typically enable_semantic_tokens and it expects a boolean value. Note that this change should also be applied to the lspServers block in marketplace.json to adhere to the project's requirement of keeping these configurations byte-equal.

Suggested change
"semantic_tokens": "full",
"enable_semantic_tokens": true,

"inlay_hints_show_variable_type_hints": true,
"inlay_hints_show_parameter_name": true,
"prefer_ast_check_as_child_process": true
},
"startupTimeout": 30000,
"restartOnCrash": true,
"maxRestarts": 3
}
}
Loading