You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
lefthook-powershell: PSScriptAnalyzer NullReferenceException is an in-process rule race, not cross-target state #598
components/lefthook-powershell/psscriptanalyzer-staged.ps1 says PSScriptAnalyzer 1.25.0 "can leak engine state between sequential targets in one process", and the per-target pwsh worker is the fix. Measurements in melodic-software/dotfiles#774 show the failure also happens inside one fresh process analyzing one file:
ChezmoiRender.psm1 analyzed alone, one fresh pwsh -NoProfile worker per run: 15 of 60 runs failed with no other load, and 79 of 200 with 6 workers running at once.
Every captured stack goes Helper.GetExportedFunction → CommandInfo.ResolveParameter inside ProvideCommentHelp or AvoidReservedCharInCmdlet. The error is a NullReferenceException, or in 2 cases an InvalidOperationException ("more than one dynamic module in each dynamic assembly").
That path runs only when a file contains Export-ModuleMember with a parameter. Once the call was removed, the same file failed 0 of 200 runs.
Upstream maintainers say the cause is a CommandInfo cache that is not thread-safe while rules run in parallel: PowerShell/PSScriptAnalyzer#1867 (open), #2188 (closed as a duplicate), and #1708 (open; same race in UseCorrectCasing, reproduced on 1.25.0). 1.25.0 is the latest release, and upstream main still calls ResolveParameter without a guard.
Proposal
Correct the runner header: process isolation does not stop this failure.
In psscriptanalyzer-target.ps1, retry a target once when every error is RULE_ERROR with a NullReferenceException or InvalidOperationException. Findings and other errors still fail on the first run. At the measured 40% worst-case rate per run, a single retry leaves about 16% (judgment, assuming independent runs), so the README should also tell consumers that a .psm1 with a single-function Export-ModuleMember can drop the call. With no Export-ModuleMember, a script module exports all its functions and aliases (Export-ModuleMember docs).
Tradeoff: that retry would break the worker's documented no-retry, fail-closed contract, so the decision belongs to a maintainer.
Keep both lanes fail-closed and add no retry. PSScriptAnalyzer 1.25.0 is still the latest release, and main has no fix. The engine has no switch to run rules sequentially, and pinning an older version doesn't help because the race was reported in 2021 (PowerShell/PSScriptAnalyzer#1708). The durable fix is upstream PowerShell/PSScriptAnalyzer#2206, which replaces the failing CommandInfo.ResolveParameter call in Helper.GetExportedFunction. It is open and has no release date. Until it ships, the approach is to remove the constructs that trigger the race.
Triggers removed
melodic-software/claude-code-plugins: fix(machine-health): drop Export-ModuleMember from Mock-Helpers claude-code-plugins#4374 (merged). Removes Export-ModuleMember from plugins/machine-health/skills/audit/tests/helpers/Mock-Helpers.psm1. The exported set is unchanged: the same 14 functions are exported before and after, checked by importing the module. Invoke-ScriptAnalyzer on that file, one fresh pwsh per run, threw on main in 3 of 10 runs; the surfaced error was the NRE. It threw in 0 of 20 runs on the branch.
melodic-software/medley: melodic-software/medley#2038 (merged). Disables PSUseCorrectCasing (the #1708 path), with a comment that names the re-enable condition.
Not changed: claude-code-proxy
scripts/CcProxyCa.psm1 and scripts/CcProxyActivation.psm1 still call Export-ModuleMember, deliberately. Their export lists hide private helpers: CcProxyCa defines 30 functions and exports 24, and CcProxyActivation defines 31 and exports 17. Removing the call would therefore make 20 internal helpers public. It would also break tests/pester/StartProxy.Tests.ps1, whose stubbed CcProxyActivation.psm1 appends a second Export-ModuleMember -Function Get-ScheduledTask[, Get-ScheduledTaskInfo]. That works only because the two calls add their lists together. Without the base list, the generated module would export only the stubs.
The copies of Export-ModuleMember in that test file are string literals. They are written into generated modules under the system temp directory at test time. PSScriptAnalyzer does not see them as commands: the test file's AST contains 0 CommandAst nodes named Export-ModuleMember. They need no change.
Those two modules stay exposed to the race until #2206 ships. Measured locally with the repo's settings file, one fresh pwsh per run, CcProxyCa.psm1 threw the NRE in 1 of 10 runs. CcProxyActivation.psm1 was not measured. The proxy's settings file leaves PSUseCorrectCasing at its default, which is disabled. If they flake before then, there are two options:
A module manifest with FunctionsToExport, which changes how the modules are imported.
Then re-enable PSUseCorrectCasing in medley, and drop the "keep PSUseCorrectCasing disabled" guidance in the runner.
Runner header correction (proposed, not edited here because the file is synced)
The header of components/lefthook-powershell/psscriptanalyzer-staged.ps1 says:
PSScriptAnalyzer 1.25.0 can leak engine state between sequential targets in one process and intermittently throw a NullReferenceException even though every target passes in isolation.
The mechanism it describes is wrong. The failure reproduces in a single fresh process on a single file (dotfiles#774: 15/60, and 79/200 under load; the NRE above came from one file in a fresh process). The cause is the CommandInfo race between rules running in parallel inside one Invoke-ScriptAnalyzer call (PowerShell/PSScriptAnalyzer#1867, #2188). Proposed replacement:
PSScriptAnalyzer 1.25.0 runs rules in parallel against a shared cached CommandInfo, which intermittently throws a NullReferenceException on files that call Export-ModuleMember or when PSUseCorrectCasing is enabled (PowerShell/PSScriptAnalyzer#1867, #1708; fixed upstream in #2206). A fresh worker per target keeps one failure from masking other targets; it does not prevent the race.
Activity
fix(machine-health): drop Export-ModuleMember from Mock-Helpers (#4374)
kyle-sexton commentedon Sep 23, 2026
This was generated by AI during triage.
Decision
Keep both lanes fail-closed and add no retry. PSScriptAnalyzer 1.25.0 is still the latest release, and main has no fix. The engine has no switch to run rules sequentially, and pinning an older version doesn't help because the race was reported in 2021 (PowerShell/PSScriptAnalyzer#1708). The durable fix is upstream PowerShell/PSScriptAnalyzer#2206, which replaces the failing
CommandInfo.ResolveParametercall inHelper.GetExportedFunction. It is open and has no release date. Until it ships, the approach is to remove the constructs that trigger the race.Triggers removed
Export-ModuleMemberfromplugins/machine-health/skills/audit/tests/helpers/Mock-Helpers.psm1. The exported set is unchanged: the same 14 functions are exported before and after, checked by importing the module.Invoke-ScriptAnalyzeron that file, one freshpwshper run, threw on main in 3 of 10 runs; the surfaced error was the NRE. It threw in 0 of 20 runs on the branch.PSUseCorrectCasing(the #1708 path), with a comment that names the re-enable condition.Not changed: claude-code-proxy
scripts/CcProxyCa.psm1andscripts/CcProxyActivation.psm1still callExport-ModuleMember, deliberately. Their export lists hide private helpers: CcProxyCa defines 30 functions and exports 24, and CcProxyActivation defines 31 and exports 17. Removing the call would therefore make 20 internal helpers public. It would also breaktests/pester/StartProxy.Tests.ps1, whose stubbedCcProxyActivation.psm1appends a secondExport-ModuleMember -Function Get-ScheduledTask[, Get-ScheduledTaskInfo]. That works only because the two calls add their lists together. Without the base list, the generated module would export only the stubs.The copies of
Export-ModuleMemberin that test file are string literals. They are written into generated modules under the system temp directory at test time. PSScriptAnalyzer does not see them as commands: the test file's AST contains 0CommandAstnodes namedExport-ModuleMember. They need no change.Those two modules stay exposed to the race until #2206 ships. Measured locally with the repo's settings file, one fresh
pwshper run,CcProxyCa.psm1threw the NRE in 1 of 10 runs.CcProxyActivation.psm1was not measured. The proxy's settings file leavesPSUseCorrectCasingat its default, which is disabled. If they flake before then, there are two options:FunctionsToExport, which changes how the modules are imported.Sunset
Invoke-Pssa.ps1and in the lefthook component.PSUseCorrectCasingin medley, and drop the "keepPSUseCorrectCasingdisabled" guidance in the runner.Runner header correction (proposed, not edited here because the file is synced)
The header of
components/lefthook-powershell/psscriptanalyzer-staged.ps1says:The mechanism it describes is wrong. The failure reproduces in a single fresh process on a single file (dotfiles#774: 15/60, and 79/200 under load; the NRE above came from one file in a fresh process). The cause is the CommandInfo race between rules running in parallel inside one
Invoke-ScriptAnalyzercall (PowerShell/PSScriptAnalyzer#1867, #2188). Proposed replacement:jessehouwing commentedon Sep 26, 2026
@kyle-sexton it would help if you could verify the proposed fix in PowerShell/PSScriptAnalyzer#2206 solves the issue for you as well and report back on the PR.
Instructions here:
https://jessehouwing.net/fixing-complex-bugs-with-ai-deepdive-into-powershellscriptanalyzer/#so-please-go-and-break-it