A fully unattended Windows 10/11 post-install automation framework for Dell hardware. One command triggers it; everything after — Windows Update, system tweaks, app installs, Tailscale setup, and cleanup — runs automatically across reboots until complete. A live WPF monitor window shows progress throughout.
Open PowerShell as Administrator on a fresh Windows install:
irm "https://raw.githubusercontent.com/karolperkowski/win_dell/main/install.ps1" | iexRe-running on an existing install updates scripts and preserves all state and logs. The monitor window appears immediately after running and stays open for the duration of the deployment.
irm "https://raw.githubusercontent.com/karolperkowski/win_dell/main/uninstall.ps1" | iexForce-kills all running tasks and processes, removes scheduled tasks, disables auto-logon, removes the repo and state file.
| Flag | Effect |
|---|---|
-KeepLogs |
Preserve C:\ProgramData\WinDeploy\Logs\ |
-KeepState |
Preserve state.json (resume from current stage) |
-Silent |
Skip confirmation prompt |
irm "https://raw.githubusercontent.com/karolperkowski/win_dell/main/reinstall.ps1" | iexFully unattended: runs uninstall (silent, no prompts), then fresh install. One command, no interaction needed.
irm ... | iex
└─ install.ps1 downloads repo ZIP, verifies SHA-256, prompts for
│ auto-logon account + password (WPF), calls bootstrap
└─ bootstrap.ps1
├─ copies repo to C:\ProgramData\WinDeploy\repo\
├─ writes state.json
├─ configures auto-logon (operator-picked account + password,
│ or legacy 'Administrator' / blank fallback)
├─ registers all scheduled tasks
├─ launches Monitor.ps1 ──→ WPF window visible immediately
└─ triggers WinDeploy-Resume task ──→ Orchestrator runs as SYSTEM
Stage pipeline:
1. TimeSync timezone, w32time service, NTP peers, force
resync with poll; reboot-retry fallback (cap 2)
2. PowerSettings activates Ultimate Performance plan (falls back
to High Performance on Home SKUs), pins display
/sleep/hibernate to Never AND lid/sleep/power
buttons to Do-nothing on both AC and DC
3. DisplaySettings primary display -> max supported resolution,
DPI scale -> 100% (via ChangeDisplaySettingsEx
+ CDS_UPDATEREGISTRY so it survives logon)
4. Debloat removes bloatware per data/bloatware.json
5. WinTweaks WinUtil preset + dark theme + Start menu left
aligned + Chrome + display scale 100% + NumLock
on + Bing off + verbose login + telemetry off
6. InstallDellSupportAssist
7. InstallDellPowerManager
8. InstallDellCommandUpdate via WINGET (Dell.CommandUpdate.Universal); DCU
drives the BIOS/firmware/driver sweep
9. ConfigureDellUpdates SupportAssist auto-consent + weekly SYSTEM-
context dcu-cli sweep (BIOS / firmware / drivers
/ Dell software) so updates keep flowing after
the deploy is done. Skips on non-Dell hardware.
10. InstallRustDesk via WINGET_MANIFEST (pinned vendor URL + SHA256)
11. InstallTailscale `tailscale up --qr` runs in a visible console
in the operator's interactive session; a
pulsing red/amber screen-edge border flashes
green once registration succeeds. Falls back
to the legacy stdout-capture + PNG path when
no interactive user is signed in.
12. RemoteAccess RDP + WinRM (TrustedHosts=100.*) + OpenSSH
13. WindowsUpdate installs all updates, reboots as needed
14. Cleanup removes tasks, disables auto-logon, final reboot
After each reboot: WinDeploy-Resume task fires → Orchestrator resumes
After each logon: WinDeploy-Monitor task fires → progress window reappears
When you want to exercise one stage in isolation — re-running InstallTailscale after a failed QR scan, validating a tweak in WinTweaks after editing the preset, smoke-testing a refactor — use tools/Run-Stage.ps1 from an elevated PowerShell:
cd C:\ProgramData\WinDeploy\repo
powershell.exe -ExecutionPolicy Bypass -File .\tools\Run-Stage.ps1 -Stage <StageName>Or from a local clone during development:
cd C:\path\to\win_dell
powershell.exe -ExecutionPolicy Bypass -File .\tools\Run-Stage.ps1 -Stage InstallTailscaleThe helper loads config/settings.json the same way the orchestrator does, imports the shared modules, dot-sources the stage script with -StageName and -Config bound, and prints the stage's @{ Status; Message } return verbatim. Exit code mirrors success / failure so it's CI-friendly.
Exactly one of (matches the production pipeline order):
| Stage | Script | Notes |
|---|---|---|
TimeSync |
core/TimeSync.ps1 |
Reboot-retry capped at 2; in REBOOT_ALLOWED_STAGES. |
PowerSettings |
core/PowerSettings.ps1 |
Ultimate Performance + AC/DC lockdown. |
DisplaySettings |
core/DisplaySettings.ps1 |
Max resolution + 100% scale via ChangeDisplaySettingsEx. |
Debloat |
core/Debloat.ps1 |
Reads data/bloatware.json. |
WinTweaks |
core/WinTweaks.ps1 |
WinUtil preset Pass 1 + direct registry Pass 2. |
InstallDellSupportAssist |
core/AppInstall.ps1 |
Skips cleanly if SupportAssist already present. |
InstallDellPowerManager |
core/AppInstall.ps1 |
Detection includes Dell Optimizer. |
InstallDellCommandUpdate |
core/AppInstall.ps1 |
WINGET Dell.CommandUpdate.Universal. |
ConfigureDellUpdates |
core/ConfigureDellUpdates.ps1 |
Registers the weekly DCU sweep task. |
InstallRustDesk |
core/AppInstall.ps1 |
WINGET_MANIFEST (pinned vendor URL + SHA256). |
InstallTailscale |
core/Tailscale.ps1 |
QR popup in interactive user session; uses Tailscale.AuthKey if set in settings.json. |
RemoteAccess |
core/RemoteAccess.ps1 |
RDP + WinRM + OpenSSH. |
WindowsUpdate |
core/WindowsUpdate.ps1 |
Long-running; multiple reboots. Prefer letting the orchestrator drive this. |
Cleanup |
core/Cleanup.ps1 |
Unregisters tasks + disables auto-logon — running this manually mid-deploy will break resume. |
Unknown names throw with the full list. Run from the repo root or anywhere — the helper resolves paths relative to itself.
Run-Stage.ps1 is for testing one stage; it deliberately does NOT touch the orchestrator's bookkeeping:
- It does not update
state.json(CurrentStage,CompletedStages,RebootCount). - It does not trigger reboots even if the stage returns
RebootRequired— you get the hashtable back and decide what to do. - It does not halt-on-error or run the consecutive-failure abort logic.
- It does not fire the webhook / tray notifications, write the completion report, or run forensics collection.
- It does not re-register scheduled tasks via
Resilience.psm1(the orchestrator entry point does that).
For a real deploy, use install.ps1. For "the deploy died, fix it and resume," prefer tools/Troubleshoot.ps1 -Action Repair -Stage <name> (which knows the state-file invariants and re-arms the resume task). Run-Stage.ps1 is the lower-level primitive both of those build on.
win_dell/
├── install.ps1 irm|iex entry point
├── uninstall.ps1 removes all WinDeploy components
├── reinstall.ps1 unattended uninstall + fresh install in one step
├── bootstrap.ps1 called by install; sets up tasks and state
├── lint.ps1 local lint runner (PSScriptAnalyzer + custom checks)
├── manifest.json auto-generated by CI — do not edit
├── manifest.sig GPG detached signature of manifest.json
├── RULES.md contributor "do / don't" — non-negotiables
├── CLAUDE.md AI-assisted development context and conventions
├── README.md this file
│
├── core/
│ ├── Config.psm1 shared constants via Get-WDConfig — single source of truth
│ ├── State.psm1 state.json read/write, reboot counter, stage tracking, StageExtras
│ ├── Logging.psm1 structured logging (console + file, per-stage logs)
│ ├── Resilience.psm1 self-healing: re-registers tasks, watchdog, auto-snapshot on stall
│ ├── Winget.psm1 winget helpers: --source winget, exit-code translator, pre-flight
│ ├── Orchestrator.ps1 master controller — runs stages, auto-snapshots on failure
│ ├── Monitor.ps1 WPF progress window + Tailscale QR + auto-snapshot pointer
│ ├── Notify.ps1 tray notification on completion (self-removing)
│ ├── Notify-Webhook.ps1 webhook notifications (Slack/Teams/etc.) for deploy events
│ ├── TimeSync.ps1 stage 1 (timezone + w32time + NTP, reboot-retry capped)
│ ├── PowerSettings.ps1 stage 2 (Ultimate Performance + AC/DC sleep/button lockdown)
│ ├── DisplaySettings.ps1 stage 3 (primary display -> max resolution + 100% scale)
│ ├── Debloat.ps1 stage 4
│ ├── WinTweaks.ps1 stage 5 (incl. Start menu left alignment)
│ ├── AppInstall.ps1 stages 6, 7, 8, 10 (Dell SupportAssist, Power Manager,
│ │ Dell Command|Update, RustDesk)
│ ├── ConfigureDellUpdates.ps1 stage 9 (SupportAssist auto-consent + weekly DCU task)
│ ├── Tailscale.ps1 stage 11
│ ├── RemoteAccess.ps1 stage 12 (RDP + WinRM + OpenSSH)
│ ├── WindowsUpdate.ps1 stage 13 (drains via WUA COM + Dell Command|Update sweep)
│ ├── DellCommandUpdate.ps1 helper engine for the DCU sweep (used by WindowsUpdate
│ │ + the weekly task + tools/Run-DellCommandUpdate.ps1)
│ └── Cleanup.ps1 stage 14
│
├── config/
│ ├── settings.json all deployment configuration — edit this
│ └── winutil-preset.json WinUtil tweak IDs
│
├── data/
│ ├── bloatware.json safe/optional app removal lists
│ └── profiles.json deployment profile definitions (App selection bundles)
│
├── docs/
│ └── GPG-SETUP.md how to generate and configure the GPG signing key
│
├── tools/
│ ├── Troubleshoot.ps1 Status / Diagnose / Repair entry point (manual + auto-fired)
│ ├── Collect-Forensics.ps1 Per-machine archive collector (D:\ -> C:\ fallback) + manifest.json
│ ├── Update-Index.ps1 regenerates INDEX.md (called by pre-commit hook)
│ ├── Get-WingetManifestFields.ps1 computes WINGET_MANIFEST hash for a vendor URL
│ ├── Invoke-OnInteractiveUser.ps1 pivots a .ps1 to the active console user via transient task
│ ├── Run-DellCommandUpdate.ps1 on-demand DCU sweep (scan + apply + reset to vendor defaults)
│ ├── Run-Stage.ps1 runs a single stage outside the orchestrator (dev / one-off)
│ ├── Show-AttentionBorder.ps1 reusable pulsing screen-edge border (operator-attention primitive)
│ ├── Show-RebootCountdown.ps1 pre-reboot WPF countdown + Skip popup (uses the attention border)
│ ├── Show-TailscaleQrInConsole.ps1 visible-console QR helper for InstallTailscale
│ ├── Test-AttentionBorder.ps1 standalone preview harness for the attention border + success flash
│ ├── Test-DcuFlow.ps1 read-only DCU scan diagnostic ("what would DCU do right now")
│ ├── Test-WinUtilDirectApply.ps1 dry-run validator for config/winutil-preset.json
│ └── Test-WinUtilApplyOne.ps1 single-preset apply/verify/revert smoke test
│
├── tests/
│ └── State.Tests.ps1 round-trip tests for core/State.psm1 (run in CI lint job)
│
└── .github/
├── dependabot.yml weekly bumps for github-actions versions
└── workflows/
└── ci.yml lint → validate task XML → sign manifest (in order)
Deploy-time operational tasks (managed by Resilience.psm1::Assert-ScheduledTasks):
| Task | Runs as | Window | Removed by |
|---|---|---|---|
WinDeploy-Resume |
SYSTEM | Hidden (launcher → task_resume.log) |
Cleanup.ps1 |
WinDeploy-Monitor |
Users | Visible (direct) | Cleanup.ps1 |
WinDeploy-Notify |
Users | Hidden (launcher → task_notify.log) |
Notify.ps1 (self) |
WinDeploy-AutoLogonSafety |
SYSTEM | Hidden | Self-deletes after 6h |
WinDeploy-Watchdog |
SYSTEM | Hidden | Cleanup.ps1 (was "never" until 2026-05-26) |
Resume and Notify run via generated launcher scripts that redirect all output to log files. Monitor runs directly — child processes cannot show WPF windows on the interactive desktop.
Self-healing: Missing operational tasks are re-registered automatically on every bootstrap and orchestrator run. Re-running irm ... | iex fully restores everything.
Post-deploy product tasks (registered by a stage, NOT by Resilience.psm1 — they survive uninstall on purpose):
| Task | Registered by | Schedule | Purpose |
|---|---|---|---|
WinDeploy DCU Weekly Sweep |
ConfigureDellUpdates stage |
Weekly (default: Sunday 03:00) | Dot-sources core/DellCommandUpdate.ps1 and calls Invoke-DellCommandUpdate so Dell BIOS / firmware / driver / Dell-software updates keep flowing under SYSTEM long after the deploy is finished. StartWhenAvailable covers laptops that were off when the trigger fired. |
All under C:\ProgramData\WinDeploy\Logs\:
| File | Contents |
|---|---|
early.log |
Pre-logger startup from all scripts |
bootstrap.log |
Bootstrap output |
session.log |
All stages, all runs |
task_resume.log |
Every Orchestrator run |
task_monitor.log |
Every Monitor run + startup entry |
task_notify.log |
Every Notify run |
monitor_crash.log |
WPF crash detail |
monitor_crash.txt |
Plain-text crash readable in Notepad |
completion_report.txt |
Final deployment summary |
tailscale_auth_url.txt |
Fallback auth URL if QR generation fails (in deploy root) |
tailscale_qr_terminal.log |
Mirror of what the QR helper window printed (QR + auth URL + tailscale daemon messages) |
Edit config/settings.json:
| Key | Default | Effect |
|---|---|---|
Stages.TimeSync.Timezone |
"Eastern Standard Time" |
Passed to tzutil /s. Any name from tzutil /l is valid. |
Stages.TimeSync.NtpServers |
["time.google.com","time.cloudflare.com","time.windows.com","pool.ntp.org"] |
First-match-wins peer list. Each is configured with 0x9 (SpecialInterval | Client). |
Stages.TimeSync.ClearRealTimeIsUniversal |
true |
Clears HKLM\...\TimeZoneInformation\RealTimeIsUniversal so Windows reads the BIOS clock as local time (Dell ships BIOS=local). Set false only on dual-boot machines that share UTC with Linux. |
Stages.TimeSync.MaxRebootRetries |
2 |
If verification fails (Source still Local CMOS Clock after polling), TimeSync returns RebootRequired up to this many times before returning Failed. |
Stages.TimeSync.NetworkWaitSeconds |
120 |
How long to wait for Resolve-DnsName to succeed against an NTP peer before attempting the first resync. |
Stages.TimeSync.VerifyTimeoutSeconds |
120 |
Total budget for the resync+poll loop (split across ResyncAttempts). |
Stages.TimeSync.ResyncAttempts |
5 |
Number of w32tm /resync /rediscover calls; each is followed by a /query /status poll until Source flips off the local clock. |
Stages.PowerSettings.PowerPlan |
"Ultimate" |
"Ultimate" = duplicate the hidden Ultimate Performance template via powercfg -duplicatescheme and activate it (falls back to High Performance if duplication is blocked on Home SKUs). "High" = activate the always-present High Performance plan directly. |
Stages.PowerSettings.DisableButtons |
true |
Pin lid close / sleep button / power button to "Do nothing" on AC and DC. |
Stages.PowerSettings.DisableSleepAndScreenOff |
true |
Pin display-off / sleep / hibernate timeouts to Never on AC and DC. |
Stages.PowerSettings.DisableHibernateFile |
true |
Run powercfg /hibernate off to reclaim hiberfil.sys disk space. |
Stages.DisplaySettings.Enabled |
true |
Toggle the entire stage. Set false to skip on headless / VM / kiosk hardware where changing display state is risky. |
Stages.DisplaySettings.SetResolutionToMax |
true |
Pin the primary display to its maximum supported resolution via ChangeDisplaySettingsEx + CDS_UPDATEREGISTRY so the next interactive logon inherits it. |
Stages.DisplaySettings.Scale |
100 |
DPI scale percent for the active user + default-user hive (LogPixels). 100 = 96 DPI native. |
Stages.WinTweaks.RunWinUtil |
true |
Run WinUtil Pass 1 (preset apply) before the direct registry tweaks in Pass 2. Set false to run only Pass 2. |
Stages.WinTweaks.StartMenuAlign |
"Left" |
Windows 11 taskbar/Start alignment. "Left" or "Center". Win10 ignores it. Takes effect on next Explorer restart (Cleanup reboot covers it). |
Stages.ConfigureDellUpdates.ConfigureSupportAssist |
true |
Apply best-effort SupportAssist registry tweaks (AutoUpdate, scheduled scan frequency, telemetry/analytics consent). |
Stages.ConfigureDellUpdates.RegisterWeeklyDcuTask |
true |
Register WinDeploy DCU Weekly Sweep -- a SYSTEM-context scheduled task that calls Invoke-DellCommandUpdate once a week. |
Stages.ConfigureDellUpdates.WeeklyDayOfWeek |
"Sunday" |
Day the weekly DCU task runs. Any value New-ScheduledTaskTrigger -DaysOfWeek accepts. |
Stages.ConfigureDellUpdates.WeeklyTime |
"03:00" |
Time of day for the weekly DCU task. 24-hour HH:mm. |
Stages.ConfigureDellUpdates.DcuResetBeforeConfigure |
true |
Every DCU sweep starts with /configure -restoreDefaults so any prior partial / hand-edited DCU config cannot leak in. Set false for environments that customise DCU via GPO or another tool. |
Stages.ConfigureDellUpdates.DcuScheduleAction |
"DownloadInstallAndNotify" |
DCU schedule action. Vendor-conservative default; flip to "DownloadInstallAndReboot" for fully unattended apply+reboot. Other values: "DownloadAndNotify", "NotifyAvailableUpdates". |
Stages.WindowsUpdate.RunDellCommandUpdate |
true |
After the native WUA-COM drain reports zero pending, run Invoke-DellCommandUpdate so BIOS / firmware / drivers / Dell software updates not in Microsoft Update get applied. |
Stages.WindowsUpdate.InstallDellCommandUpdateIfMissing |
true |
If dcu-cli.exe is absent when the WindowsUpdate stage tries to use it, attempt a winget install of Dell.CommandUpdate.Universal first. Belt-and-suspenders with the new InstallDellCommandUpdate stage. |
Stages.Cleanup.RunForensics |
true |
At the tail of the Cleanup stage, run tools/Collect-Forensics.ps1 to bundle state + logs + hardware facts into <ForensicsRoot>\<hostname>\runs\…. Wrapped in its own try/catch -- collection failure can never promote a Cleanup failure. |
Forensics.Root |
(auto) | Override the per-machine forensics root. Resolution order: this key > D:\WinDeploy-Forensics (when D:\ is writable) > C:\WinDeploy-Forensics. |
Forensics.AutoZip |
false |
Zip each run subfolder when collection finishes. Convenient for emailing; doubles archive size. |
Forensics.RedactKeys |
["AuthKey","Password","Secret","Token","ApiKey","PrivateKey","ConnectionString"] |
Keys whose values are redacted from collected JSON before they land in the forensics archive. Add new secret-bearing key names here when you introduce them. |
Notifications.Webhook |
"" |
Slack / Teams / Discord / generic JSON webhook URL. Empty = disabled. |
Notifications.OnStart / .OnStageFailure / .OnComplete |
true |
Toggle each webhook event independently. |
Tailscale.AuthKey |
"" |
Pre-auth key from login.tailscale.com/admin/settings/keys. When non-empty, registers via --authkey and skips the QR/browser flow. Recommended for unattended deploys. |
Tailscale.QrTimeoutMinutes |
15 |
How long the QR registration wait loop runs before the stage gives up. The QR + URL are visible from the moment the helper window appears, so 15 min is plenty for a real human at the keyboard. |
Tailscale.MaxRebootRetries |
2 |
When QrTimeoutMinutes expires AND no login attempt was detected (BackendState stayed at NeedsLogin the whole window), the stage returns Complete with a deferred-retry marker up to this many times instead of halting the deploy. The orchestrator continues to RemoteAccess / WindowsUpdate / Cleanup; InstallTailscale is in DRAIN_STAGES so it re-runs on every subsequent reboot, showing the QR again on the post-reboot pass. After the cap is exhausted on a still-unregistered machine, the stage returns Failed. The "operator clicked but didn't finish" case (BackendState moved out of NeedsLogin) bypasses this entirely -- straight to Failed. |
Tailscale.QrTerminalWindow |
true |
Spawn a visible PowerShell console in the interactive user's session and let tailscale up --qr render the QR natively. Falls back to the legacy stdout-capture + PNG path when false or when no interactive user is signed in. |
Tailscale.QrTerminalFormat |
"small" |
Forwarded to tailscale up --qr-format. Options: auto, ascii, small (Unicode half-blocks, ~21 rows), large (~37 rows). ascii is the most portable for terminals without half-block glyphs. |
Tailscale.QrTerminalAutoCloseOnRegister |
true |
When true, the helper window closes the moment tailscale up exits with success. When false, it waits for the operator to press Enter so the final state stays readable. |
Tailscale.AttentionBorder |
true |
While the QR is waiting, pulse a red/amber border (~14 px) along the edges of every connected display. Always-on-top, click-through, leaves the center of the screen unobstructed. Flashes green for ~3 s once registration succeeds, then exits. |
Orchestrator.RebootCountdown.Enabled |
true |
Show a WPF countdown popup (with the attention border alongside) in the interactive user's session before every reboot. Set false to fall back to the legacy silent Start-Sleep + Restart-Computer. Suppressed automatically when no interactive user is signed in. |
Orchestrator.RebootCountdown.Seconds |
60 |
Countdown duration before the reboot fires. |
Orchestrator.RebootCountdown.AllowSkip |
true |
When true, the popup shows a Skip Reboot button -- the operator can defer this reboot; the stage is already marked Complete so the deploy continues to the next stage without rebooting. When false, the popup is informational only (the Skip button is hidden). |
Debloat.RemoveOptional |
false |
Remove only the safe bloatware list. Set true to also remove the optional list. |
Cleanup.FinalReboot |
true |
Reboot after the Cleanup stage completes. Set false to leave the machine running. |
Stages.Cleanup.ShowCompletionSummary |
true |
At the tail of Cleanup, spawn tools/Show-CompletionSummary.ps1 in the interactive user's session -- a WPF popup with the per-stage table, WindowsUpdate counts (from C1), reboot count, Tailscale state, and the forensics archive path. Suppressed when no interactive user is signed in; a plaintext <LogDir>\completion-summary.txt is written either way. |
Stages.Cleanup.SummaryAutoCloseSeconds |
60 |
How long the completion summary popup stays up before auto-closing. Set 0 to leave the popup open until the operator clicks OK. |
Monitor.TileDashboard |
true |
After bootstrap.ps1 starts the resume task, spawn tools/Show-DeployDashboard.ps1 -- a Windows Terminal window with three live panes: state snapshot (current stage / completed count / WindowsUpdate progress / Tailscale state, refreshed every 2 s), task_resume.log tail (color-tagged by log level), and current-stage log tail (auto-switches as CurrentStage advances). Falls back to three separate powershell.exe windows when wt.exe is absent. Suppressed when no interactive user is signed in; the deploy is never blocked by a dashboard failure. |
Before handing off to bootstrap.ps1, install.ps1 spawns a WPF popup (tools/Pick-AutoLogonAccount.ps1) that:
- Enumerates enabled local accounts via
Get-LocalUser(falls back toWin32_UserAccountwhen the LocalAccounts module is unavailable). Built-inGuest,DefaultAccount, andWDAGUtilityAccountare filtered out. - Auto-selects the currently signed-in interactive user when it's in the list; otherwise the first enabled account.
- Offers a "use a different name" override checkbox for cases like a freshly-provisioned domain account.
- Captures the password via WPF
PasswordBox.SecurePassword-- it lives only as aSecureStringin memory. - Writes the chosen
{ Username; PasswordSecure }to a DPAPI-encrypted clixml file under$env:TEMP. The file is decryptable only by the same user on the same machine. install.ps1forwards the clixml path intobootstrap.ps1via-AutoLogonPasswordClixml.Set-AutoLogonreads the clixml, decodes the SecureString viaSecureStringToBSTRjust long enough to write toHKLM\..\Winlogon\DefaultPassword, zeroes the BSTR infinally, and deletes the clixml.
The plaintext-in-registry write is unchanged -- it's the documented Windows auto-logon mechanism. The existing mitigations stay in place: AutoLogonCount=99, the WinDeploy-AutoLogonSafety task flips AutoAdminLogon=0 after 6 hours, and Cleanup.ps1 flips it earlier on a normal deploy exit.
Skipping the picker (unattended / CI / no operator at the keyboard):
irm "https://raw.githubusercontent.com/karolperkowski/win_dell/main/install.ps1" |
iex -NoAutoLogonPickerinstall.ps1 -NoAutoLogonPicker falls back to the legacy Administrator / blank-password defaults. The picker is also auto-skipped when no enabled local accounts can be enumerated -- the deploy proceeds with the legacy defaults plus a logged warning.
[Skip auto-logon] from inside the picker has the same effect: legacy defaults + warning. Use this when the operator explicitly does not want auto-logon for this deploy.
| Tweak | Detail |
|---|---|
| Dark theme | HKLM + HKCU + Default user hive |
| Start menu alignment | TaskbarAl=0 (Left) per-user + default hive. Configurable via Stages.WinTweaks.StartMenuAlign. |
| Display scale | Set to 100% (96 DPI) for current and default user |
| NumLock on | Applied to current and default user hive |
| Bing search removed | Start menu Bing search disabled |
| Verbose login | Shows user name on login screen |
| Telemetry | AllowTelemetry = 0 (both HKLM paths) |
| Activity history | Disabled |
| Location tracking | Denied |
| File extensions | Always visible in Explorer |
| Hidden files | Shown |
| Xbox Game DVR | Disabled |
| Fast startup | Disabled |
| Widgets (Win 11) | Disabled |
| Clock seconds | Shown in taskbar |
| Chrome | Installed via winget |
| WinUtil preset | Applied from config/winutil-preset.json |
ci.yml runs three jobs in order:
lint ──┐
├──→ sign manifest (push to main only, both must pass)
validate┘
Manifest signing uses GPG. See docs/GPG-SETUP.md. GPG is currently configured and producing real signatures.
| Symptom | Cause | Fix |
|---|---|---|
| Monitor window off-screen | Previous position from disconnected display | Fixed: CenterScreen + bounds clamp on load |
stream was not readable |
Concurrent Add-Content with SYSTEM writer |
Fixed: [System.IO.File]::AppendAllText in all log writers |
$Window cannot be retrieved |
XAML parse failed before assignment | Check monitor_crash.txt for the XAML error |
| Tasks not registering | Resilience ran before repo was copied | Fixed: Resilience runs after Copy-RepoToDeployRoot |
parameter set cannot be resolved |
-LogonType with -GroupId |
Remove -LogonType from group-based principals |
| Monitor showing error panel | State property case mismatch | Fixed: all state reads use PascalCase. Lint rule enforces this. |
| Auto-logon not cleared | Cleanup stage skipped | AutoLogonSafety task clears it unconditionally after 6h |
WindowsUpdate stage returns Failed with MaxCyclesHitWithPending |
Cap reached and updates still pending after MAX_UPDATE_CYCLES (default 8) |
Investigate the listed pending titles in the WindowsUpdate log; previously masked as silent Complete. See StageExtras['WindowsUpdate_MaxCyclesHitWithPending'] for the count. |
| PSGallery unreachable | No internet during WindowsUpdate stage | Fixed: pre-flight connectivity check with clear error message |
| QR code missing in Monitor | All QR generation methods failed | Fixed: auth URL saved to tailscale_auth_url.txt as fallback |
| Wide-character log files | PS 5.1 *>> redirect writes UTF-16LE |
Fixed: launchers use Out-File -Encoding UTF8 |
| Orchestrator crash on first run | Bootstrap ran inline as user, not SYSTEM | Fixed: bootstrap triggers WinDeploy-Resume task |
All winget installs fail with -1978335138 |
msstore source's TLS cert pinning fails under SYSTEM | Fixed: every winget call passes --source winget; WINGET_MANIFEST bypasses sources entirely |
Stage returned invalid result (type: Object[]) |
Native & winget 2>&1 polluted the script return pipeline |
Fixed: Invoke-WingetCli in Winget.psm1 captures output into a variable |
| Tailscale stuck "Starting tailscale up - waiting for auth URL..." | Daemon already authed (silent succeed) or out-of-band sign-in | Fixed: dual-condition wait (URL OR Test-TailscaleRegistered OR timeout); pre-spawn check skips QR when already registered |
| Test machine running months-old code | bootstrap.ps1 re-uses extracted repo; only install.ps1 pulls from GitHub |
Fixed: install.ps1 writes VERSION stamp; bootstrap WARNS when VERSION is >7 days old; orchestrator logs VERSION on every startup |
Stale LastError for an already-retried stage |
Set-StageComplete didn't clear it |
Fixed: Set-StageComplete clears LastError when it matched the now-successful stage |
| Sticky CI "INDEX.md out of date" failures | INDEX.md describes its own line count, but writing changes it | Fixed: pre-commit regenerates in a loop until git diff is empty (converges in 1-2 iterations) |
| Machine wall clock wrong by the timezone offset on first boot | Old WinTweaks set RealTimeIsUniversal=1, making Windows interpret the BIOS clock (local time on Dells) as UTC |
Fixed: registry value removed in WinTweaks; TimeSync clears any leftover value on every run |
TimeSync returns Failed with Source='Local CMOS Clock' |
w32tm /resync returns when the request is dispatched, not after the sync completes; verification ran too early |
Fixed: TimeSync now polls /query /status for up to VerifyTimeoutSeconds, retries ResyncAttempts times, and falls back to RebootRequired (capped at MaxRebootRetries) for a fresh network stack |
TimeSync succeeds but the clock never moves on a machine with a dead CMOS battery |
Default MaxPosPhaseCorrection / MaxNegPhaseCorrection is 15h, so a multi-year skew is silently refused |
Fixed: TimeSync sets both to 0xFFFFFFFF (no limit) and restarts w32time before the first resync |
Scheduled task launches powershell.exe but no console window appears on the user's desktop |
Task Scheduler gives the process an interactive token but does NOT allocate a console -- the script runs and file I/O works, nothing renders | Fixed: Start-QrTerminalSession registers the task as cmd.exe /c start "<title>" /WAIT powershell.exe ... so start allocates a fresh console in the operator's session, and /WAIT keeps the task in the Running state until the operator closes the window |
Tailscale QR renders as mojibake (Γûê etc.) instead of █ blocks |
tailscale.exe writes the QR as UTF-8; PowerShell decodes native exe output using the console's OEM code page (CP437 on US-English / CP850 on Western-European Windows) |
Fixed: Show-TailscaleQrInConsole.ps1 runs chcp 65001 + sets [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 before invoking tailscale |
| Stage's stdout-capture path prints "NativeCommandError" / call-site banner above the QR | PS 5.1 2>&1 on a native exe wraps every stderr line as an ErrorRecord and prints the script path + source line above the actual output |
Fixed: dropped 2>&1 -- stderr passes through to the console naturally, only stdout is teed to the log |
WPF helper script throws "Cannot convert ... to System.Double" on New-Object |
PS variables are case-insensitive: a typed function param like [double]$W silently shadows a local $w = New-Object Window, so the assignment fails the type constraint and $w ends up as a double |
Fixed: rename single-letter typed params ($W -> $WidthPx, $H -> $HeightPx) whenever the function uses object locals |
Every deploy automatically bundles its own forensic record at the tail of the Cleanup stage by invoking tools/Collect-Forensics.ps1. The same script can be run manually any time:
powershell -ExecutionPolicy Bypass -File C:\ProgramData\WinDeploy\repo\tools\Collect-Forensics.ps1 -Reason 'post-incident-check'Output layout (per machine, on D:\ when writable, else C:):
D:\WinDeploy-Forensics\
<hostname>\
manifest.json <-- grows with every run
runs\
20260519-134500-cleanup\ <-- one folder per run
state.json
tailscale.json (redacted: AuthKey etc.)
tailscale-live.json
VERSION
settings.json (redacted copy)
hardware.json (manufacturer/model/serial/BIOS/OS)
tools-versions.txt (winget/tailscale/dcu-cli)
powercfg-active.txt + powercfg-query.txt
scheduled-tasks.txt (WinDeploy-* + DCU Weekly Sweep)
system-errors.txt (50 most recent ERROR/CRITICAL events)
auto-snapshot-*.txt (from Troubleshoot.ps1 -Action Status)
run-summary.json (machine-readable single-run summary)
logs\ (full copy of $WD.LogDir)
Root-directory resolution: D:\ if it exists and a sentinel file can be created (catches read-only CD-ROMs / locked USB drives); otherwise falls back to C:\WinDeploy-Forensics\. Override either via -ForensicsRoot on the CLI or Forensics.Root in config/settings.json.
Self-copy: on every run the tool also copies itself to <root>\bin\Collect-Forensics.ps1 so the tool stays colocated with its output (survives a C:\ reimage). The repo copy at C:\ProgramData\WinDeploy\repo\tools\ remains the canonical entry point -- the <root>\bin\ copy is a derivative artifact, refreshed on every invocation.
Redaction: any JSON property whose name matches the Forensics.RedactKeys list (default: AuthKey, Password, Secret, Token, ApiKey, PrivateKey, ConnectionString) has its string value replaced with "<redacted len=N>" before the file is copied to the archive. Non-string values are left untouched.
manifest.json is a single per-machine index file listing every run with its timestamp, reason, trigger, outcome (success / partial / failure / in-progress), version SHA, elapsed minutes, failed stages, and the path to its full run folder. Concurrent writers (manual run + Cleanup tail) are serialized via a manifest.lock sentinel file.
Disable the Cleanup-stage auto-run by setting Stages.Cleanup.RunForensics = false in config/settings.json. The manual CLI invocation always works regardless.
The unified CLI dispatcher under tools\windeploy.ps1 is the single entry point for all operator-facing scripts. Run with no arguments (or list) to see every subcommand and what it wraps:
# Read-only snapshot of state, logs, tailscale daemon, deployed VERSION.
# Writes to C:\ProgramData\WinDeploy\Logs\auto-snapshot-<timestamp>.txt
.\tools\windeploy.ps1 status
# Stage-specific read-only deep-dive.
.\tools\windeploy.ps1 diagnose InstallTailscale
# Stage-specific destructive recovery (kills hung processes, hot-patches from main, restarts task).
.\tools\windeploy.ps1 repair InstallTailscale
# Other subcommands: forensics, dashboard, dell-update, check-ci, index, run-stage, ...
.\tools\windeploy.ps1 listThe dispatcher is a thin wrapper -- each subcommand forwards verbatim to the canonical script under tools/. The underlying scripts (Troubleshoot.ps1, Collect-Forensics.ps1, Check-CiStatus.ps1, etc.) are still callable directly if you prefer.
Status runs automatically on:
- any FATAL throw in the orchestrator,
- a stage failure that halts the deploy,
- the consecutive-failure abort path,
- the watchdog detecting a stage idle for 20+ min (excluding WindowsUpdate / Cleanup),
- the watchdog about to kill an orchestrator that has run for >4 hours.
The Monitor window surfaces a "AUTO-SNAPSHOT" pointer when a recent (<24h) snapshot exists, so the operator sees forensic dumps without log spelunking.
install.ps1 writes C:\ProgramData\WinDeploy\repo\VERSION containing the commit SHA, branch, extract timestamp, and ZIP URL fetched from the GitHub API at extract time. The orchestrator logs every VERSION line at startup, and bootstrap.ps1 warns loudly if the deployed VERSION is more than 7 days old. Re-run the install.ps1 one-liner to refresh code on an existing deployment — bootstrap.ps1 alone does not pull from GitHub.