Summary
cwm-roslyn-navigator holds permanent file locks on analyzer / source-generator DLLs in the NuGet global packages folder (~/.nuget/packages/**/analyzers/**) for the entire lifetime of the MCP server process. On Windows this makes dotnet restore fail with Access to the path '<Analyzer>.dll' is denied whenever a package version change requires NuGet to rewrite an already-extracted analyzer assembly.
The locks are never released while the server runs — not after the tool call completes, not on idle. The only remedy today is killing the process.
This is a downstream manifestation of the still-open Roslyn bug dotnet/roslyn#78196, amplified by the navigator's compilation cache.
Impact
Day to day this is invisible, because a normal build never rewrites an extracted analyzer. It bites specifically on package upgrades — exactly when a developer is most likely to be leaning on the navigator to understand the blast radius.
Observed on a real solution while bumping an internal package set. Three consecutive dotnet restore attempts each failed on a different analyzer DLL, because each attempt got a little further before hitting the next locked file:
error : Access to the path 'Microsoft.AspNetCore.OpenApi.SourceGenerators.dll' is denied.
error : Access to the path 'Riok.Mapperly.Abstractions.dll' is denied.
error : Access to the path 'System.ClientModel.SourceGeneration.dll' is denied.
Confirmed the lock and the holder directly:
$f = "$env:USERPROFILE\.nuget\packages\microsoft.aspnetcore.openapi\10.0.10\analyzers\dotnet\cs\Microsoft.AspNetCore.OpenApi.SourceGenerators.dll"
[System.IO.File]::Open($f,'Open','ReadWrite','None')
# The process cannot access the file ... because it is being used by another process.
Get-Process | Where-Object { $_.Modules.FileName -like "*OpenApi.SourceGenerators*" } | Select Name,Id
# cwm-roslyn-navigator x9
Killing the processes released every handle immediately and dotnet restore then succeeded first time.
Two things ruled out during diagnosis, worth recording so others don't repeat them:
dotnet build-server shutdown does not help. It clears MSBuild / VBCS nodes only; the navigator is a separate process. Our first retry still failed, just on a different DLL.
- It is not antivirus. Windows Defender real-time protection was off (
Get-MpComputerStatus → RealTimeProtectionEnabled: False), and writing a file with the identical name into a fresh directory succeeded. The denial is specifically on overwriting a file the navigator has mapped.
Root cause
dotnet/roslyn#78196 — "Roslyn locks and doesn't unlock AnalyzerReference files after calling GetCompilationAsync". Roslyn loads analyzer/generator assemblies during compilation and never releases the handles on Windows. Opened April 2025, still open, untriaged, no maintainer response, no milestone. Reported against 4.13.0; this repro is on Microsoft.CodeAnalysis 5.0.0, so it is still present there.
Since that issue shows no sign of movement, the practical fix belongs downstream — as it did for OmniSharp.
The navigator currently meets every precondition for the bug (v0.7.0, mcp/CWM.RoslynNavigator):
| Precondition |
Where |
| Default analyzer assembly loader — no shadow copy |
src/WorkspaceManager.cs:74 — MSBuildWorkspace.Create(). No ShadowCopy / AnalyzerAssemblyLoader anywhere in src/. |
| Calls the exact trigger |
GetCompilationAsync — 15 call sites across SymbolResolver.cs, WorkspaceManager.cs, GetDiagnosticsTool, FindDeadCodeTool, GetTestCoverageMapTool, DetectAntiPatternsTool, DetectCircularDependenciesTool, GetDependencyGraphTool |
| Deliberately retains compilations |
src/WorkspaceManager.cs:21 MaxCachedCompilations = 30; :25 ConcurrentDictionary<ProjectId, Compilation> _compilationCache |
| Long-lived host |
MCP server runs for the whole editor session |
The cache is the amplifier: even after a tool call returns, up to 30 Compilation objects — and therefore their AnalyzerReference assemblies — are held deliberately. Dispose() (:430-433) and the workspace reload path (:71-72) are the only things that let go, and neither runs during normal operation.
Secondary problem: process accumulation
There were 9 cwm-roslyn-navigator processes alive simultaneously on one machine, each holding its own handle set. Even a perfect per-process fix leaves orphans locking files if instances accumulate across sessions/worktrees. Worth treating as a separate defect.
Suggested fix
Primary: shadow-copy the analyzer assemblies
This is the proven remedy and it is what OmniSharp adopted for the identical symptom — OmniSharp/omnisharp-roslyn#1465 (closed by fix), see also PR #2236 "Reuse Roslyn's analyzer assembly loader".
Roslyn ships ShadowCopyAnalyzerAssemblyLoader precisely for hosts that must not lock the originals. Loading analyzers from a shadow copy means the files in ~/.nuget/packages are never mapped, so NuGet can always rewrite them.
This fixes the root cause with no behavioural trade-off — no lost cache, no dropped warm state, no surprise restarts. Everything below is a mitigation for the case where this proves impractical.
Requested discussion: lifetime strategy
Filing this partly to ask which lifetime model you want, because the mitigations differ in how much they disturb expected long-lived-host usage (VS-style warm caching, where dropping the workspace on a timer would be a regression):
a) Terminate on completion — dispose the workspace after each tool call. Guarantees no lingering locks, but destroys the compilation cache and makes every call a cold load. On a large solution that is a severe latency regression and would defeat the purpose of MaxCachedCompilations. Not recommended as a default.
b) Configurable idle timeout — release the workspace (and its analyzer handles) after N seconds of inactivity, reload lazily on next use. Keeps the cache warm during active work and frees locks while the developer is at a terminal running restore/build — which is exactly when the conflict occurs. Reasonable default in the 60–300s range, opt-out via 0/disabled for users who prefer permanent warmth. This is the best behaviour-preserving mitigation if shadow copy cannot be adopted.
c) General/global termination — a blunt shutdown of the server. Effectively what users do by hand today. Fine as an explicit escape hatch, wrong as automatic policy.
d) Explicit release tool — an MCP tool (e.g. release_workspace / unlock) letting the agent drop handles on demand before a restore, then reload. Cheap to implement, composes well with (b), and keeps the decision with the caller rather than a timer.
Suggested combination: (a-primary) shadow copy as the actual fix, plus (d) as an immediate escape hatch, and (b) as a configurable safety net — with all timer-based behaviour off by default so VS-style long-lived usage is unaffected unless opted into.
Environment
|
|
| Plugin |
dotnet-claude-kit 0.7.0, mcp/CWM.RoslynNavigator |
| TFM |
net10.0 |
| Roslyn |
Microsoft.CodeAnalysis.Workspaces.MSBuild 5.0.0, Microsoft.CodeAnalysis.CSharp.Workspaces 5.0.0 |
| MSBuild locator |
Microsoft.Build.Locator 1.7.8 |
| SDK |
.NET SDK 10.0.302 |
| OS |
Windows 11 Pro 26200 |
| Defender RTP |
Disabled (ruled out as a cause) |
Repro
- Open a multi-project solution so the navigator loads the workspace.
- Run any tool that reaches
GetCompilationAsync (e.g. get_diagnostics, find_dead_code, detect_antipatterns).
- Change a
PackageReference version such that a transitive analyzer/source-generator package resolves to a different version.
dotnet restore.
Expected: restore succeeds.
Actual: error : Access to the path '<Analyzer>.dll' is denied. Repeats on a different analyzer each attempt until the navigator process is killed.
References
Current workaround
Get-Process -Name cwm-roslyn-navigator -ErrorAction SilentlyContinue | Stop-Process -Force
dotnet restore
# then reconnect the MCP server (/mcp in Claude Code)
Note dotnet build-server shutdown is not sufficient — it does not touch the navigator process.
Summary
cwm-roslyn-navigatorholds permanent file locks on analyzer / source-generator DLLs in the NuGet global packages folder (~/.nuget/packages/**/analyzers/**) for the entire lifetime of the MCP server process. On Windows this makesdotnet restorefail withAccess to the path '<Analyzer>.dll' is deniedwhenever a package version change requires NuGet to rewrite an already-extracted analyzer assembly.The locks are never released while the server runs — not after the tool call completes, not on idle. The only remedy today is killing the process.
This is a downstream manifestation of the still-open Roslyn bug dotnet/roslyn#78196, amplified by the navigator's compilation cache.
Impact
Day to day this is invisible, because a normal build never rewrites an extracted analyzer. It bites specifically on package upgrades — exactly when a developer is most likely to be leaning on the navigator to understand the blast radius.
Observed on a real solution while bumping an internal package set. Three consecutive
dotnet restoreattempts each failed on a different analyzer DLL, because each attempt got a little further before hitting the next locked file:Confirmed the lock and the holder directly:
Killing the processes released every handle immediately and
dotnet restorethen succeeded first time.Two things ruled out during diagnosis, worth recording so others don't repeat them:
dotnet build-server shutdowndoes not help. It clears MSBuild / VBCS nodes only; the navigator is a separate process. Our first retry still failed, just on a different DLL.Get-MpComputerStatus→RealTimeProtectionEnabled: False), and writing a file with the identical name into a fresh directory succeeded. The denial is specifically on overwriting a file the navigator has mapped.Root cause
dotnet/roslyn#78196 — "Roslyn locks and doesn't unlock AnalyzerReference files after calling
GetCompilationAsync". Roslyn loads analyzer/generator assemblies during compilation and never releases the handles on Windows. Opened April 2025, still open, untriaged, no maintainer response, no milestone. Reported against 4.13.0; this repro is on Microsoft.CodeAnalysis 5.0.0, so it is still present there.Since that issue shows no sign of movement, the practical fix belongs downstream — as it did for OmniSharp.
The navigator currently meets every precondition for the bug (v0.7.0,
mcp/CWM.RoslynNavigator):src/WorkspaceManager.cs:74—MSBuildWorkspace.Create(). NoShadowCopy/AnalyzerAssemblyLoaderanywhere insrc/.GetCompilationAsync— 15 call sites acrossSymbolResolver.cs,WorkspaceManager.cs,GetDiagnosticsTool,FindDeadCodeTool,GetTestCoverageMapTool,DetectAntiPatternsTool,DetectCircularDependenciesTool,GetDependencyGraphToolsrc/WorkspaceManager.cs:21MaxCachedCompilations = 30;:25ConcurrentDictionary<ProjectId, Compilation> _compilationCacheThe cache is the amplifier: even after a tool call returns, up to 30
Compilationobjects — and therefore theirAnalyzerReferenceassemblies — are held deliberately.Dispose()(:430-433) and the workspace reload path (:71-72) are the only things that let go, and neither runs during normal operation.Secondary problem: process accumulation
There were 9
cwm-roslyn-navigatorprocesses alive simultaneously on one machine, each holding its own handle set. Even a perfect per-process fix leaves orphans locking files if instances accumulate across sessions/worktrees. Worth treating as a separate defect.Suggested fix
Primary: shadow-copy the analyzer assemblies
This is the proven remedy and it is what OmniSharp adopted for the identical symptom — OmniSharp/omnisharp-roslyn#1465 (closed by fix), see also PR #2236 "Reuse Roslyn's analyzer assembly loader".
Roslyn ships
ShadowCopyAnalyzerAssemblyLoaderprecisely for hosts that must not lock the originals. Loading analyzers from a shadow copy means the files in~/.nuget/packagesare never mapped, so NuGet can always rewrite them.This fixes the root cause with no behavioural trade-off — no lost cache, no dropped warm state, no surprise restarts. Everything below is a mitigation for the case where this proves impractical.
Requested discussion: lifetime strategy
Filing this partly to ask which lifetime model you want, because the mitigations differ in how much they disturb expected long-lived-host usage (VS-style warm caching, where dropping the workspace on a timer would be a regression):
a) Terminate on completion — dispose the workspace after each tool call. Guarantees no lingering locks, but destroys the compilation cache and makes every call a cold load. On a large solution that is a severe latency regression and would defeat the purpose of
MaxCachedCompilations. Not recommended as a default.b) Configurable idle timeout — release the workspace (and its analyzer handles) after N seconds of inactivity, reload lazily on next use. Keeps the cache warm during active work and frees locks while the developer is at a terminal running
restore/build— which is exactly when the conflict occurs. Reasonable default in the 60–300s range, opt-out via0/disabled for users who prefer permanent warmth. This is the best behaviour-preserving mitigation if shadow copy cannot be adopted.c) General/global termination — a blunt shutdown of the server. Effectively what users do by hand today. Fine as an explicit escape hatch, wrong as automatic policy.
d) Explicit release tool — an MCP tool (e.g.
release_workspace/unlock) letting the agent drop handles on demand before a restore, then reload. Cheap to implement, composes well with (b), and keeps the decision with the caller rather than a timer.Suggested combination: (a-primary) shadow copy as the actual fix, plus (d) as an immediate escape hatch, and (b) as a configurable safety net — with all timer-based behaviour off by default so VS-style long-lived usage is unaffected unless opted into.
Environment
dotnet-claude-kit0.7.0,mcp/CWM.RoslynNavigatornet10.0Microsoft.CodeAnalysis.Workspaces.MSBuild5.0.0,Microsoft.CodeAnalysis.CSharp.Workspaces5.0.0Microsoft.Build.Locator1.7.8Repro
GetCompilationAsync(e.g.get_diagnostics,find_dead_code,detect_antipatterns).PackageReferenceversion such that a transitive analyzer/source-generator package resolves to a different version.dotnet restore.Expected: restore succeeds.
Actual:
error : Access to the path '<Analyzer>.dll' is denied.Repeats on a different analyzer each attempt until the navigator process is killed.References
GetCompilationAsync(open, untriaged)Current workaround
Note
dotnet build-server shutdownis not sufficient — it does not touch the navigator process.