Skip to content

About

Give Claude Code an unattended SYSTEM/admin channel on Windows (the elevation it lacks natively) - a queue drained by a SYSTEM scheduled task or service.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

admin-hook-runner — a SYSTEM elevation channel for Claude Code (Windows)

Claude Code (and agents like it) run as your normal user on Windows. They have no built-in way to run elevated — no sudo, and they can't click a UAC prompt. So anything needing admin (registry under HKLM, protected folders, scheduled tasks, killing protected processes) is out of reach. See the open request: anthropics/claude-code#29275 — "built-in sudo watcher for privileged operations".

This is a tiny, do-it-yourself fix: a command queue drained by a SYSTEM task/service.

  • You install it once, by hand, as Administrator (that's the only human step).
  • After that the agent drops a PowerShell one-shot into a queue file; a process running as NT AUTHORITY\SYSTEM picks it up, runs it, and returns the output. No UAC ever again.

⚠️ Security — read this

This grants an automated agent unattended SYSTEM (full admin) access to the machine, with no further prompts. That is the entire point — and a real tradeoff. Install it only on a machine you control and where you intend the agent to have that power. Uninstall is one menu choice away (below).

Install

Two ways to run it — pick either:

  • Double-click / "Run with PowerShell" → you get a menu.
  • Command line: powershell -ExecutionPolicy Bypass -File bootstrap.ps1 -Mode task|service|uninstall

If it isn't elevated it offers to relaunch itself as Administrator (accept the UAC prompt). If a permission error slips through, it tells you plainly: right-click PowerShell → Run as administrator.

The menu:

 1) Install as a WINDOWS SERVICE  (RECOMMENDED - ~3s loop, SYSTEM, auto-start; needs NSSM)
                                  (also installs the AdminHookPlunger :5052 watchdog)
 2) Install as a SCHEDULED TASK   (fallback only - 60s, and AdminHookPlunger cannot restart it)
 3) UNINSTALL                     (removes the task and/or service)
 Q) Quit

Service vs task: install it as a SERVICE, always. It reacts in ~3s instead of ~60s, auto-starts as SYSTEM, and above all it is what AdminHookPlunger expects: the plunger (a separate SYSTEM service on 127.0.0.1:5052 that unclogs a wedged runner) restarts it with Restart-Service AdminHookRunner, so a scheduled-task install leaves the plunger nothing to restart. The service runs powershell.exe -File admin_hook_runner.ps1 -Loop -Interval 3 under NSSM (winget install NSSM.NSSM). The scheduled task is kept only as a no-dependency fallback.

The plunger is bundled. In service mode the installer also lays down and starts AdminHookPlunger from admin-hook-plunger/ in this repo — a tiny, separate SYSTEM service (loopback :5052) whose only job is to unclog the runner if a queued command ever wedges it. You don't install it separately, and uninstall removes it too. See admin-hook-plunger/README.md for why a watchdog that can't run caller input is the whole point.

Uninstall

Run bootstrap.ps1 → 3, or -Mode uninstall. It removes the scheduled task and/or the service and the AdminHookPlunger service. Files in C:\services\admin-hook-runner are left in place; delete the folder to remove them too.

How the agent uses it

run_elevated.py is the client. It writes the queue, waits for the SYSTEM runner to consume it, and hands back per-command {command, output, exit_code}:

py run_elevated.py "Remove-Item 'C:\ProgramData\SomeApp\telemetry.db' -Force"
py run_elevated.py "schtasks /query /fo LIST /v"        # sees SYSTEM-only tasks
py run_elevated.py status                                # is the channel installed / last run

Files

file what
admin_hook_runner.py the runner — drains the _sysfix.ps1 queue as SYSTEM (one-shot, or --loop for service)
run_elevated.py the client — the agent calls this
bootstrap.ps1 the human installer/uninstaller (menu; self-elevates)
_sysfix.ps1 (runtime) the queued one-shot; created by the client, deleted by the runner
logs/heartbeat.txt (runtime) proves the runner is alive + running as SYSTEM
admin-hook-plunger/ the plunger — a separate SYSTEM service (:5052) that unclogs the runner if a queued command wedges it; installed automatically with the service. See its README.

The runner in Python (our default language)

We build in Python. The runner ships as PowerShell here for one reason only: zero dependencies — PowerShell is on every Windows box, so a random Claude Code user can install this without first installing Python. That's the only reason; it's a deliberate call for a tiny, must-run-anywhere component, not a language preference.

If you already have Python, admin_hook_runner.py (in this repo) is the identical runner — same contract, same queue file, same SYSTEM behavior. Register it with the scheduled task pointing at pythonw.exe admin_hook_runner.py instead of the .ps1, or just read it as the reference:

import argparse, os, subprocess, time

BASE = os.path.dirname(os.path.abspath(__file__))
QUEUE = os.path.join(BASE, "_sysfix.ps1")
HEARTBEAT = os.path.join(BASE, "logs", "heartbeat.txt")
_NOWIN = getattr(subprocess, "CREATE_NO_WINDOW", 0)

def run_queued() -> bool:
    if not os.path.exists(QUEUE):
        return False
    try:
        subprocess.run(["powershell.exe", "-NoProfile", "-ExecutionPolicy", "Bypass",
                        "-File", QUEUE], creationflags=_NOWIN, timeout=300)
    finally:
        try: os.remove(QUEUE)
        except OSError: pass
    return True

def heartbeat(ran: bool) -> None:
    os.makedirs(os.path.dirname(HEARTBEAT), exist_ok=True)
    stamp = time.strftime("%Y-%m-%d %H:%M:%S")
    with open(HEARTBEAT, "w", encoding="utf-8") as f:
        f.write(f"{stamp}  ran_as={os.environ.get('USERNAME','?')}  drained_queue={ran}\n")

def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--loop", action="store_true")     # service mode: poll forever
    ap.add_argument("--interval", type=float, default=3.0)
    a = ap.parse_args()
    if a.loop:
        while True:
            heartbeat(run_queued()); time.sleep(max(0.5, a.interval))
    else:
        heartbeat(run_queued())
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

The client (run_elevated.py) is Python already. The queue is just a file, so the client can be any language — Python is simply what we reach for.

How it works (one paragraph)

The runner runs as SYSTEM on a timer. Each tick it checks for _sysfix.ps1 next to itself; if present it executes it with powershell -File (already SYSTEM, so the commands inherit full privilege) and deletes it. The client generates that one-shot to run your commands and append their output to a results log it then reads back. That's the whole mechanism — a file-based queue across a privilege boundary. Deliberately minimal, because everything here runs as SYSTEM.

About

Give Claude Code an unattended SYSTEM/admin channel on Windows (the elevation it lacks natively) - a queue drained by a SYSTEM scheduled task or service.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages