Skip to content

Latest commit

 

History

History
257 lines (191 loc) · 12 KB

File metadata and controls

257 lines (191 loc) · 12 KB

windows-bootstrap — Step-by-Step Checklist

Eight phases. Phases 1-5 are agentic (this conversation runs them). Phase 6 is the user (~60 sec hands-on). Phases 7-8 are agentic again (resume after target boots).

Total wall-clock: ~30 min on a fast connection + USB 3.0 stick. Add ~30 min if USB 2.0 stick (slow DISM split).


Phase 1 — PROBE

Confirm preconditions before doing anything destructive.

  • USB plugged in? Get-Disk | Where-Object BusType -eq 'USB'. Confirm size ≥ 8 GB. Note the Number (typically 1).
  • ISO present? Test-Path "$ProjectRoot\iso\Win11_*.iso". If not, queue Phase 3 (download).
  • C: drive headroom? Need ≥ 10 GB free during ISO download + USB write. Get-PSDrive C. If tight, suggest cleanmgr but don't force it.
  • Internet? Implicit — quick Test-Connection 1.1.1.1. Needed for ISO + first-boot Tailscale install.
  • CDP Chrome on :9333 alive? Invoke-WebRequest http://127.0.0.1:9333/json/version. Needed for headless Tailscale auth-key generation. If down, fall back to interactive paste-in.
  • Hostname not already in tailnet? tailscale status | Select-String <proposed-name>. Collisions auto-suffix to -1, -2 etc., creating noise.

If any check fails, surface the gap to the user before proceeding.


Phase 2 — SCOPE

Collect inputs. Required before rendering any template.

Field Source Example
{{HOSTNAME}} user DevTower
{{USERNAME}} user (often = hostname) DevTower
{{PASSWORD_ENCODED}} computed from plain password — see render note below RABlAHYAVABvAHcAZQByADIAMAAyADYAUABhAHMAcwB3AG8AcgBkAA==
{{LICENSE_KEY}} user (Win 10/11 Pro) XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
{{TIMEZONE}} default Pacific Standard Time
{{TARGET_HW_MODEL}} user (informational, drives boot-key + bypass decision) Dell XPS 15 9550/9560 (P82G)
{{HW_BYPASS}} inferred from CPU gen true for 7th gen Intel
{{USB_DISK_NUMBER}} from Phase 1 1
{{USB_MODEL_HINT}} from Phase 1 (substring of FriendlyName) Kingston
{{ISO_PATH}} absolute path under iso/ ...\iso\Win11_25H2_Pro_x64.iso
{{TAILSCALE_AUTH_KEY}} Phase 4 output tskey-auth-...
{{TAILNET_DOMAIN}} for tailscale ssh smoke test inferred from tailscale status

Boot key by vendor:

Vendor Boot menu BIOS setup
Dell F12 F2
HP F9 (or Esc → F9) F10
Lenovo F12 (ThinkPads: Enter then F12) F1
ASUS F8 (desktop) / Esc (laptop) Del / F2
MSI F11 Del
Surface hold Volume Down + power hold Volume Up + power

Capture this in the build's BUILD-COMPLETE.md so the user knows what to press.


Phase 3 — ISO

Pull a fresh Win 11 25H2 Pro x64 ISO if not present.

  • Pull Fido.ps1 (Microsoft's PowerShell-only ISO downloader, by Rufus's author):
    Invoke-WebRequest -Uri "https://github.com/pbatard/Fido/raw/master/Fido.ps1" -OutFile "$ProjectRoot\scripts\Fido.ps1" -UseBasicParsing
  • Get the signed download URL (it expires in a few hours, so use it immediately):
    $url = & powershell -ExecutionPolicy Bypass -File "$ProjectRoot\scripts\Fido.ps1" -Win 11 -Rel Latest -Ed Pro -Lang English -Arch x64 -GetUrl
  • Download in background (run_in_background — typically 3-15 min depending on connection):
    $ProgressPreference = 'SilentlyContinue'
    Invoke-WebRequest -Uri $url -OutFile "$ProjectRoot\iso\Win11_25H2_Pro_x64.iso" -UseBasicParsing
  • Verify size (Win 11 25H2 is ~7-8 GB; if < 5 GB, download truncated).

Skip this phase if ISO is already on disk.


Phase 4 — AUTH-KEY

Generate (or accept) a Tailscale pre-auth key.

