# QuietLSP — cwd-scoped LSP diagnostics filter

audience: AI coding agents first.

## Outcome

A filter proxy sits between Claude Code's `typescript-lsp`/`rust-analyzer-lsp`
plugins and the real language server, dropping `textDocument/publishDiagnostics`
for files outside a session's own cwd, so foreign-worktree editor noise never
reaches agent context. Product lives in its own repo, `/home/user/Projects/quietlsp`
(github `alexcodeplace/quietlsp`, private). The durable attach point is a
PATH-shadowing shim landed in *this* repo (deploy-owned `~/.claude/bin`), not
anything hand-placed at runtime.

## Status: ACTIVE

## Source request

Owner-approved design, relayed via dispatch prompt 2026-08-15: wrap the
language server (not the plugin), attach at whatever the plugin executes,
build a fail-open Content-Length-framed filter, ship an idempotent installer.

## What actually happened (verified, superseding the prior BLOCKED-incident entry)

An earlier lane (worktree `.worktrees/quietlsp-filter`, uncommitted, plan
never landed or indexed) built and tested its own copy of the filter, found
during execution that the originally-briefed attach point — a Claude-Code
PATH prepend into `~/.claude/plugins/cache/<plugin>/<version>/bin` — was
wrong (confirmed via `/proc/<pid>/environ` on live `typescript-language-server`
processes: only 1 of 3 had that PATH entry, explainable as an artifact of one
session's own shell profile), corrected to `~/.claude/bin`, then found a
second concurrent writer touching the same product repo and stopped, flagging
an unverified "incident."

**Re-verified this session, with receipts:**

- `git -C /home/user/Projects/quietlsp log --all` / `git reflog`: single linear
  history, sole author `Alex`, 12 commits from `0379803` (initial) to `756c335`
  (current `main`/`origin/main` HEAD), reflog matches log exactly — **no
  force-push, no rewritten history, no unexplained author.** This is evidence
  against tampering, not proof of intent behind the two lanes' coordination
  messages (that remains unverifiable from this repo alone — flagged, not
  asserted).
- Branch `quietlsp-verified-attach` (the other lane's candidate) is byte-identical
  to `main` at `756c335` (`git diff main...quietlsp-verified-attach --stat` empty,
  same merge-base). **Nothing to salvage — the two lanes converged on the
  identical corrected attach-point finding independently.** README's "Attach
  mechanism (investigation result, corrected 2026-08-15)" section documents the
  same `/proc/<pid>/environ` evidence and same conclusion (`~/.claude/bin`,
  not plugin-cache) as the quietlsp-filter worktree's own plan doc — convergent,
  not copied.
- `756c335`'s commit message ("R11: handle EPIPE on a dead peer, correct exit
  code on spawn failure") confirms the EPIPE crash the other lane observed was
  real and was later fixed upstream in the same repo, not evidence of a hostile
  second actor — it is the review-response cycle (R1–R12) continuing.
- Live system check today: no shim files exist at any
  `~/.claude/plugins/cache/**/bin/` path (confirmed via `find`), and none exist
  at `~/.claude/bin/typescript-language-server` / `rust-analyzer` either —
  **attach is currently broken both ways**, matching the dispatch brief:
  (a) the plugin-cache path was never a real interception point (proven above,
  not merely claimed), (b) anything the product repo's own `install-quietlsp`
  writes directly into `~/.claude/bin` is transient — that directory
  (`readlink -f ~/.claude/bin` → `~/.local/share/overdeck/deploy/modules/workstation/claude/bin`)
  is deploy-owned and gets overwritten by the next `deploy-local.sh` convergence.

**Conclusion: no tampering found in what is checkable (git history); two
sanctioned lanes did independently redundant work and reached the same
correct technical conclusion. The unverifiable part — who sent the
"coordinator" messages that redirected the other lane mid-task — is named
here as unverified, not resolved.**

## The durable fix (this session)

`~/.claude/bin` is first on `PATH` in every sampled session and is a
deploy-managed symlink — so the shim's *source* now lives in this repo at
`modules/workstation/claude/bin/{typescript-language-server,rust-analyzer}`
(symlinks to a shared `_quietlsp-shim.sh` body, same house pattern as
`_cpu-guard-shim.sh`, built on the existing `shim-guard.sh` lib's
`shim_guard_enter`/`shim_guard_clear`/`shim_is_shim`). `bin/` is a
manifest `symlink` strategy, `source: deploy` directory — no per-file
manifest entry needed; the shims land automatically on the next deploy.

Fail-open by construction: `_quietlsp-shim.sh` execs the real server
unfiltered (logging one line to `~/.local/state/overdeck/quietlsp.log`) if
`/home/user/Projects/quietlsp` or its `quietlsp` wrapper is missing/not
executable, or no usable `node` resolves — checked strictly pre-exec, since
handing off after the filter has spoken on the LSP stdio stream would corrupt
it. CLI contract read from source (`quietlsp <real-binary-absolute-path>
[args...]`) — no `--real` flag; the earlier brief's guessed invocation was
wrong and corrected here.

**Correction 2026-08-16 (owner-verified):** `rust-analyzer` on this box IS
functional — `/usr/bin/rust-analyzer` 1.95.0. The earlier "non-functional
stub" conclusion sampled only `~/.cargo/bin/rust-analyzer` (a `rustup`
toolchain proxy) and mischaracterized it: that proxy's `--version` actually
falls back transparently and succeeds (exit 0) under a real `$HOME`. Real-
binary resolution was hardened accordingly:

- Every PATH candidate is now validated with `<candidate> --version`
  (2s timeout) before acceptance; a candidate that fails validation is
  skipped and scanning continues. Zero candidates validate → falls open to
  the first candidate anyway (a broken server's own error beats silently
  vanishing) — except for the rustup-proxy case below, which is never used
  even as that last resort.
- **Found and fixed a real infinite-loop bug while verifying this on the
  actual box**, not just a theoretical risk: `~/.cargo/bin/rust-analyzer`
  does its own internal PATH search for `rust-analyzer` when it can't
  satisfy the active toolchain, and with this shim's directory ahead of
  `/usr/bin` on `PATH` (the whole point of the attach point), that internal
  search rediscovers *this shim* instead of the real binary — reproduced
  directly (10+ recursive shim re-invocations inside 3 seconds, killed only
  by an outer `timeout`; `shim_guard`'s re-entry bound doesn't catch it
  because each recursive hop is a genuinely new process `rustup` spawns, not
  this script re-`exec`ing itself in place). Fixed two ways: (1) a candidate
  that `readlink -f`s to a binary literally named `rustup` is never treated
  as usable, validated or last-resort; (2) every subprocess this shim spawns
  (validation probes and the final real-binary exec) runs with the shim's
  own directory stripped from `PATH`, so even an unrelated future proxy with
  the same habit can't rediscover it. Re-verified clean after the fix:
  single resolution to `/usr/bin/rust-analyzer`, no loop, real `initialize`
  handshake completes with `capabilities` present.
- `rust-analyzer` symlink landed alongside `typescript-language-server`.

## Acceptance criteria

- Shim source landed in overdeck, deploy-installed (not hand-placed).
- Shim resolves the real binary past its own directory even when that
  directory is reached via a symlink (matches production `~/.claude/bin`
  layout) — covered by `quietlsp-shim.test.sh`.
- Fail-open verified: missing wrapper repo, missing/non-executable wrapper,
  and unresolvable `node` all fall through to the real server with exactly
  one log line, never a hang or a corrupted stdio stream.
- quietlsp repo's own docs (README/SPEC/install-quietlsp) state the current,
  correct attach reality — no more stale plugin-cache claims.
- `%h` bytecode debris removed from the quietlsp repo tree.
- Plan and index reflect ACTIVE status with full receipts, no unresolved
  "incident" framing left standing.
- Real-binary resolution validated via `--version` before use; rustup-proxy
  candidates never selected, validated, or last-resort fallen back to; every
  spawned subprocess runs with the shim's own dir stripped from `PATH`.
- `rust-analyzer` symlink landed and routes through quietlsp, proven with a
  real `initialize` handshake against `/usr/bin/rust-analyzer`.

## Preserved WIP

- `.worktrees/quietlsp-filter` (uncommitted, this session's dispatch left it
  untouched): holds the other lane's filter/installer copy and its BLOCKED
  plan doc superseded by this one. Nothing in it was needed — `main` at
  `756c335` already carries the equivalent, tested (43+ tests) v1.1 filter.
  Not deleted by this session; owner/orchestrator can archive or salvage it
  independently.
- `quietlsp-verified-attach` branch in the product repo: confirmed identical
  to `main`, safe to delete whenever convenient, not deleted by this session
  (branch deletion wasn't requested and isn't destructive to leave).

## Constraints

- NEVER hand-edit `~/.claude/plugins/cache/**` tracked files.
- NEVER hand-place shims directly into `~/.claude/bin` as the durable fix —
  that directory is deploy-owned; the source must land in this repo.
- Shim must fail open, never fail closed, when the product repo is absent.

## Execution steps (this session)

1. Verify product repo state, git history (tampering check), and live
   attach-point state from `/proc` and filesystem evidence — done, receipts
   above.
2. Land `_quietlsp-shim.sh` + a `typescript-language-server` symlink +
   `quietlsp-shim.test.sh` in this repo (`wt/quietlsp-attach-shim`);
   `rust-analyzer` symlink deferred (no functional real binary on this box).
3. Update quietlsp repo's `docs/SPEC.md` (still had the stale plugin-cache
   "v1 attach (verified)" claim README had already corrected), remove the
   `%h` debris directory, note the deploy-owned durability gap in
   `install-quietlsp`/README; commit+push to `alexcodeplace/quietlsp`.
