If you’ve made it this far I hope the journey helped understand the module.

The IntuneScriptLab series ran ten posts. The first laid out what the module is for, and each one after it took a single piece apart: the rules, the harness, Win32 apps, filters, logs, the tenant, the analyzer wrapper, CI and the lab. That order makes sense for explaining a module. It’s the wrong order for someone with a remediation misbehaving in production, who starts from what the portal shows and wants to know which command to run.

This post is the index for that reader, and each row points into the post that covers it.

The module is on the PowerShell Gallery as 0.34.0, with 25 commands, 15 rules and the seven Should-* Pester assertions:

1
Install-PSResource -Name IntuneScriptLab -Scope CurrentUser

Start from what the portal shows you

What you seeStart withRead
A remediation reports “Without issues” on a device that still has the problemTest-IntuneScript, then Invoke-IntuneRemediationTestLinting, running
“Recurred” or “Failed” after a remediation that seemed to workInvoke-IntuneRemediationTestRunning
The output column shows the wrong line, or garbled charactersTest-IntuneScriptLinting
The script works in your console and fails from IntuneInvoke-IntuneRemediationTest or Invoke-IntunePlatformScriptTest with -Context SystemRunning
A Win32 app ends at 0x87D1041C, not detected after installationInvoke-IntuneDetectionTest, Test-IntuneWin32RuleInstalled
A Win32 app reports Not applicableInvoke-IntuneRequirementTest, Test-IntuneWin32Requirement, Test-IntuneAssignmentFilterInstalled, filters
A policy never shows up on a deviceTest-IntuneDeployedScript, then Get-IntuneAgentTimeline on the deviceTenant, logs
The portal hasn’t caught up with the deviceGet-IntuneScriptHealth, Get-IntuneAgentLogTenant, logs
The script in the tenant isn’t the one in gitCompare-IntuneDeployedScriptTenant

The static check comes first where it can, because it’s the cheapest. Test-IntuneScript needs only the file: no tenant, no elevation, and it runs on Linux too, where the folder names that tell it what a script is work with forward slashes just the same. The rows that start somewhere else are the ones a file alone can’t answer.

“Without issues” on a device that has the problem

This is the classic. A detection that ends without an exit statement exits 0, and so does one that uses return 1 at script scope, the pattern in Microsoft’s own sample. Either way the agent reads “no issue” and skips the remediation. IslExitCodeIssue catches both statically, and the harness shows you the status the portal would show.

The other way round catches people too. A remediation runs on any non-zero exit, so a detection that throws or fails to parse under Windows PowerShell 5.1 triggers a remediation nobody planned.

Recurred, Failed and the output column

Remediation results depend on more than exit codes. A remediation that writes to stderr is reported as failed even when it exits 0, and the post-detection is skipped. The output column holds the last line of the console output, host streams included, and only its last 2,048 characters. Write-Host at the end of a detection becomes the portal’s text. A platform script’s result is the opposite: the whole output, untruncated.

Garbled characters usually start in the file. Windows PowerShell 5.1 reads UTF-8 without a BOM as ANSI, so Grüße arrives as Grüße. IslEncodingIssue reports a non-ASCII script without a BOM, and Repair-IntuneScript adds one.

Works in your console, fails from Intune

Your console is 64-bit, interactive and running as you. Intune runs remediations and platform scripts in Windows PowerShell 5.1, 32-bit by default when the policy was made in the portal, often as SYSTEM in session 0. The drives you mapped don’t exist there, and without -NonInteractive a prompt waits until the timeout. The harness reproduces that launch, as SYSTEM through a one-shot scheduled task or as another account through a task in that account’s session. IslArchitectureIssue, IslContextIssue and IslInteractiveCall cover the static side of the same differences.

Win32 apps: detected, installed, applicable

A detection script counts as installed only with exit 0 and something on stdout. Anything on stderr makes it not detected, even with both. Write-Host counts as stdout and Write-Warning doesn’t count as stderr. Requirement scripts compare their whole stdout, case-insensitively for strings. The file, registry and MSI rules have their own semantics, mostly as documented, with one rule type that Graph accepts and the agent doesn’t evaluate. The Win32 post walks through each one, and Invoke-IntuneWin32AppTest runs the detect, install, detect flow locally.

