SCRIPT LIBRARY · POWERSHELL
Forcing a Group Policy Refresh with PowerShell and SCCM
A gpupdate wrapper that checks the domain trust first, never bounces anyone's session, and reports a real success or failure back to ConfigMgr.
- What it does
- Confirms the device is domain-joined and its secure channel is healthy, runs gpupdate for the computer, user or both, answers "No" to any logoff or reboot prompt, and returns the exit code and the tail of the output.
- Requires
- Windows PowerShell 5.1 (Test-ComputerSecureChannel is 5.1 only; the check is skipped on PowerShell 7)
- A domain-joined device
- Permissions
- Local administrator or SYSTEM for computer policy. User policy has to run as the signed-in user.
- Runs on
- Windows 10/11, Windows Server 2016+
- Tested
- Parse-checked and dry-run with mocked gpupdate, CIM and secure channel calls in PowerShell 7.4
You've just changed a GPO, the change is needed now, and the default refresh interval is 90 minutes plus a random offset of up to 30. So you want to kick a refresh on a group of machines, and you want to know which ones actually took it.
The old version of this post was mostly ConfigMgr packaging code wrapped around a one-line gpupdate /force. That line did the job on a good day. On a bad day it could sit waiting for a "log off now?" answer nobody was there to give, and it reported success no matter what gpupdate said.
This version does the checks I'd do by hand first. Is the machine on the domain at all? Can it still talk to a domain controller over its secure channel? Then it runs the refresh without letting it log anyone off, and passes gpupdate's exit code back so the ConfigMgr deployment status tells the truth.
<#
.SYNOPSIS
Forces a Group Policy refresh on the local device and reports whether it worked.
.DESCRIPTION
Checks that the device is domain-joined and still trusts the domain, then runs
gpupdate /force (or a lighter changes-only refresh) for the target you choose, answering "No" to any logoff or reboot
prompt so nothing gets bounced mid-afternoon. Returns an object with the exit code
and the tail of gpupdate's output, and exits non-zero on failure so ConfigMgr
reports it correctly.
.PARAMETER Target
Computer, User or Both. User policy only makes sense when the script runs as the
signed-in user, not as SYSTEM.
.PARAMETER WaitSeconds
How long gpupdate waits for policy processing to finish before returning.
.PARAMETER ChangedOnly
Skip /force and apply only settings that changed. Much lighter on domain
controllers when you're hitting a whole collection.
.PARAMETER SkipSecureChannelCheck
Don't test the computer's trust relationship with the domain first.
.EXAMPLE
.\Invoke-GroupPolicyRefresh.ps1
.EXAMPLE
.\Invoke-GroupPolicyRefresh.ps1 -Target Both -WaitSeconds 300 -WhatIf
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidateSet('Computer', 'User', 'Both')]
[string]$Target = 'Computer',
[ValidateRange(0, 3600)]
[int]$WaitSeconds = 120,
[switch]$ChangedOnly,
[switch]$SkipSecureChannelCheck
)
$result = [pscustomobject]@{
ComputerName = $env:COMPUTERNAME
Target = $Target
DomainJoined = $null
SecureChannel = $null
ExitCode = $null
Result = $null
Output = $null
}
$system = Get-CimInstance -ClassName Win32_ComputerSystem
$result.DomainJoined = [bool]$system.PartOfDomain
if (-not $result.DomainJoined) {
$result.Result = 'Skipped (not domain-joined)'
return $result
}
if ($Target -ne 'Computer' -and [Security.Principal.WindowsIdentity]::GetCurrent().IsSystem) {
Write-Warning 'Running as SYSTEM, so the user half of this refresh applies to SYSTEM, not to whoever is signed in.'
}
# A broken trust relationship makes gpupdate fail in confusing ways. Check it up front.
if (-not $SkipSecureChannelCheck -and (Get-Command -Name Test-ComputerSecureChannel -ErrorAction SilentlyContinue)) {
try { $result.SecureChannel = Test-ComputerSecureChannel -ErrorAction Stop }
catch { $result.SecureChannel = $false; Write-Verbose "Secure channel test failed: $($_.Exception.Message)" }
if (-not $result.SecureChannel) {
$result.Result = 'Failed (no secure channel to the domain)'
Write-Error 'This computer cannot talk to a domain controller over its secure channel. Fix the trust relationship first.'
$result
exit 1
}
}
$arguments = @("/wait:$WaitSeconds")
if (-not $ChangedOnly) { $arguments = @('/force') + $arguments }
if ($Target -ne 'Both') { $arguments = @("/target:$($Target.ToLower())") + $arguments }
if (-not $PSCmdlet.ShouldProcess($env:COMPUTERNAME, "gpupdate $($arguments -join ' ')")) {
$result.Result = 'WhatIf'
return $result
}
Write-Verbose "Running gpupdate $($arguments -join ' ')"
# Pipe "N" so a policy that wants a logoff or reboot doesn't get one.
$output = 'N' | & gpupdate.exe @arguments 2>&1
$result.ExitCode = $LASTEXITCODE
$result.Output = (@($output | ForEach-Object { "$_".Trim() } | Where-Object { $_ }) | Select-Object -Last 4) -join ' | '
$result.Result = if ($result.ExitCode -eq 0) { 'Success' } else { 'Failed' }
$result
if ($result.ExitCode -ne 0) { exit $result.ExitCode }
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-Target | string | Computer | Computer, User or Both. Running as SYSTEM, only Computer really means anything. |
-WaitSeconds | int | 120 | How long gpupdate waits for processing to finish before handing control back. Policy keeps applying in the background after that. |
-ChangedOnly | switch | — | Drop /force and apply only what changed. Use this when you're refreshing a big collection. |
-SkipSecureChannelCheck | switch | — | Don't test the trust relationship first. Handy on a VPN that's slow to come up, where you'd rather let gpupdate try anyway. |
Run it
Refresh computer policy on this machine.
.\Invoke-GroupPolicyRefresh.ps1See exactly what gpupdate command it would run.
.\Invoke-GroupPolicyRefresh.ps1 -Target Both -WhatIfA lighter refresh across a whole collection, as a ConfigMgr program.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Invoke-GroupPolicyRefresh.ps1 -ChangedOnlyHit a few machines remotely and see which ones failed.
Invoke-Command -ComputerName PC-0142, PC-0187 -FilePath .\Invoke-GroupPolicyRefresh.ps1 | Where-Object Result -ne 'Success'What you'll see
ComputerName : PC-0142
Target : Computer
DomainJoined : True
SecureChannel : True
ExitCode : 0
Result : Success
Output : Updating policy... | Computer Policy update has completed successfully.
How it works
- Is it even on the domain?
Win32_ComputerSystem.PartOfDomainanswers that. Workgroup machines are skipped and marked as such, instead of showing up as failures. - Can it reach a DC? On Windows PowerShell 5.1,
Test-ComputerSecureChannelchecks the machine account's trust with the domain. A broken trust is the classic reason gpupdate throws vague errors, so it's worth catching up front. - Build the command.
/target:computeror/target:user(or neither, for both), plus/forceunless you asked for-ChangedOnly, plus/wait. - Run it without the drama. The script pipes
Ninto gpupdate, so if a policy wants a logoff or restart, the answer is no. Standard output and errors are captured together. - Tell the truth. The last few lines of output go into the result object, and a non-zero gpupdate exit code becomes the script's exit code. ConfigMgr marks the deployment failed on the machines where it actually failed.
For a quick refresh on a handful of machines, ConfigMgr's Run Scripts is the easiest route: approve the script once and run it against a collection straight from the console. A package with a program still makes sense if you want it scheduled or repeated.
One habit from the old version I've dropped on purpose: deleting and recreating the ConfigMgr package every time the script ran. That wipes the deployment history and breaks existing deployments. Update the package content instead.
Take it further
- Check what actually applied.
gpresult /r /scope computeron one of the machines shows the GPOs that applied, and the ones that were filtered out and why. - Use the GPMC for an OU. Right-click an OU in Group Policy Management and choose Group Policy Update. It uses the same remote task trick as
Invoke-GPUpdateand needs the remote scheduled task firewall rules open. - Log it. Pipe the results to
Export-Csvfrom anInvoke-Commandrun and you've got a record of which machines had the new policy, and when.
Things that'll trip you up
- SYSTEM doesn't have your user's policy. A ConfigMgr program runs as SYSTEM, so a user refresh from there refreshes SYSTEM's user policy, which isn't what you meant. For user settings, deploy it to run with the user's rights, or use Invoke-GPUpdate from the GroupPolicy module, which schedules a task in each signed-in user's session.
- Some settings still need a logoff or reboot. Software installation, folder redirection and a few others only apply at startup or sign-in. The script answers "No" to the prompt on purpose, so those wait for the next natural restart instead of bouncing someone mid-call.
- /force on 2,000 machines at once hurts. Every machine re-downloads and re-applies everything, all at the same domain controllers. Use -ChangedOnly for big collections, or stagger the deployment.
- Off-network machines will fail, and that's accurate. No domain controller, no policy. A failure on a laptop that's at home without VPN is the right answer, not a bug.
- A broken trust relationship stops here. If the secure channel test fails, the script stops and says so. Rejoining the domain or running Test-ComputerSecureChannel -Repair on the machine is the fix, not another gpupdate.