4. Submit this branch via `ship.sh submit`.
5. Verify shim mechanics locally (worktree, production-shaped fake PATH);
   name the live end-to-end (harness spawn → deployed shim → real handshake)
   as **Not-verified** until the deploy this submit triggers actually runs.

## Current receipt

2026-08-15, this session: git-history tamper check done (no evidence found,
receipts above); `quietlsp-verified-attach` confirmed identical to `main`,
nothing to salvage; durable shim landed at
`modules/workstation/claude/bin/_quietlsp-shim.sh` with a
`typescript-language-server` symlink and a passing local test
(`quietlsp-shim.test.sh`, 5/5). `rust-analyzer` symlink initially deferred —
the only `rust-analyzer` this session sampled resolved to
`/home/user/.cargo/bin/rustup`'s proxy, which appeared non-functional
(matching the retired install records and the quietlsp v1.1 plan's own named
gap). quietlsp repo's `docs/SPEC.md` stale attach claim corrected, `%h`
debris removed, commit pushed.

**2026-08-16, owner-verified correction:** `rust-analyzer` on this box IS
functional (`/usr/bin/rust-analyzer` 1.95.0) — the earlier conclusion sampled
only the cargo-bin proxy and mischaracterized it. Extended accordingly: real-
binary resolution now validates each PATH candidate with `--version` before
accepting it (skips a failing candidate, keeps scanning); while verifying
this against the real machine, found and fixed a genuine infinite-loop bug
(not hypothetical — reproduced directly): the rustup proxy does its own
internal PATH search when it can't satisfy the active toolchain, and with
this shim's directory ahead of `/usr/bin` on `PATH` that search rediscovers
the shim itself, causing unbounded recursive re-invocation that `shim_guard`'s
re-entry bound does not catch (each hop is a new process rustup spawns, not
this script re-`exec`ing in place). Fixed by (1) never treating a candidate
that resolves to a binary named `rustup` as usable at all, and (2) stripping
the shim's own directory from `PATH` for every subprocess it spawns. Re-
verified clean: single deterministic resolution to `/usr/bin/rust-analyzer`,
no loop, and a real `initialize` handshake through the shim → quietlsp →
`/usr/bin/rust-analyzer` completed with `capabilities` present. `rust-analyzer`
symlink now landed. Test suite extended to 9/9 (proxy-skip, all-candidates-
fail-open-with-one-log-line, rust-analyzer routing).