Not applicable has more causes than requirement rules. A base requirement, an assignment filter that no Windows device can match, or a user-context app assigned to a device group end there too. Test-IntuneAssignmentFilter parses a filter rule the way the service does and evaluates it against your machine or a device you describe.

A policy that never shows up

Some assignments don’t reach a device. A policy with no assignment, or with only an exclusion, doesn’t appear in a device’s resolved policies. A user-context script on an Entra registered device is downloaded and skipped. Test-IntuneDeployedScript reports both from the tenant. When the tenant looks right, the device’s own logs say what the agent decided, and Get-IntuneAgentTimeline puts one policy’s lines in order.

The portal is behind

A remediation result can take up to an hour to reach Graph, because the agent uploads it with its next hourly report. A result that didn’t change isn’t reported again. Platform scripts and Win32 apps report within seconds. Get-IntuneScriptHealth shows what the tenant knows, and the agent log on the device is ahead of it.

One symptom, start to finish

This is the 0x87D1041C row worked through. This Win32 detection script looks reasonable. It sits at Win32\Widget\Detect.ps1, and Test-IntuneScript reads the folder to know it’s a Win32 detection. A lone Detect-Widget.ps1 would be read as a remediation’s detection and checked against different rules, so name the folder or pass -ScriptType Win32Detection:

1
2
3
$version = Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\Contoso\Widget' -Name Version
if ([version]$version -ge [version]'2.1') { exit 0 }
exit 1

Test-IntuneScript finds two problems before anything runs. I’ve left out the Information rows:

1
2
3
4
5
6
7
8
9
Line     : 2
Severity : Error
RuleName : IslOutputIssue
Message  : Nothing is ever written to stdout: exit 0 alone means "not detected". Write-Output a line before exit 0 on the installed path

Line     : 1
Severity : Warning
RuleName : IslOutputIssue
Message  : Get-ItemPropertyValue writes an error record to stderr when its target is missing, which makes the app 'not detected'. Use -ErrorAction SilentlyContinue (or Test-Path first) on the not-installed path

The Error is the 0x87D1041C. On a device with Widget 2.1 installed, the script exits 0 with nothing on stdout, the agent reads that as not detected, runs the install, still doesn’t detect the app and reports that code. The Warning is the other branch. On my desktop, without the app, Invoke-IntuneDetectionTest gives the answer the agent would (StdErr is the full error record; its first line is shown):

1
2
3
4
5
6
Detected        : False
Reason          : Exit code 1: only exit 0 can mean installed; Nothing on stdout: exit 0 alone is "not detected"; Output on stderr: any error output means "not detected" even with exit 0 and stdout
ExitCode        : 1
StdErr          : Get-ItemPropertyValue : Cannot find path 'HKLM:\SOFTWARE\Contoso\Widget' because it does not exist.
Architecture    : x64
Context         : User

Not detected is the right verdict on a machine without Widget, for three reasons where one should do. The fix is a line of output on the installed path and an -ErrorAction on the lookup:

1
2
3
4
5
6
$version = Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\Contoso\Widget' -Name Version -ErrorAction SilentlyContinue
if ($version -and [version]$version -ge [version]'2.1') {
    Write-Output "Widget $version"
    exit 0
}
exit 1

Test-IntuneScript then reports no warnings or errors, and the harness says not detected with an empty StdErr and only the exit code and the missing output as reasons. The agent runs detection scripts as SYSTEM; -Context System does the same from an elevated session, and Invoke-IntuneWin32AppTest runs the install between two detections when you have the package.

Where each piece runs

On your workstation, Test-IntuneScript and the harness run against the file you’re editing. The PSScriptAnalyzer wrapper puts the same rules into Invoke-ScriptAnalyzer and the editor, with one gotcha: filtering with -Severity Error drops the Intune findings, because each custom rule registers as a warning.

