ai newsSo., 1. Nov.
EilmeldungClaude Code's Safety Telemetry Event Lived Six Days. Audit Your Agents With Git

Giving my Grok Bot assistant admin rights without switching off UAC

Case file Incident: Oct 24, 2026 Symptom: dr eggbot, one of my Grok Bot assistants, runs PowerShell on my PC from a normal, non-elevated terminal. Every admin step came back to me as "paste this into an admin window." Evidence: restore points, HKLM registry writes, bcdedit and SYSTEM scheduled task

dev.toSonntag 03:28 · 01.11.12 Min. Lesezeit
Bild: dev.to

Posted on Nov 1 • Originally published at mikepred.github.io

I was rebuilding my Windows 11 setup with dr eggbot. It runs PowerShell on my PC, but its terminal isn't elevated, so every admin-only step turned into a command for me to copy into an admin window. After a few of those I asked the obvious question: can we set up an SSH key so you can run these yourself?

It could, but dr eggbot explained the catch before building anything. You turn on Windows' built-in OpenSSH server, authorize a key, and the assistant runs ssh to localhost. Windows gives an admin account's SSH session a fully elevated token with no UAC prompt. That part is the point. The problem is where the key lives: in my profile, so the assistant's terminal can read it. That means anything running as me, whether a sketchy installer, a bad package or the assistant itself, could pick up that key and get admin without a prompt. It's UAC switched off in all but name.

So I had three options: full SSH admin, a short list of admin jobs, or keep pasting. I asked whether the short-list version could handle something like Enable-ComputerRestore. It could, as long as that job was on the list. What it can't do is run a brand-new admin command on the spot. Anything new means updating the protected copy of the scripts, and that takes one paste from me in an admin window. I decided that paste was a feature. It's my approval step. I went with the short list.

I pasted the installer once, from an admin window. Then dr eggbot started the RestorePoint task from its own non-elevated terminal, and it worked: a fresh restore point, exit 0.

A working menu only proves half of it, though. The other half is whether the normal account can quietly change what the menu does. So dr eggbot tried, from the same account it runs as. Writing a new file into the folder: blocked. Appending a line to runner.ps1: blocked. Re-registering a task with a changed definition: access denied. It can start the tasks. It can't change what they do.

The first real job was applying my tweaks script through the Tweaks task. The registry changes landed, but the task sat at "Running" for more than five minutes with nothing in its log past the header. The script's last step was winget configure --enable, so we blamed winget. A hidden task has no console, and winget probably wanted one. We moved that step into its own process with a time limit, reran it, and it came back clean in two seconds. Case closed, I thought.

It wasn't. When dr eggbot finally read winget's own log, winget hadn't done anything on either attempt. I'd passed --enable together with --disable-interactivity, and winget rejects that combination: "Option '--enable' and '--disable' cannot be used with other arguments." It quit within milliseconds, both times. On the first run, the thing actually hanging was PowerShell, still waiting on the inline | Out-Null pipe after winget had exited. Why that pipe stayed open is still unproven. The "clean" rerun was only clean because Start-Process doesn't pipe output, and the feature I wanted was never turned on.

The real fix was the right flag ( --enable on its own) plus running winget as its own process. On the next run winget's log showed "Leaf command succeeded: root:configure," and the task finished in about six seconds. The time limit is now five minutes, because --enable can download from the Store. It's insurance, not the fix, and it has never fired.

That run turned up one more bug. The tweaks script has a check mode that reports drift, and when it ran without admin it called a SYSTEM-owned task "missing" because it couldn't see it. Now it says "not checked (needs admin)."

Which raised a fair question: hadn't I just given it admin? Sort of. It could apply settings through the menu, but it couldn't read admin-only state back to confirm them. So we added a fifth action, Check. It's read-only and runs the drift check elevated. I reran the installer (one more paste, one more approval), dr eggbot started Check itself, and it came back "All settings match." with exit 0. It verified admin-only settings without getting any broader admin.

