What those attestation badges do NOT cover. The SBOM, provenance, and Sigstore badges attest build integrity over the source archive -- they are not Windows Authenticode, assert no verified-publisher identity, and are not a third-party security audit. The exact boundary is stated in What this does and does not prove below and in TRUST.md, "Honest limits".
As Claude edits a .ps1, .psm1, or .psd1, this plugin runs real PowerShell Editor Services
(PSES) + PSScriptAnalyzer over that file and feeds the result -- syntax errors and lint findings,
with fix suggestions -- straight back into Claude's context, so a mistake gets caught and
corrected in the same turn. It is language tooling, not project tooling.
No recurring prompt injection; context is added only when a PowerShell edit produces diagnostics or enabled guidance.
A language server spawns only when a PowerShell file is edited, and one warm process serves the
whole session, so each edit pays a fast pipe round-trip instead of a cold start.
Install and verify in about five minutes -- install to a real caught diagnostic, including the
first-session bootstrap you cannot skip. Requires pwsh (PowerShell 7+) on your PATH
(winget install Microsoft.PowerShell if it is missing); then:
# 1. In Claude Code -- add the marketplace, install, then enable the plugin:
/plugin marketplace add manderse21/claude-powershell-lsp
/plugin install powershell-lsp@claude-powershell-lsp
/plugin enable powershell-lsp
# 2. Start a new session (or /reload-plugins) so the hooks load and the first
# SessionStart bootstraps PSES + the warm daemon.
# 3. Confirm it is healthy BEFORE you rely on it -- run the preflight DOCTOR from
# inside the enabled session (so it can see the plugin data dir):
/powershell-lsp:doctor
# All PASS (benign UNKNOWNs are fine) -> ready. A FAIL names the exact fix.
See it catch something. Ask Claude to write:
function Frobnicate-Thing { Get-Process }and the PostToolUse hook returns, right in Claude's context:
The cmdlet 'Frobnicate-Thing' uses an unapproved verb. (PSUseApprovedVerbs)
Claude sees its own mistake and corrects it without you switching tools. Full prerequisites and the step-by-step walkthrough are in Quick start.
Three capabilities, in the order most users meet them.
The core loop above, live on every supported host today: each PowerShell edit is analyzed by a warm PSES + PSScriptAnalyzer and the findings return in the same turn, deduped, ordered, and filtered by your configuration. Nothing else needs enabling.
What the live surface covers, and what it does not. Diagnostics are delivered through a PostToolUse hook, so the analyzed set is exactly the files Claude edits in this session -- one file per edit, as it is edited. There is no file watcher and no background sweep of files nobody touched, which is what keeps the always-on cost at zero. Automatic live workspace analysis remains on the roadmap. Explicit whole-repository scanning is available today through lsp-scan.ps1 and CI/SARIF integration. See Repository and CI validation below for that path.
Set nativeServe to shim and hover, go-to-definition,
find-references, and documentSymbol serve to Claude Code's native LSP client on a .ps1 /
.psm1 / .psd1 -- resolved through PSES, not just the diagnostics from the warm hook.
It is off by default because it is a workaround: Claude Code's LSP client currently rejects
the standard server-to-client requests PSES sends during initialization (the upstream
#1359-class handshake gap), so a thin stdio proxy closes the gap locally. off is a byte-exact
transparent pass-through, and the diagnostics hook is wholly independent of this knob. Mechanics,
the removal path, and the Windows Claude Code 2.1.196-2.1.200 known issue are in
docs/configuration.md, nativeServe and
Why a hook, not native registration.
The same diagnostics engine is also a standalone gate. scripts/lsp-scan.ps1 runs over a path --
one file or a whole directory -- and emits SARIF 2.1.0 for GitHub code scanning, or a
human-readable text report.
# Scan a directory, emit SARIF for code scanning (the default format):
pwsh -File scripts/lsp-scan.ps1 ./src -OutputPath results.sarifThe scan is a sibling invocation of the exact path the PostToolUse hook uses, so a finding is identical whether it surfaces while Claude edits or in your CI.
Full text -- the other invocations, the SARIF severity mapping, the exit codes, and the workflow this repository uses to scan itself -- is in docs/repository-scanning.md.
Three slash commands, available once the plugin is enabled. Each wraps a script that already ships -- they add no analysis of their own:
| Command | What it does |
|---|---|
/powershell-lsp:doctor |
The full preflight health check with a named fix for anything wrong (scripts/doctor.ps1). Report-only. |
/powershell-lsp:status |
The same checks rendered as one line each -- a health glance rather than a fix-list (scripts/doctor.ps1 -Summary). |
/powershell-lsp:scan <path> |
Scans a file or directory with the same engine the edit hook uses (scripts/lsp-scan.ps1). This is the explicit whole-repository path. |
status runs the identical checks as doctor and produces the identical statuses and exit code;
only the presentation differs.
Checked in order by the Quick start below.
- PowerShell 7+ (
pwsh) on your PATH. As of 1.1.1 the plugin's hooks launch underpwsh; Windows PowerShell 5.1 alone cannot bootstrap them. Check withpwsh -v. - Internet access on the first enabled session -- or a configured offline source. PSES and
PSScriptAnalyzer are downloaded on first use, not vendored (see
Pinned versions). The download is idempotent and marker-gated. On a machine
with no egress, point the plugin at an internal mirror or a pre-staged bundle instead --
see Offline and air-gapped installation;
every source is verified against the same SHA-256 pin. With neither configured and no internet,
the first run surfaces an honest
unavailablebanner instead of failing silently (see Diagnostics status). - On managed / locked-down Windows, a security control (WDAC / AppLocker / ExecutionPolicy
/ Constrained Language Mode) can block a downloaded component; it then reads as
unavailablerather than crashing. See Troubleshooting.
Windows PowerShell 5.1 can still serve as the PSES child host (set ps_host to powershell);
it simply cannot launch the hooks themselves. See Platform support.
The three-step block at the top of this README is the whole job -- from install to a real caught diagnostic. A few of its steps are deliberate, documented here rather than removed:
/plugin enablestays an explicit step. The plugin ships disabled by default (defaultEnabled: false) because it downloads a bundle and spawns a language server, so enabling it is a conscious opt-in.- The new session / reload is required -- Claude Code loads plugin hooks at session start, so enabling alone does not load them.
- The first enabled session does the rest itself. Its
SessionStarthook downloads PSES and vendors PSScriptAnalyzer (both idempotent and marker-gated), then launches one warm daemon for the session. Early edits readincompletewhile PSES finishes starting, and settle once it reports ready. No fixed number of edits is enforced -- an edit readsincompletewhenever it arrives before PSES is ready, so how many you see depends on how fast you edit and how loaded the machine is. On a quiet host it is typically one. See Diagnostics status for how to tell startup from a stall. - Run the doctor (step 3).
/powershell-lsp:doctoris the in-session form and needs no paths; the rawscripts/doctor.ps1invocation under Troubleshooting is for the out-of-session case, where the slash command is not available. It turns the worst onboarding failure -- enabled but a prerequisite is missing, so diagnostics silently do nothing -- into a named, actionable fix-list, and it confirms the warm daemon is actually answering before you trust a silent result as "analyzed, clean". It is report-only: it never downloads, repairs, or starts anything. See the preflight doctor.
Set these via the /plugin config UI for powershell-lsp, or leave the defaults. Every default
is safe: no knob has to be set for the live diagnostics loop to work.
docs/configuration.md is the authoritative reference -- every knob's allowed values, precedence, guards, and edge cases, one anchored section per knob. The config panel and the tables below are summaries of it.
Start with one setting, not twenty. The profile knob is a curated preset over every other
knob. Pick the row that matches how much you want surfaced -- the middle column is the value you
type into the config panel:
| Profile | Value | What you get |
|---|---|---|
| Compatibility | safe (default) |
Exactly today's shipped defaults. safe maps nothing rather than restating the defaults, so the diagnostics surface is byte-for-byte unchanged. Start here. |
| Recommended | recommended |
A broader but still quiet surface: the base ruleset, formatter and module hints as suggestions, reference-count facts, and two lines of edit context. Nothing writes to your files. |
| Comprehensive | strict |
recommended plus an audit posture: whole-file scope, no per-file cap, and longer log retention -- so a finding is never hidden by scoping or truncation. |
Three ways to configure, in one sentence each. Leave everything alone and you get
Compatibility (safe), which is byte-for-byte today's behavior. Set profile to recommended or
strict for a curated preset. Or go custom: set knobs yourself -- an explicitly-set knob always
wins, whether or not a profile is also set. "Custom" is not a profile value, it is what you get by
setting a knob, which is why there is no fourth mechanism.
Every knob. The full surface -- profile first, then the nineteen knobs it presets:
| Key | Default | Meaning |
|---|---|---|
profile |
safe |
The preset chooser above: safe = Compatibility, recommended = Recommended, strict = Comprehensive. A knob you set explicitly always wins over the profile. See profile |
ps_host |
pwsh |
PSES host executable: pwsh (PowerShell 7+, recommended/tested) or powershell (Win 5.1) |
severityThreshold |
Hint |
Least-severe level to report: Error > Warning > Information > Hint |
ruleInclude |
(empty) | Comma-separated PSScriptAnalyzer rule codes to report exclusively; empty = all |
ruleExclude |
(empty) | Comma-separated rule codes to suppress (e.g. PSAvoidUsingWriteHost) |
timeoutMs |
5000 |
Total hard cap (ms) before the PostToolUse client degrades to log-only |
debounceMs |
150 |
Edits landing within this window (ms) fold into one analysis pass |
keepLastN |
10 |
Newest rolling log files kept per family (swept at SessionStart) |
idleTtlMin |
30 |
Daemon self-terminates after this many minutes with no diagnostics request |
perFileCap |
20 |
Max diagnostics reported per file; the rest collapse into an ... and N more line; 0 = no cap |
enableStats |
false |
Append one JSONL timing line per analyzed edit to logs/stats.jsonl; observe-only, never changes output. Logs an absolute path per line -- see the privacy note |
settingsPath |
(empty) | Absolute path to a PSScriptAnalyzerSettings.psd1 to honor, overriding auto-discovery; a relative value is ignored |
scopeToEdit |
true |
Scope surfaced diagnostics to the lines the edit touched (plus editContextLines); fails open to whole-file when the range is indeterminate |
editContextLines |
0 |
Extra context lines kept above and below the touched range when scopeToEdit is on |
formatOnEdit |
off |
suggest surfaces a formatter diff and never rewrites your file; apply additionally writes it back behind a stale-write / atomic / byte-fidelity guard and is doubly opt-in. Values: off, suggest, apply. See formatOnEdit |
ruleset |
pses-default |
Live diagnostics ruleset tier. pses-default keeps PSES's built-in no-settings set (about 15 rules); base opts in to the shipped enumerated ruleset so PSAvoidUsingWriteHost and the three Error-severity security rules surface. Repo settings always win. See ruleset |
moduleAwareness |
off |
suggest adds an Information hint when a command is exported by a known module that is not installed here. Positive-identification only, silent on ambiguity, never writes. See moduleAwareness |
nativeServe |
off |
shim serves hover / go-to-definition / find-references / documentSymbol to Claude Code's native LSP client through a handshake proxy. off is a byte-exact pass-through. See Native code navigation |
referenceSurfacing |
off |
counts surfaces bare per-function facts (cross-file reference counts, where a call is defined) as additive Information, never a diagnostic. Silent on ambiguity, never writes. See referenceSurfacing |
orgPolicy |
(empty) | Absolute path to a centrally-managed PSScriptAnalyzerSettings.psd1 whose ExcludeRules are enforced above repo-local config -- an org can take a rule away, never force one on. Fails open. See orgPolicy |
Diagnostics are returned in a stable order (severity, then line, then column), deduped, threshold- and rule-filtered, then capped per file.
These filters apply on top of whatever PSES publishes. By default (ruleset =
pses-default) PSES runs its own built-in no-settings rule set for live analysis, which is
narrower than the Invoke-ScriptAnalyzer CLI default -- for example PSAvoidUsingWriteHost is
not surfaced on the fly even though the CLI flags it. The filter knobs (severityThreshold,
ruleInclude, ruleExclude) can suppress or narrow what PSES reports. To broaden the live
surface instead, set ruleset = base, or point settingsPath at your own settings file.
Every analyzed edit resolves to one of four statuses. The clean case is silent; the other three
surface a one-line banner in Claude's context, so a result is never mistaken for "analyzed,
clean" when it was not actually analyzed. The wording is owned in one place
(Get-DiagnosticsStatusBanner in scripts/lib/lsp-common.ps1).
| Status | When | What you see / what to do |
|---|---|---|
ok |
The PSScriptAnalyzer pass settled and the analyzer was available. | Nothing extra -- diagnostics (if any) are shown, no banner. The warm happy path. |
incomplete |
The pass did not settle for this edit -- PSES timed out, threw, exited, a supervised re-spawn was mid-flight, or PSES is still starting. | analysis did not complete -- this edit was NOT checked. Transient: the next edit usually succeeds once PSES is ready. |
degraded |
PSES is up and settled, but the vendored PSScriptAnalyzer is absent, so only the parser ran. | parser-only mode -- PSScriptAnalyzer unavailable, lint rules were NOT checked (syntax errors are still reported). Start a fresh session so ensure-pssa re-vendors; see logs/ensure-pssa.log. |
unavailable |
PSES could not start at all, for the whole session -- the bundle never bootstrapped (a clean box, offline or behind a proxy), or it is present but failed to initialize. | PowerShell editor services could not start ... Diagnostics will stay OFF for this whole session until it is fixed and the session is restarted. Fix the install/startup, then start a fresh session; see logs/ensure-pses.log and logs/pses-daemon.log. |
incomplete (transient) and unavailable (permanent for the session) are deliberately distinct,
with distinct remedies. When the daemon is unreachable entirely -- no pipe at all -- the
PostToolUse client surfaces its own honest banner, so even the no-pipe case is never silent.
There are two such banners, and they carry opposite remedies, so read which one you got.
Both are prefixed PowerShell diagnostics unavailable for <path>: and continue:
Case 1 -- wait. The client relaunched the daemon itself; your next edit gets it.
the analyzer had stopped (e.g. after idle) and is being restarted -- this edit was NOT checked; your next edit should be.
Case 2 -- act. The relaunch was suppressed or failed, so nothing will fix it on its own.
the analyzer was not reachable and could not be restarted automatically -- this edit was NOT checked. Start a new session to restart it.
The phrase could not be restarted automatically appears in only the second, so searching your
transcript for it is what tells the act case from the wait case.
The incomplete banner is byte-identical every time it fires, so several in a row do not
themselves tell you which of three different things is happening. The daemon log distinguishes
them precisely. Look in logs/pses-daemon.log under your plugin data directory:
... while not ready (state=initializing)-- PSES is still starting. Wait: it settles by itself once PSES reports ready. This is the cold-start case.... while not ready (state=respawning)-- PSES died and is being re-spawned. Wait: the re-spawn is automatic.- No
while not readyline at all for those edits -- PSES is ready, so the analysis itself is not converging. This is the large-file case, and it will not settle on its own. The lever istimeoutMs: the largest files need roughly 6-7 s of analysis against a 5000 ms default. ... while unavailable (permanent)-- PSES could not start for this session at all. This surfaces the distinctunavailablebanner rather thanincomplete; fix the install and start a fresh session.
The absence of the while not ready line is the load-bearing signal: it is what separates
still starting (which ends by itself) from will never settle on this file (which does not).
Measured on pwsh 7.6.3, Windows 11 Pro, at the v1.24.3 build, on 2026-07-17:
warm-path latency (edit -> diagnostic round-trip) has a median of 2228 ms and a p95 of
2463 ms over 30 iterations. A figure without its sample size is not evidence, which is why
the n travels with it. Cold-start latency is deliberately not published, because it is not
currently measured to publication standard.
Full text -- why the cold-start number is withheld, the un-re-measured v1.12.0 spawn attribution, and how CI guards these medians -- is in docs/performance.md; the harness, method, and full numbers are in docs/benchmarks.md.
Diagnostics are delivered through a PostToolUse hook backed by a warm, per-session daemon -- one PSES stays hot for the whole session, so each edit pays a pipe round-trip instead of a cold PSES start.
Full text -- the SessionStart / PostToolUse / SessionEnd flow diagram and the stdout and state rules that go with it -- is in docs/warm-daemon.md. The full flow from edit to banner is in ARCHITECTURE.md.
Native registration works as of v1.18.1; end-to-end serve does not. Claude Code's LSP
client rejects the standard server-to-client requests PSES sends during initialization (the
#1359-class handshake), so the direct path times out -- gated upstream, not on this plugin's
launcher. The opt-in nativeServe = shim closes that gap
locally.
Full text -- the registration-versus-serve distinction, the duplicate-diagnostics heads-up for when serve lands, and the pointer to the 23-probe methodology matrix -- is in docs/native-registration.md.
Two components are downloaded on first use, each pinned by version and SHA-256: PSES v4.6.0
(scripts/ensure-pses.ps1) and PSScriptAnalyzer 1.25.0 (scripts/ensure-pssa.ps1).
Full text -- the pin table, how to bump either pin, and why the CI .nupkg cache is a transport
optimization and never a trust shortcut -- is in
docs/pinned-versions.md; the hash table is in TRUST.md.
As of 1.1.1 the hooks require pwsh (PowerShell 7). Windows PowerShell 5.1 is supported as
the PSES child host (set ps_host to powershell), not as the hook interpreter. CI runs the
Pester suite -- including the full warm-daemon integration suite -- green on a four-leg matrix:
Windows pwsh 7, Windows PowerShell 5.1, Ubuntu pwsh, and macOS pwsh.
Full text -- what each leg proves and how the scripts stay cross-platform -- is in docs/platform-support.md.
A curated corpus (tests/corpus/) proves the diagnostics the tool reports are correct -- not
merely present, and not merely honest when it cannot analyze. Three sample categories carry the
headline: clean (50 cases, expect zero findings), known-bad (36 cases, six per surfaced
rule, asserting the exact rule id, line, and severity), and parser-error (3 cases).
Which fixtures the headline scores. Get-CorpusCorrectnessReport
(tests/corpus/Corpus.Common.ps1) builds the false-positive denominator from every clean spec
and the true-positive denominator from every bad spec, as enumerated by Get-CorpusSampleSpec:
samples/clean contributes both its *.ps1 and its *.txt fixtures, samples/bad its *.ps1
fixtures. The corpus also carries bashism, compat, pre-pssa and module fixtures, each
separately asserted; none of them enters either headline denominator. The denominators below
are therefore the full scored sets, not a subset.
Measured correctness (default config, all four CI legs): a 0% false-positive rate (0 of 50
known-good cases produced any finding) and 100% true-positive coverage (36 of 36 known-bad
cases surfaced their expected rule). These numbers are not prose -- they are recomputed from the
live tool on every CI run and guarded (tests/PowerShellLsp.Corpus.Tests.ps1 fails CI if the
false-positive rate rises above zero or coverage drops below 100%, and separately floors each
scored set at 30 fixtures, so a rate cannot be made defensible by shrinking the oracle), with
the per-run report uploaded as a CI artifact. That floor is a floor, not a ratchet: it does
not pin the corpus at its present size, so a deliberate withdrawal -- as in v1.29.0 -- stays green
while it stays above the floor. The claim is measured and defensible, not exhaustive.
The invariant that makes it trustworthy: every expected finding is derived by running the REAL tool over the sample and snapshotting exactly what it emits -- never hand-authored, never model-authored. A hand-edited snapshot cannot make the test pass; it would simply disagree with the real tool.
One fact the corpus surfaced: the tool's effective default ruleset (via PSES) is narrower than
raw PSScriptAnalyzer -- it surfaces six rules on the fly and drops others the CLI flags (e.g.
PSAvoidUsingWriteHost, PSUseSingularNouns). Set ruleset = base to broaden it.
The corpus is a commons, and it is yours to use. It publishes under the project's own Apache-2.0 license -- one license across the whole repository, no separate regime for the fixtures -- so you can vendor cases into a differently licensed test suite of your own. Every file in the audited surface was authored in this repository, with no third-party rightsholder anywhere in it. Full text -- the provenance audit, the derivation invariant, the exact steps to reproduce the measurement from a clean clone, how to cite it, and what it deliberately does not attest -- is in docs/corpus.md.
Every diagnostic surfaced is also teed to a local, append-only dogfood log so real editing drives the roadmap's quality work; capture, the offline review tool, and the never-commit rules are in docs/dogfood.md.
Start with the preflight doctor. It checks prerequisites and bootstrap health in one place and prints a named fix-list. Inside an enabled session use the slash command; the raw script is the form that still works outside a session, where the slash command does not exist:
/powershell-lsp:doctor # inside an enabled Claude Code session
pwsh -File scripts/doctor.ps1 # out-of-session, from the root of a local clone
The out-of-session form needs the plugin tree's own path. scripts/doctor.ps1 is relative to
the plugin tree, not to your working directory, so it runs as written only from the root of a
local clone. If you installed with /plugin, there is no scripts/ beside your project and pwsh
exits 64 with its own usage error before the doctor runs at all. Point it at the marketplace
cache instead:
pwsh -File ~/.claude/plugins/cache/claude-powershell-lsp/powershell-lsp/<version>/scripts/doctor.ps1
<version> is not optional: the cache keeps every installed version side by side, and the highest
one is not necessarily the one serving a live session (see
Which version is actually running). Prefer the slash command
whenever a session is available -- it needs no path, and a raw run cannot see the plugin data
directory, so several checks report UNKNOWN rather than a verdict.
Each check reports PASS, a specific failure with the fix, or an honest UNKNOWN when it
genuinely cannot determine. The doctor is report-only: it never downloads, repairs, runs the
bootstrap, or starts/restarts anything.
Full text -- every check in order, why the end-to-end and active-ruleset checks exist, and the observe-only daemon-check contract -- is in docs/preflight-doctor.md.
Common symptoms -- 'pwsh' is not recognized, a leftover user-level PSES hook doubling up,
Executable not found in $PATH in the /plugin Errors tab, no diagnostics at all, a failing
handshake, and the PSES v4.6.0 PrepareRenameHandler NRE -- are each diagnosed with their fix in
docs/troubleshooting.md.
Security-control blocks on managed Windows. This plugin does exactly what locked-down estates gate: it downloads executables, runs PowerShell, and spawns a daemon. When a control blocks one at first start, the SessionStart banner names the most likely control and the legitimate remediation -- on positive evidence only, with calibrated confidence, never a guessed control. The detection table (ExecutionPolicy, Constrained Language Mode, WDAC, Defender ASR, Smart App Control) and the commands to investigate are in docs/troubleshooting.md. The plugin only ever detects and explains a block -- it never bypasses, disables, or modifies a security control.
You do not have to take this plugin's integrity on trust. The two pinned dependencies it downloads
on first run are each verified against a SHA-256 computed from the real known-good artifact
before use, and a mismatch fails closed. The pins live in scripts/ensure-pses.ps1
($PsesTag / $PsesSha256) and scripts/ensure-pssa.ps1 ($PssaVersion / $PssaSha256); the
hash table is in TRUST.md.
Every tagged release is built by this repository's gated release pipeline, which publishes a SLSA v1.0 build-provenance attestation over the release archive and a keyless gitsign (Sigstore) signature on the release tag -- both through GitHub's OIDC identity, with no maintainer-held key in the trust path:
gh release download v1.17.0 --repo manderse21/claude-powershell-lsp --pattern "*.tar.gz"
gh attestation verify powershell-lsp-1.17.0.tar.gz --repo manderse21/claude-powershell-lsp
What this does and does not prove. This is build provenance and integrity over the downloadable
source archive -- it proves the release came untampered from this repository's pipeline. It is
not Windows Authenticode and does not assert a Windows verified-publisher identity (no
SmartScreen reputation, no signed-script trust); Authenticode signing is deliberately not pursued
for a git-distributed plugin. If your estate requires signed scripts, sign them with your own
certificate: pwsh -File scripts/sign-plugin.ps1 -Thumbprint <your-code-signing-thumbprint> --
see TRUST.md, "Sign it yourself".
That is the correct boundary here: the integrity of the normal
/plugin install path rests on the git commit and the keyless-signed tag themselves, not on
the archive. The step-by-step walkthrough (including verifying the tag with gitsign, with sample
output) is in SECURITY.md, and exactly what the
provenance covers is in
docs/RELEASING.md.
The two facts a support thread opens with, and both are printed by the commands you already run for health -- as header lines above the check table, so they are there even when every check below them is UNKNOWN:
powershell-lsp doctor -- preflight self-check (report-only)
version: 1.30.0
provenance floor: v1.29.0 (earliest version-attributable release in the RETAINED lifecycle window; 41 attributable, 12 pre-floor)
/powershell-lsp:doctor and /powershell-lsp:status print both lines, identically. Neither is a
check: they contribute nothing to the of N checks count and cannot move the exit code.
- version is the plugin manifest's version, read at run time -- not a build string and not a guess. It is the same value the plugin stamps into every lifecycle record it writes. It reports the tree the doctor was run from, not the daemon currently serving your session; after an upgrade those differ (see Which version is actually running).
- provenance floor is the earliest release the plugin's own clearance data can be attributed
to. It is window-relative: the lifecycle log is a rolling family trimmed to the
keepLastNnewest files, so the floor names the earliest attributable release among the records still retained, and it rises as older records age out. It is an honesty marker, never a filter -- records below it are still counted in every rate, simply never attributed to a release.
The other renderings are honest answers rather than a fabricated floor: (none) when records exist
but none carries a usable version; (absent) when no lifecycle log has been written yet, or when
one exists but holds no record; and (undetermined) when the search ran under a fallback data root,
where "nothing was ever captured" and "this run could not find it" cannot be told apart.
The command output is the live answer -- prefer it to this page. Each figure has exactly one
source (Get-PluginVersion and Get-LifecycleProvenanceFloor), and the clearance provenance floor that scripts/rule-efficacy-ledger.ps1 prints comes from that same function, so the
readout, the ledger and this section cannot disagree. The numbers above are illustrative; yours
come from your install.
After an upgrade, the doctor's version: line and the daemon serving your session can
legitimately disagree. The upgrade replaces the tree on disk, but the daemon already running keeps
serving the session it started under -- deliberately, because restarting it mid-session would be
worse. So the doctor can report 1.31.0 with a clean 10 pass, 0 fail while a 1.30.0 daemon is
answering your edits, and both statements are true.
The version: header reports the tree. The authoritative answer for what is actually live
is the daemon's own log, which stamps its version at every start:
[2026-08-18T09:14:02.1234567-04:00] [31428] --- daemon start: powershell-lsp 1.30.0 session=<id> ...
Search for daemon start: and read the last match in logs/pses-daemon.log under your plugin
data directory (every line carries a timestamp and the daemon pid). If it names a
lower version than the doctor's header, you have upgraded but not yet restarted; start a new
session and the next daemon comes up on the new tree. The session record itself carries no version
field, so the log is the only place this is recorded.
Each live daemon writes one JSON record at <CLAUDE_PLUGIN_DATA>/session/<session-id>.json and
heartbeats it while it runs. It carries sessionId, pid, pipe, host, state, started,
heartbeat and psesPid.
You do not normally need it -- the doctor reads it for you and reports the verdict. It matters in
the one case the doctor cannot resolve on your behalf: when more than one Claude Code session is
live, the daemon check cannot tell which daemon serves this session and reports UNKNOWN. The
session directory is where the ids are. Listing it shows one file per live daemon; state and
heartbeat show which are healthy and current, and the file name is the session id you can pass to
scripts/doctor.ps1 -SessionId <session-id> to scope the check.
It is also the only liveness instrument that exists unconditionally -- it is written whether or not anything else is working, which is what makes it useful when the doctor itself cannot answer.
Evaluating this plugin for a managed or locked-down Windows estate? TRUST.md is the approve-or-deny reference: what runs locally and what never leaves the machine (no network service, no telemetry), the pinned + SHA-256-verified downloads, the CycloneDX SBOM and build provenance, the signing posture, paste-ready WDAC / AppLocker allow-list rules, and the governance / bus-factor posture. docs/trust.md assembles the verifiable chain in one place, every claim linked to a file in this repository or an artifact on the Release.
Found a vulnerability? See SECURITY.md -- report it privately via GitHub private vulnerability reporting (never a public issue).
Releases are cut by a maintainer-triggered, gate-validated pipeline -- never automatically on
push or merge. It refuses to tag unless the target commit is merged to main, green on every CI
leg, and version-matched, then cuts the keyless gitsign-signed tag and publishes a GitHub Release
with CHANGELOG-sourced notes, an SBOM, and a provenance attestation. See
docs/RELEASING.md.
This README is the pitch, the quickstart, the honest-status model, and this map. Every deep dive is one click away.
| What you want | Where it lives |
|---|---|
| Every knob -- allowed values, precedence, guards, edge cases | docs/configuration.md |
| How the warm daemon starts, serves, and shuts down | docs/warm-daemon.md |
Why a hook rather than native .lsp.json registration |
docs/native-registration.md |
| Whole-repository and CI scanning -- SARIF, exit codes, the self-scan workflow | docs/repository-scanning.md |
| Measured latency, and the figure deliberately withheld | docs/performance.md, docs/benchmarks.md |
| What the preflight doctor checks, and what it refuses to do | docs/preflight-doctor.md |
| A specific symptom and its fix | docs/troubleshooting.md |
| Which hosts are supported, and what each CI leg proves | docs/platform-support.md |
| The pinned components, and how to bump one | docs/pinned-versions.md |
| The quirks that bite when changing the runtime | docs/DEV_NOTES.md |
| Dogfood capture and the offline review tool | docs/dogfood.md |
| The correctness corpus as a commons -- license, provenance, reproduction | docs/corpus.md |
| What runs locally, what is downloaded, the signing posture | TRUST.md, docs/trust.md |
| The whole system in one document -- design rationale, measured evidence, stated limits | docs/whitepaper.md |
| How a diagnostic flows from edit to banner | ARCHITECTURE.md |
| What is frozen in 1.x, and what a change costs | CONTRACT.md |
| What changed, and when | CHANGELOG.md |
| What is next, blocked, and deferred | ROADMAP.md |
| Why a decision was made (or declined) | docs/decision-ledger.md |
| Where development is headed, as one visual map -- a derived view of ROADMAP.md and the decision ledger, refreshed at each release | docs/control-map.html |
| How a release is cut, and what provenance covers | docs/RELEASING.md |
| The single-maintainer bus factor and the fork path | CONTINUITY.md |
Contributions are welcome. Start with CONTRIBUTING.md (prerequisites, how to run the suite, the test story), ARCHITECTURE.md (how a diagnostic flows from edit to banner), and docs/DEV_NOTES.md (the quirks that bite). What is next, blocked, and deferred is in ROADMAP.md. Found a false positive? The report-a-false-positive form feeds it straight into the correctness corpus. The single-maintainer bus factor and the open-source fork path are stated honestly in CONTINUITY.md.
Git hooks (contributors). This repo ships a tracked pre-push guard that refuses a direct push
to origin/main -- main lands via a reviewed, merged PR, never a local push. Enable it once per
clone with pwsh -File scripts/install-git-hooks.ps1; it sets core.hooksPath, so the guard fires
from linked worktrees too. A deliberate one-off is allowed and audited:
POWERSHELL_LSP_ALLOW_PUSH_TO_MAIN="<reason>" git push .... See
CONTRIBUTING.md.
Apache-2.0. See LICENSE and NOTICE.
The change to Apache-2.0 is forward-only, effective from v1.32.0. Every previously
published release keeps the license it shipped under, and those grants are irrevocable: v1.0
through v1.6.0 remain MIT; v1.6.1 through v1.31.2 remain GPL-3.0-or-later. Nothing
here revokes, rescinds, or diminishes a grant already made -- if you are using a release published
before this change, your existing license is untouched.
Apache-2.0 is a permissive license, not copyleft. It adds an explicit patent grant and the NOTICE-propagation mechanics that enterprise license allow-lists are written around; it does not require a downstream fork to publish its changes, which GPLv3 did. See CONTINUITY.md for what that means for the fork path.
PowerShell Editor Services and PSScriptAnalyzer are downloaded at install time (not bundled in this repository) and remain under their own MIT licenses (Microsoft); MIT is Apache-2.0-compatible. See THIRD-PARTY-LICENSES.md.