In a pull request, the gate script and workflow template annotate the diff, write a job summary and optionally a SARIF log for code scanning. The Pester assertions and the suite template run the harness on a Windows runner. The two halves catch different bugs: the static check failed the return 1 detection that the runtime test passed.

Against the tenant, the audit commands read the deployed scripts through Graph, read-only, and check them with the policy’s own bitness and context.

On a device, the log readers turn the agent’s CMTrace logs into objects and timelines, and Export-IntuneAgentDiagnostic packs them up for someone else to read.

My two main uses have been Test-IntuneScript as I’m writing and then Get-IntuneAgentLog to troubleshoot.

In the lab, the validation kit is where the rules’ evidence came from. It isn’t part of the Gallery package.

Where the documentation and the devices disagreed

Each rule cites an experiment, and the experiments were run against what Microsoft Learn says. Most of the documentation held up. These are the places it didn’t, as Findings.md records them:

DocumentedObservedExperiment
A remediation runs only when detection exits 1Any non-zero exit runs itREM-EXIT-2, REM-EXIT-NEG1
Microsoft’s sample detection returns before exit 1return 1 ends the script with exit 0, so the remediation is skippedREM-RETURN-EXIT
Remediations need Entra joined or hybrid joined devicesSYSTEM-context remediations run on Entra registered devices; user-context ones are skippedRound 1, both join types
Registered devices don’t receive platform scriptsThe same: SYSTEM runs, user context is skippedRound 1, both join types
Scripts are UTF-8 (or ASCII)UTF-8 without a BOM is decoded as ANSIRounds 1 and 2
A failed platform script retries three timesThree runs in total, at policy fetches every eight hoursPS-FAIL, PS-FAIL-2
Scripts must be under 200 KBA 504 KB remediation and a 660 KB platform script were accepted; a 500 KB script ranPS-SIZE-500KB, REM-SIZE-250KB
File “does not exist” is a detection ruleGraph accepts it and the agent doesn’t evaluate itW32-FILE-NOTEXIST, W32-FILE-NOTEXIST-FALSE
An exclusion wins over an includeInclude and exclude of the same group: the service drops the exclusion and the policy runsASSIGN-INEX
A Win32 app’s isAssigned says whether it’s assignedStill false 30 minutes and more after assignmentRound 2
mobileApps/{id}/deviceStatuses returns install statusGone from beta; the export job is the working pathRound 2

The include-and-exclude row was wrong in Findings for a while, and so was the tenant audit post. The round watched for 55 minutes and saw no run, and the device’s probe file showed weeks later that the policy had been running hourly. Both are corrected now. When a device disagrees with a finding, the experiment id in the evidence string leads to the script that ran, in the repository.

One of the benefits of writing this series has been catching the errors that had slipped by and be able to correct them.

Gaps in the evidence

The rules were measured on Windows 11 Enterprise LTSC 24H2 VMs. The Intune agent’s behavior on Windows on ARM is still marked pending in Findings. The one ARM64 device available runs Windows 11 Home; enrolled in the lab tenant in October 2026 with 27 experiments assigned, it synced eleven times in 75 minutes and never received the agent. The documentation says Home devices don’t get it, and this one didn’t. So the module knows what the hosts look like there, from a local survey, and nothing about what the agent does with them.

Unfortunately while I do have a ARM-based Surface, I haven’t needed to upgrade it from Home to Professional, which hurts when I’m testing things like this.

The harness reproduces the launch. It doesn’t fetch policy, queue runs, report to Graph or apply the Enrollment Status Page’s ordering. The log post covers those from the device’s side.

The tenant view is limited to what Graph returns. An exclusion the service dropped leaves no trace to read, and run states lag the devices.

I’m still trying to figure out what to do for my next series of posts, or module to tackle. I’d love to hear suggestions in the comments.

The module’s source, the validation kit and the full findings are in the IntuneScriptLab repository. The module facts match IntuneScriptLab 0.34.0; the ARM64 Home round and the Linux folder-name test are on main, after that release.