Headless path (preferred):

  • Render templates/generate-tailscale-key.py.tmpl to $ProjectRoot/scripts/generate-tailscale-key.py. Adjust KEYS_URL only if Tailscale UI moves.
  • Run it. The script auto-detects whether Chrome :9333 is logged into Tailscale admin (via Google SSO usually) and drives the "Generate auth key" flow.
  • Output is SUCCESS tskey-auth-... on stdout. Capture into $TailscaleAuthKey for Phase 5.
  • If it prints NEEDS_LOGIN → the user needs to sign into Tailscale admin in their CDP-instrumented Chrome once, then re-run.
  • If toggle ticks fail (Pre-approved), the resulting key may require manual approval in Tailscale admin after first node registration. Acceptable. Note in BUILD-COMPLETE.md.

Interactive fallback:

Leave {{TAILSCALE_AUTH_KEY}} as the literal __TAILSCALE_AUTH_KEY__ placeholder. The rendered post-install.ps1 will detect the placeholder and run tailscale up --ssh interactively, printing the login URL to setup-complete.log. The user reads it post-boot, clicks Approve.


Phase 5 — BUILD-MEDIA

Render templates, wipe USB, write install media. Destructive — gate behind explicit approval.

  • Render templates with placeholders substituted:

    • templates/autounattend.xml.tmpl$ProjectRoot/autounattend.xml
    • templates/post-install.ps1.tmpl$ProjectRoot/scripts/post-install.ps1
    • templates/SetupComplete.cmd.tmpl$ProjectRoot/scripts/SetupComplete.cmd
    • templates/build-usb.ps1.tmpl$ProjectRoot/scripts/build-usb.ps1
    • templates/build-usb-runner.ps1.tmpl$ProjectRoot/scripts/build-usb-runner.ps1
    • Also copy ~/.claude/skills/windows-bootstrap/recover.ps1$ProjectRoot/recover.ps1 (no template needed -- it reads $env:TS_AUTH_KEY at runtime). The build script picks it up and lays both recover.ps1 and a per-build RECOVER.cmd at the USB root as a safety net for any post-install failure.

    Substitute {{HOSTNAME}}, {{USERNAME}}, {{PASSWORD_ENCODED}}, {{LICENSE_KEY}}, {{TIMEZONE}}, {{ISO_PATH}}, {{USB_DISK_NUMBER}}, {{USB_MODEL_HINT}}, {{TAILSCALE_AUTH_KEY}} literally.

    Render note for password placeholders — autounattend has TWO encoded password slots, each with a different Microsoft-mandated suffix:

    $plain = 'DevTower2026'   # what the user types at the lock screen
    
    # For LocalAccount + AutoLogon (suffix: "Password")
    $passwordEncoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($plain + 'Password'))
    
    # For built-in Administrator backdoor (suffix: "AdministratorPassword")
    $adminEncoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($plain + 'AdministratorPassword'))
    
    # Substitute:
    #   {{PASSWORD_ENCODED}}       -> $passwordEncoded   (LocalAccount + AutoLogon)
    #   {{ADMIN_PASSWORD_ENCODED}} -> $adminEncoded      (AdministratorPassword)

    Both <PlainText>false</PlainText>. The Administrator slot is a v4 fallback: if LocalAccount creation fails silently (PITFALLS #15), Administrator is still enabled with a known password. PITFALLS #12 documents why PlainText=true is unreliable.

  • Run build-usb.ps1 in dry-run mode first (no -Execute) — confirms ISO + USB + paths.

  • Spawn build-usb-runner.ps1 elevated via Start-Process -Verb RunAs:

    Start-Process powershell.exe -ArgumentList @(
      '-NoProfile', '-ExecutionPolicy', 'Bypass',
      '-File', "$ProjectRoot\scripts\build-usb-runner.ps1",
      '-TailscaleAuthKey', $key
    ) -Verb RunAs
  • UAC prompt is the ONE click the user must do during the build phase. Tell them explicitly: "UAC incoming, click Yes." Don't try to auto-click — Win 11's secure desktop blocks UI Automation by default (PITFALLS #9).

  • Poll for $ProjectRoot/build-done.flag. Read its JSON when present:

    { "result": "SUCCESS"|"FAILED-RC"|"FAILED-EXC: ...", "log_file": "...", "finished_utc": "..." }
  • On SUCCESS: skip to Phase 6.

  • On FAILED-EXC related to appraiserres.dll: the slow DISM-split has already completed. Don't re-run the whole script. Manually finalize (autounattend + sources\$OEM$\) — see PITFALLS #6.

  • On FAILED-RC: read the log, fix the bug, re-run.

Time estimate Phase 5: ~5 min on USB 3.0, ~25 min on USB 2.0 (DISM split-image dominates).


Phase 6 — HANDOFF

Tell the user to physically move the USB. The only ~60 sec of hands-on.

1. Right-click D: in Explorer → Eject. Wait for safe-to-remove toast.
2. Plug USB into <target-host>.
3. Power on, immediately tap <BOOT_KEY> repeatedly until boot menu appears.
4. Select "UEFI: <USB_MODEL_HINT>" — pick the UEFI variant, not Legacy.
5. Walk away. Setup wipes Disk 0, runs unattended, ~25-30 min.

Provide this verbatim in the chat. Don't try to be clever — the user reads it once and moves.


Phase 7 — JOIN

Wait for the target to announce on tailnet. Resume agentic flow.

  • Poll tailscale status every 60s for ~45 min. Target appears as 100.x.x.x <HOSTNAME> <user>@ windows.
  • If after 45 min still absent: pull the install log via the target's local console (the user must be at the keyboard if Tailscale didn't join). Get-Content C:\Windows\Setup\Scripts\setup-complete.log.
  • If (needs approval) appears next to the new node in tailscale status: that's the Pre-approved toggle didn't get set in Phase 4. Open https://login.tailscale.com/admin/machines and click approve. PITFALLS #11.
  • Smoke-test:
    tailscale ssh <USERNAME>@<HOSTNAME> 'whoami; hostname; (Get-CimInstance Win32_OperatingSystem).Caption'
    Expect: <HOSTNAME>\<USERNAME>, <HOSTNAME>, Microsoft Windows 11 Pro.

Phase 8 — RECORD

Capture the new node in the registry + ssh config + memory.

  • Append a stanza to your fleet credentials registry (e.g. ~/.config/<fleet-name>/credentials.yaml):

    nodes:
      DevTower:
        role: workstation
        bootstrapped: 2026-05-07
        hardware:
          model: Dell XPS 15 9550/9560 (P82G)
          cpu: <fill from tailscale ssh>
          ram: <fill>
          storage: <fill>
          os: Microsoft Windows 11 Pro 25H2
        network:
          tailscale_ip: 100.x.x.x
          tailscale_hostname: DevTower
        auth:
          local_user: DevTower
          local_password_initial: DevTower2026
          local_password_rotated: <yyyy-mm-dd if rotated>
          license_key_source: G2A 2026-05-01
          windows_activation: <ok | failed>
          ssh_via: tailscale ssh DevTower@DevTower
          preferred_access: tailscale ssh
        bootstrap_steps_applied:
          - Win 11 25H2 Pro via build-usb.ps1
          - autounattend.xml: HW bypass, no MS account, local user, autologin once
          - post-install.ps1: Tailscale install, OpenSSH, beacon
  • Add an alias to ~/.ssh/config (under the existing Host blocks):

    Host devtower dt
      HostName DevTower
      User DevTower
      # Tailnet only — no public-IP fallback
      ProxyCommand tailscale ssh -W %h:%p
  • (Optional) Update memory: append a line to MEMORY.md referencing the project + bootstrap date.

  • Smoke-test final aliases:

    ssh dt 'hostname'

    Should land you in PowerShell as DevTower without prompting.


Output

After all 8 phases:

  • Documents/2026/windows-bootstrap/ (or per-build folder) holds rendered scripts + ISO + logs + BUILD-COMPLETE.md.
  • New stanza in credentials registry.
  • New Host block in ssh config.
  • Target node live on tailnet, reachable as tailscale ssh <HOSTNAME>@<HOSTNAME> and ssh <alias>.
  • This skill's PITFALLS.md gets a new entry if anything new went wrong (always re-validate after each bring-up).

What This Skill Doesn't Do (Yet)

  • Driver pre-stage: Win 11 25H2 fetches most drivers via Windows Update on first boot. For older HW (XPS 9560: GTX 960M, Killer wireless), you may need to manually install vendor drivers post-boot. Future iteration could pre-stage drivers in sources\$OEM$\$1\Drivers\ for offline install.
  • Domain join: skill assumes workgroup. Domain-joined fleets need different OOBE flow.
  • BitLocker: explicitly disabled in autounattend (<HideOnlineAccountScreens> + no <BitLocker> block). Turn it on deliberately post-install if needed.
  • Custom branding / start menu: out of scope. autounattend has <RegisteredOwner> only.