SCRIPT LIBRARY · POWERSHELL
Adding Domain Groups to Local Administrators on Remote PCs with PowerShell
Put a domain group into the local Administrators group on a list of computers (or take one out) over PowerShell remoting, with a result for every machine and -WhatIf first.
- What it does
- Adds or removes domain groups (or users) in the built-in Administrators group on remote Windows computers through PowerShell remoting. Returns one result per computer and member, and supports -WhatIf.
- Requires
- Windows PowerShell 5.1 or PowerShell 7 on the machine you run it from
- PowerShell remoting (WinRM) enabled on the targets
- Windows PowerShell 5.1 on the targets (for the LocalAccounts cmdlets)
- Permissions
- Local administrator on each target computer.
- Runs on
- Windows 10/11 and Windows Server 2016+ targets
- Tested
- Parse-checked and dry-run with mocked remoting and LocalAccounts cmdlets in PowerShell 7.4
Part 4 of the thread Local admin, done right
Using a security group for local admin rights beats adding people one by one. You manage membership in one place, and when someone changes roles you take them out of a group instead of visiting 300 machines. The hard part is getting the group onto those 300 machines in the first place.
The original version of this post didn't work, which I only found out when I went back to it. Add-LocalGroupMember doesn't have a -ComputerName parameter, so it quietly can't reach remote machines, and a "$computer:" inside a string is a parse error before anything even runs. This rewrite runs the cmdlet on each computer through Invoke-Command, finds the Administrators group by its well-known SID so it works on non-English Windows, and can remove members as well as add them.
Every computer and member gets a result: Done, Skipped (already there, or already gone), WhatIf or Failed, with the reason. One machine that's switched off doesn't stop the rest of the run.
<#
.SYNOPSIS
Adds (or removes) domain groups in the local Administrators group on remote computers.
.DESCRIPTION
Uses PowerShell remoting to run Add-LocalGroupMember or Remove-LocalGroupMember on each
computer. The built-in Administrators group is found by its well-known SID (S-1-5-32-544),
so it works on non-English Windows too. Each computer and member gets its own result row,
and one unreachable machine doesn't stop the rest. Supports -WhatIf.
.PARAMETER ComputerName
Computers to change. Accepts pipeline input, so Get-Content .\computers.txt | works.
.PARAMETER Member
Domain groups (or users) to add or remove, e.g. CONTOSO\Tier2-Workstation-Admins.
.PARAMETER Remove
Remove the members instead of adding them.
.PARAMETER Credential
Alternate credentials for the remoting connection.
.EXAMPLE
Get-Content .\computers.txt | .\Set-LocalAdminMember.ps1 -Member 'CONTOSO\Tier2-Workstation-Admins' -WhatIf
.EXAMPLE
.\Set-LocalAdminMember.ps1 -ComputerName PC-0142, PC-0187 -Member 'CONTOSO\Old-Helpdesk' -Remove
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
[Alias('Name', 'DNSHostName')][AllowEmptyString()][string[]]$ComputerName,
[Parameter(Mandatory)][ValidatePattern('^[^\\]+\\[^\\]+$')][string[]]$Member,
[switch]$Remove,
[pscredential]$Credential
)
begin {
$verb = if ($Remove) { 'Remove' } else { 'Add' }
$prep = if ($Remove) { 'from' } else { 'to' }
# Runs on the target computer. Returns one result per member.
$remoteBlock = {
param([string[]]$Members, [bool]$Remove)
$adminsSid = 'S-1-5-32-544'
foreach ($m in $Members) {
try {
if ($Remove) { Remove-LocalGroupMember -SID $adminsSid -Member $m -ErrorAction Stop }
else { Add-LocalGroupMember -SID $adminsSid -Member $m -ErrorAction Stop }
[pscustomobject]@{ Member = $m; Result = 'Done'; Detail = '' }
}
catch [Microsoft.PowerShell.Commands.MemberExistsException] {
[pscustomobject]@{ Member = $m; Result = 'Skipped'; Detail = 'Already a member' }
}
catch [Microsoft.PowerShell.Commands.MemberNotFoundException] {
[pscustomobject]@{ Member = $m; Result = 'Skipped'; Detail = 'Not a member' }
}
catch {
[pscustomobject]@{ Member = $m; Result = 'Failed'; Detail = $_.Exception.Message }
}
}
}
}
process {
foreach ($computer in $ComputerName) {
$computer = $computer.Trim()
if (-not $computer) { continue }
$approved = @($Member | Where-Object { $PSCmdlet.ShouldProcess($computer, "$verb $_ $prep local Administrators") })
foreach ($m in $Member | Where-Object { $approved -notcontains $_ }) {
[pscustomobject]@{ ComputerName = $computer; Action = $verb; Member = $m; Result = 'WhatIf'; Detail = '' }
}
if (-not $approved) { continue }
$invoke = @{ ComputerName = $computer; ScriptBlock = $remoteBlock; ArgumentList = @($approved, [bool]$Remove); ErrorAction = 'Stop' }
if ($Credential) { $invoke.Credential = $Credential }
try {
foreach ($r in Invoke-Command @invoke) {
[pscustomobject]@{ ComputerName = $computer; Action = $verb; Member = $r.Member; Result = $r.Result; Detail = $r.Detail }
}
}
catch {
# Couldn't reach the machine at all: report every member as failed.
foreach ($m in $approved) {
[pscustomobject]@{ ComputerName = $computer; Action = $verb; Member = $m; Result = 'Failed'; Detail = $_.Exception.Message }
}
}
}
}
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-ComputerName | string[] | — | Computers to change. Takes pipeline input, including objects with a Name or DNSHostName property, so Get-ADComputer output pipes straight in. |
-Member | string[] | — | Domain groups or users in DOMAIN\Name form, like CONTOSO\Tier2-Workstation-Admins. |
-Remove | switch | — | Remove the members instead of adding them. |
-Credential | pscredential | — | Alternate credentials for the remoting connection. |
-WhatIf | switch | — | List every change without connecting to anything. |
Run it
Preview adding the workstation admin group to a list of PCs.
Get-Content .\computers.txt | .\Set-LocalAdminMember.ps1 -Member 'CONTOSO\Tier2-Workstation-Admins' -WhatIfEvery computer in an OU, straight from AD.
Get-ADComputer -Filter * -SearchBase 'OU=Laptops,OU=Tier 2,DC=contoso,DC=com' | .\Set-LocalAdminMember.ps1 -Member 'CONTOSO\Tier2-Workstation-Admins'Take an old group off two machines.
.\Set-LocalAdminMember.ps1 -ComputerName PC-0142, PC-0187 -Member 'CONTOSO\Old-Helpdesk' -RemoveKeep a record, and see just the failures.
Get-Content .\computers.txt | .\Set-LocalAdminMember.ps1 -Member 'CONTOSO\Tier2-Workstation-Admins' | Tee-Object -Variable results | Where-Object Result -eq 'Failed'What you'll see
ComputerName Action Member Result Detail
------------ ------ ------ ------ ------
PC-0142 Add CONTOSO\Tier2-Workstation-Admins Done
PC-0187 Add CONTOSO\Tier2-Workstation-Admins Skipped Already a member
PC-0311 Add CONTOSO\Tier2-Workstation-Admins Failed WinRM cannot complete the operation. Verify that the specified computer name is valid...
PC-0405 Add CONTOSO\Tier2-Workstation-Admins Done
How it works
- Decide what's allowed. For each computer, every member goes through
ShouldProcesslocally. Under-WhatIfyou get a WhatIf row per member and no connection is ever made. - Run the change on the target. The approved members are sent to the computer with
Invoke-Command, whereAdd-LocalGroupMemberorRemove-LocalGroupMemberruns against the group with SIDS-1-5-32-544. That's the built-in Administrators group on every Windows machine, whatever language it's in. - Treat "already done" as fine. Adding someone who's already a member, or removing someone who isn't, comes back as Skipped instead of an error, so re-running the script is harmless.
- Keep going on failure. If a computer can't be reached, every member for that machine is reported as Failed with the remoting error, and the script moves on to the next one.
Each result is an object with the computer, action, member, result and detail, so Export-Csv gives you a record of exactly which machines were changed.
Take it further
- Go parallel for big lists.
Invoke-Commandcan take many computers at once with-ThrottleLimit. It's much faster, at the cost of messier per-machine error handling. - Report before you change. Run
Get-LocalGroupMember -SID S-1-5-32-544throughInvoke-Commandto see who's in Administrators today. You'll probably find a few surprises worth removing with-Remove. - Sort out the built-in Administrator account too. Windows LAPS gives every machine a unique, rotating local admin password, which pairs well with group-based admin rights.
Things that'll trip you up
- This is a one-time change, not enforcement. Anyone with admin rights on the machine can take the group back out, and new machines won't get it. For the long term, use Intune's Account protection "Local user group membership" policy or Group Policy (Restricted Groups, or Local Users and Groups preferences). Use this script for the one-off fixes and the machines policy hasn't reached.
- Mind your tiers. Don't add Domain Admins or any other Tier 0 group to workstation local admins. If one of those machines is compromised, so is every privileged credential that ever signed in there. Workstations get a Tier 2 group, full stop.
- Remoting has to work first. The targets need WinRM enabled and reachable through the firewall, and you need to reach them by a name Kerberos is happy with. If every machine fails with the same WinRM error, test one with Test-WSMan before blaming the script.
- Entra-joined devices are a different story. This is for domain-joined machines and domain groups. For Entra-joined devices, manage local admins through Intune, where Entra ID groups can be added directly.