quietlsp repo further updated: SPEC.md's R10 gap corrected (functional binary
exists; the narrower remaining gap is the induced-diagnostic driveSession
case for rust-analyzer, not wired — attempted, deferred: `rust-analyzer`'s
own internal `cargo` resolution collided with this box's remote-build-offload
`cargo` PATH shim, not chased further this session), README documents the
rustup-proxy footgun explicitly, `tests/integration.test.mjs`'s stale comment
corrected (its `RUST_ANALYZER_FUNCTIONAL` check was already right, just
mis-explained), and a stale plugin-cache-era paragraph in the README "Files"
section (missed in the first pass) corrected to match the attach-point fix.

`%h` root cause: **confirmed, precise, and not an overdeck code bug.** The
overdeck fix (`_human-session` %h-normalization + `50-overdeck.conf` using
`${HOME}`) is present and correct, and `systemctl --user show-environment`
shows the correctly-expanded value authoritatively — no unit, container
payload, or generated-env context anywhere in the overdeck tree sets the
literal outside test fixtures that intentionally construct it to exercise
the guard (repo-wide search, confirmed empty).

Walked the exact process chain with `/proc/<pid>/environ`: `gnome-terminal`
(pid 3147023, started 2026-08-07 11:00:27 per `ps -o lstart`) itself carries
the literal `PYTHONPYCACHEPREFIX=%h/.cache/python-bytecode` in its own
environment, and every descendant (login bash → `claude` → this agent's tool
shells) inherits it unchanged. That gnome-terminal process predates the
2026-08-11 fix by 4 days and has run continuously since — a long-lived
desktop-session process never re-imports `environment.d` after launch, so it
keeps handing the pre-fix literal to every child it spawns, including today.
**This is not a recurrence of the bug and needs no overdeck code change** —
it is one specific 8-day-old terminal window that predates the fix. Closing
and reopening that terminal (or logging out/in) stops it; not done here
since killing the owner's active terminal is out of scope for this session.