This assumes your everyday account is an administrator under UAC, which is the Windows default, and that you're comfortable reading PowerShell. The scripts are trimmed versions of mine. Keep your working copies in a folder such as %USERPROFILE%\WinSetup. The installer copies them into the protected folder; that copy is the only one the tasks ever run.

1. Decide on the menu. Write down the admin jobs you actually want the assistant to run, as verbs. Mine are RestorePoint, Tweaks (my settings script), Apps (install from a list), SaveDumps (the crash-dump copy from my boot loop post ) and Check (a read-only drift report). Anything that takes free-form input, like "run this command" or "install this package name," doesn't belong on the list. That's a shell with extra steps.

2. Write the runner ( runner.ps1 ). It accepts one action from a fixed list and logs everything.

param ([ Parameter ( Mandatory )][ ValidateSet ( 'RestorePoint', 'Tweaks', 'Apps', 'SaveDumps', 'Check' )][ string ] $Action ) # The task starts a fresh -NoProfile process, so nothing upstream sets this. Without it, a failed registry write # is a non-terminating error, the catch below never sees it, and the log still says exit 0. $Error ActionPreference = 'Stop' # Load modules only from system folders. The per-user module folder is writable by anything running as you. $ env: PSModulePath = " $ env: ProgramFiles \WindowsPowerShell\Modules; $ env: SystemRoot \System32\WindowsPowerShell\v1.0\Modules" $root = $PSScriptRoot $logs = Join-Path $root 'logs'; New-Item $logs -ItemType Directory -Force | Out-Null Start-Transcript -Path ( Join-Path $logs ( '{0}-{1:yyyyMMdd-HHmmss}.log' -f $Action, ( Get-Date ))) | Out-Null $code = 0 try { switch ( $Action ) { 'RestorePoint' { Enable-ComputerRestore -Drive 'C:\' New-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SystemRestore' -Name SystemRestorePointCreationFrequency -Value 0 -PropertyType DWord -Force | Out-Null Checkpoint-Computer -Description 'WinSetup: manual restore point' -RestorePointType MODIFY_SETTINGS } 'Tweaks' { & ( Join-Path $root '01-tweaks.ps1' ) } 'Apps' { & ( Join-Path $root '02-apps.ps1' ) } # installs only what's in apps.json, which lives in this folder too 'SaveDumps' { & ( Join-Path $root 'save-dumps.ps1' ) } 'Check' { & ( Join-Path $root '01-tweaks.ps1' ) -Check } # read-only drift report } } catch { $code = 1; "ERROR: $( $_.

Exception. Message ) " } "RESULT: $Action exit $code " Stop-Transcript | Out-Null exit $code Enter fullscreen mode Exit fullscreen mode $ErrorActionPreference = 'Stop' turns every error into one the catch sees, so a failed step can't end in a log that says exit 0. The scripts the runner calls inherit it too.

If one of them is meant to shrug off an error, say so on that line with -ErrorAction Continue.

Every script the runner calls sits next to it in the protected folder. If an action called a script somewhere your normal account can write, that action would be "run whatever is in that file," as admin.

3. Write the installer ( install-admin-tasks.ps1 ). It locks the folder, copies the scripts in and registers one task per action. Rerun it whenever you change a script or the menu.

$Error ActionPreference = 'Stop' $id = [ Security.Principal.WindowsIdentity ]:: GetCurrent () if ( -not ([ Security.Principal.WindowsPrincipal ] $id ). IsInRole ([ Security.Principal.WindowsBuiltInRole ]:: Administrator )) { throw 'Run this from an admin PowerShell.' } $src = $PSScriptRoot; $dst = 'C:\ProgramData\WinSetupAdmin' $user = $id. Name; $sid = $id.

User. Value $actions = 'RestorePoint', 'Tweaks', 'Apps', 'SaveDumps', 'Check' # The check the installer trusts: stop at any link, and if anyone but Administrators or SYSTEM owns, or can change, # anything under $path. The walk is hand-rolled because Get-ChildItem -Recurse in Windows PowerShell follows junctions. $trusted = 'S-1-5-32-544', 'S-1-5-18' $change = [ int ][ Security.AccessControl.FileSystemRights ] 'Write, Delete, DeleteSubdirectoriesAndFiles, ChangePermissions, TakeOwnership' -bor 0 x50000000 # + generic all/write function Assert-Locked ( $path ) { $queue = New-Object Collections.Queue; $queue.