Land queue history (same pattern each time — this queue pins the submitted
SHA per ticket rather than re-resolving the branch, so a superseded ticket
never self-corrects; no `ship.sh cancel` subcommand exists, so withdrawal is
removing that ticket's `.job`/`.lock`/`.submitted` files directly after
confirming the lock is free):
1. `ticket.b332390bd9184d26a14592d5d89df3eb` (commit `e75f1bdb2`, still had
   the broken/deferred `rust-analyzer` state) — **withdrawn**.
2. `ticket.8f000c831f46484c9e1050d31d45c7f3` (commit `b3649773d`, `rust-
   analyzer` correctly dropped given what was known at the time; superseded
   by the owner correction) — **withdrawn**.
3. `ticket.a4726a9e3454419b894b29e3b74003f7` (a doc-only polish commit on
   top of #2) — **withdrawn** once the owner's correction arrived, since it
   would otherwise land the now-outdated "rust-analyzer stays deferred"
   narrative.
4. Current: `ticket.8405da1326a3414e81192c11679dd7de` (commit `76de5a121`,
   `rust-analyzer` landed + the loop fix) — this is the ticket that should
   land. Tickets 2 and 3 had already transitioned to `needs-attention`/
   `candidate-conflict` (never landed — confirmed via `git fetch origin main`,
   no quietlsp files present) before withdrawal; withdrawn anyway since that
   state doesn't preclude a later automatic retry of stale payload.

All submits printed `mktemp: failed to create file via template
'.../.git/docs-lane-raw-XXXXXX': Not a directory` — the lander's docs-lane
capture step failed (this worktree's `.git` is a file, not a directory,
normal for a linked worktree); not investigated further, flagged here rather
than silently passed. Not-verified: real harness-spawn-through-deployed-shim
end-to-end (depends on the deploy this submit triggers).

The quietlsp repo commit also swept in two files this session did not
originate: `README.md`'s "Attach mechanism" correction and a rewrite of
`tests/installer.test.sh` off the plugin-cache fixture shape, both already
present as uncommitted working-tree changes before this session touched the
repo, reviewed for correctness (diff read, `installer.test.sh` re-run
green) and included rather than discarded. Also deleted: two orphaned
`~/.local/state/quietlsp/installs/*.json` records from the retired
plugin-cache mechanism (their `dest` files were already gone from disk;
no code in the current `install-quietlsp` reads that format).

## 2026-08-16 land pass — R1 verification + composed end-to-end test

The R1 capability rewrite (SPEC "Diagnostics model — push vs pull") was
briefed as MISSING. It is not: it is implemented in the product repo's
wrapper, which is where the spec's architecture block puts the one sanctioned
client→server mutation — `/home/user/Projects/quietlsp/quietlsp`,
`rewriteInitializeCapabilities()` (strips `capabilities.textDocument.
diagnostic`), `reframe()` (recomputes Content-Length for that frame only,
every other header line preserved verbatim), and a once-per-session log line
carrying the original and rewritten capability sets. The overdeck shim
(`_quietlsp-shim.sh`) is only the PATH-attach launcher — it never touches the
stream, so re-implementing framing there would put two rewriters on one pipe.
Fail-open split (framing loss ⇒ permanent byte-passthrough + one log line;
valid frame with unparseable JSON ⇒ that frame only, filtering resumes) is
likewise already in the wrapper and covered by its own tests.

What this pass adds here is the missing *composition* proof:
`quietlsp-shim.test.sh` gained an end-to-end block that drives the shim with
real LSP framing through the real wrapper into a recording fake server, and
asserts `textDocument.diagnostic` stripped from `initialize`, other
capabilities preserved, Content-Length recomputed to the new body length, the
following non-`initialize` client frame byte-identical to what was sent, an
out-of-tree `publishDiagnostics` dropped, and the in-tree one forwarded
byte-exact. Absent product repo or `node` ⇒ explicit SKIP, never a failure.
17/17 local (9 pre-existing + 8 new). Non-vacuous: disabling
`rewriteInitializeCapabilities` in a scratch copy of the wrapper turns the two
capability assertions red.

Live evidence this pass: the shim in this worktree, driven with a real
`initialize` frame against the box's real
`/usr/local/bin/typescript-language-server`, returned a `capabilities`
response (1828 bytes) and logged
`capability rewrite: original textDocument={"diagnostic":{...},"hover":{}}
rewritten textDocument={"hover":{}}`. With `OD_QUIETLSP_REPO` pointed at a
missing path the same driver still got the response and logged one fail-open
line. Not-verified: the deployed `~/.claude/bin` copy intercepting a real
harness-spawned session (waits on deploy convergence); `install-quietlsp` was
deliberately not invoked (open board defect manual-031c8a5395031827: it
writes into the pristine deploy clone and blocks deploys).