Enqueue (( Get-Item -LiteralPath $path -Force )) while ( $queue. Count ) { $item = $queue. Dequeue () if ( $item.

Attributes -band [ IO.FileAttributes ]:: ReparsePoint ) { throw "Link found: $( $item. FullName ). Delete the folder and rerun." } $itemAcl = Get-Acl -LiteralPath $item.

FullName if ( $trusted -notcontains $itemAcl. GetOwner ([ Security.Principal.SecurityIdentifier ]). Value ) { throw "Not owned by Administrators: $( $item.

FullName ). Delete the folder and rerun." } foreach ( $r in $itemAcl. GetAccessRules ( $true, $true, [ Security.Principal.SecurityIdentifier ])) { if ( $r.

AccessControlType -eq 'Allow' -and $trusted -notcontains $r. IdentityReference. Value -and ([ int ] $r.

FileSystemRights -band $change )) { throw " $( $r. IdentityReference. Value ) can change $( $item.

FullName ). Delete the folder and rerun." } } if ( $item. PSIsContainer ) { Get-ChildItem -LiteralPath $item.

FullName -Force | ForEach-Object { $queue. Enqueue ( $_ ) } } } } # Unregister the old tasks before touching the folder, so a run that stops below never leaves tasks pointing at a folder # that might not be locked (say, one you deleted and someone else recreated). Get-ScheduledTask -TaskPath '\WinSetup\' -ErrorAction SilentlyContinue | Unregister-ScheduledTask -Confirm: $false # A new folder is created already locked, in one step: owner Administrators and one complete permission list # (Administrators + SYSTEM full control, Users read and run, nothing inherited), so nothing can be planted in it first. # An existing folder is never re-permissioned (that would push the new list into whatever is inside).

It's only reused # if it already passes the check; otherwise delete it and rerun. $acl = New-Object Security.AccessControl.DirectorySecurity $acl. SetSecurityDescriptorSddlForm ( 'O:BAD:PAI(A;OICI;FA;;;BA)(A;OICI;FA;;;SY)(A;OICI;0x1200a9;;;BU)' ) if ( Test-Path -LiteralPath $dst ) { Assert-Locked $dst } else { [ void ][ IO.Directory ]:: CreateDirectory ( $dst, $acl ) } # Delete and re-copy each script rather than overwrite it, so the new file inherits the list above instead of keeping old grants. foreach ( $f in 'runner.ps1', '01-tweaks.ps1', '02-apps.ps1', 'apps.json', 'save-dumps.ps1' ) { $target = Join-Path $dst $f if ( Test-Path -LiteralPath $target ) { Remove-Item -LiteralPath $target -Force } Copy-Item -LiteralPath ( Join-Path $src $f ) -Destination $target icacls $target /setowner '*S-1-5-32-544' | Out-Null } Assert-Locked $dst # check the result instead of trusting it; no tasks get registered if this throws $ps = " $ env: SystemRoot \System32\WindowsPowerShell\v1.0\powershell.exe" # full path, not a PATH lookup $svc = New-Object -ComObject 'Schedule.Service'; $svc. Connect () $principal = New-ScheduledTaskPrincipal -UserId $user -LogonType Interactive -RunLevel Highest $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit ( New-TimeSpan -Hours 2 ) -MultipleInstances IgnoreNew foreach ( $a in $actions ) { $act = New-ScheduledTaskAction -Execute $ps -Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `" $dst \runner.ps1 `" -Action $a " Register-ScheduledTask -TaskPath '\WinSetup\' -TaskName $a -Action $act -Principal $principal -Settings $settings -Force | Out-Null # Admins + SYSTEM full control; your account may read and start the task, not edit it.

Without this flag, # Task Scheduler quietly adds its own Allow entry for the account the task runs as, which is you. $TASK_DONT_ADD_PRINCIPAL_ACE = 0 x10 $svc. GetFolder ( '\WinSetup' ). GetTask ( $a ).

SetSecurityDescriptor ( "D:(A;;FA;;;BA)(A;;FA;;;SY)(A;;FRFX;;; $sid )", $TASK_DONT_ADD_PRINCIPAL_ACE ) # Read the stored list back: anyone but Administrators or SYSTEM with more than read and run fails the install. $readRun = 0x1200A9 -bor 0xA0000000 # FRFX, plus generic read/execute $stored = New-Object Security.AccessControl.RawSecurityDescriptor ( $svc. GetFolder ( '\WinSetup' ). GetTask ( $a ).

GetSecurityDescriptor ( 4 )) foreach ( $ace in $stored. DiscretionaryAcl ) { if ( $ace. AceQualifier -eq 'AccessAllowed' -and 'S-1-5-32-544', 'S-1-5-18' -notcontains $ace.

SecurityIdentifier. Value -and ( $ace. AccessMask -band -bnot $readRun )) { Unregister-ScheduledTask -TaskPath '\WinSetup\' -TaskName $a -Confirm: $false throw "Task $a gives $( $ace.

SecurityIdentifier. Value ) more than read and run. Unregistered it." } } } Get-ScheduledTask -TaskPath '\WinSetup\' | Select-Object TaskName, State Enter fullscreen mode Exit fullscreen mode A few notes on what those lines do:

4. Install it from an admin window. Open PowerShell with Run as administrator and run:

powershell -NoProfile -ExecutionPolicy Bypass -File " $ env: USERPROFILE \WinSetup\install-admin-tasks.ps1" Enter fullscreen mode Exit fullscreen mode You should see the five tasks listed as Ready.

5. Start a task from your normal account (a regular, non-elevated PowerShell window), then read its log:

Start-ScheduledTask -TaskPath '\WinSetup\' -TaskName 'Check' Start-Sleep 10 Get-ChildItem C:\ProgramData\WinSetupAdmin\logs | Sort-Object LastWriteTime | Select-Object -Last 1 | Select-String 'RESULT' Enter fullscreen mode Exit fullscreen mode You want RESULT: Check exit 0. Open the whole log if it says anything else.

6. Test the boundary from your normal account. All three should print "blocked." If any prints "ALLOWED," stop and fix the permissions before you hand the menu to anything.

try { Set-Content C:\ProgramData\WinSetupAdmin\probe.txt 'x' -EA Stop; 'WRITE ALLOWED (bad)' } catch { 'write blocked' } try { Add-Content C:\ProgramData\WinSetupAdmin\runner.ps1 '#' -EA Stop; 'EDIT ALLOWED (bad)' } catch { 'edit blocked' } $svc = New-Object -ComObject Schedule.Service; $svc. Connect () try { $d = $svc. GetFolder ( '\WinSetup' ).

GetTask ( 'Check' ). Definition; $d. RegistrationInfo.

Description = 'probe' $svc. GetFolder ( '\WinSetup' ). RegisterTaskDefinition ( 'Check', $d, 4, $null, $null, 3 ) | Out-Null; 'TASK EDIT ALLOWED (bad)' } catch { 'task edit blocked' } Enter fullscreen mode Exit fullscreen mode 7.

Run external tools as their own process, with a time limit, and check the exit code. If one misbehaves, read the tool's own log before you blame it. In my tweaks script that looks like this:

# --enable must be the only option; adding --disable-interactivity makes winget exit with a usage error $wg = Start-Process winget -ArgumentList 'configure', '--enable' -WindowStyle Hidden -PassThru if ( -not $wg. WaitForExit ( 300000 )) { $wg. Kill (); 'winget configure --enable timed out after 5 min (skipped)' } elseif ( $wg. ExitCode -ne 0 ) { "winget configure --enable exited with $( $wg. ExitCode ); check winget's log" } Enter fullscreen mode Exit fullscreen mode winget writes its logs under %LOCALAPPDATA%\Packages\Microsoft.DesktopAppInstaller_8wekyb3d8bbwe\LocalState\DiagOutputDir.

Quelle: dev.to (dev.to). Original öffnen