Docs
← Back to the builder

First-run command snippets

The catalogue of copy-paste commands you'd otherwise hunt across three forum threads. Each snippet is tagged with its runtime ([PS] / [CMD] / [BAT] / [VBS]), the pass it belongs in (FirstLogonCommands, RunSynchronousCommand in specialize, or SetupComplete.cmd), and a risk note where one exists — Tamper Protection caveats, lab-only warnings, edition restrictions. For how to wire these into the XML, see the command reference. For how the pass cycles interact with sysprep, see sysprep workflows.

Pass placement guide

Unattend has three places to run commands during Windows setup. Choosing the wrong one means your commands run at the wrong time, in the wrong context, or not at all.

LocationWhen it runsContextHas network?Use for
SetupComplete.cmdAfter install completes, before first logonSYSTEMYes (usually)Machine-level config, software installs, registry edits that need no user context
FirstLogonCommandsAt first user logon, as that userLogged-in userYesPer-user config, tasks that need a user profile to exist
RunSynchronousCommand (specialize)During the specialize pass, before OOBESYSTEMDriver-dependent (reliable on VMs, hit-or-miss on bare metal)Lightweight registry edits and name configuration before OOBE; avoid network calls here

The builder exposes all three. For most post-install configuration work, SetupComplete.cmd is the right place: it runs in SYSTEM context with network available, and it is a plain batch script you control completely. FirstLogonCommands is for per-user tasks: shortcuts, profile configuration, user-level registry writes.

Order matters in FirstLogonCommands. The <Order> value determines execution sequence. The builder's snippet library assigns order values automatically when you add snippets, but check them if you manually edit the XML — gaps in the order sequence cause silent skips.
Specialize-pass networking: VMs are reliable, bare metal is not. The TCP/IP stack is technically initialized by the time the specialize pass runs, but interface-up depends on the NIC driver loading first. On virtual machines (vmxnet3, virtio-net, Hyper-V synthetic) the driver is already in WinPE and networking is up by specialize. On bare metal with vendor NICs still being installed mid-pass — Realtek, Intel I225-V, Aquantia — a network call here is a coin flip. Move anything network-bound to SetupComplete.cmd if you target a mix of hardware.

Install Chocolatey

[CMD] + [PS] SetupComplete.cmd SYSTEM context

Downloads and installs the Chocolatey package manager so subsequent commands can install software with choco install. Risk: requires internet at first boot — fails silently if the target has no DNS or proxy access.

Chocolatey is a Windows package manager. Installing it during setup gives you a reliable way to install and update software on deployed machines without staging installers on every machine. Use this when you want to install additional software at first boot without a software distribution platform like SCCM or Intune.

@echo off
:: Install Chocolatey
powershell -NoProfile -ExecutionPolicy Bypass -Command ^
  "[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12; ^
  iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))"

:: Verify -- use 'if errorlevel N' (matches >=N), NOT %errorlevel% in an if-block.
:: Inside a parenthesized block, %errorlevel% expands ONCE at parse time and stays
:: stuck at its pre-block value; you need 'if errorlevel 1' or 'setlocal
:: enabledelayedexpansion' + !errorlevel!.
choco --version > NUL 2>&1
if errorlevel 1 (
    echo Chocolatey install failed >> %TEMP%\setup-errors.log
) else (
    echo Chocolatey installed successfully >> %TEMP%\setup-log.log
)
Requires internet access. This downloads the Chocolatey installer from chocolatey.org. If your deployment targets have no internet access at first boot, stage the installer on a network share or bundle it in the image before capture.

Installing packages immediately after

:: After the Chocolatey install block above:
choco install firefox googlechrome vlc -y --no-progress 2>&1 >> %TEMP%\choco-install.log

Set execution policy to RemoteSigned

[PS] SetupComplete.cmd SYSTEM context

Persists a machine-wide PowerShell execution policy of RemoteSigned so local scripts run unsigned but anything downloaded still needs a signature. No significant risk; it is the standard enterprise baseline.

Windows defaults to Restricted execution policy, which blocks all PowerShell scripts. Setting it to RemoteSigned allows local scripts to run without a signature while still requiring remote scripts (downloaded from the internet) to be signed. This is the standard enterprise baseline — it allows your admin scripts to run without disabling all policy enforcement.

powershell -NoProfile -Command "Set-ExecutionPolicy RemoteSigned -Scope LocalMachine -Force"

If you need to run scripts only in the current session context during setup, use -Scope Process instead of -Scope LocalMachine. The scope affects whether the change persists after setup completes.

AllSigned vs RemoteSigned. For machines in high-security environments, AllSigned requires all scripts (including local ones) to be signed. That is the correct policy for production servers. RemoteSigned is the standard for admin workstations and deployment targets where you control the scripts.

Disable Defender real-time scanning (lab use only)

[PS] SetupComplete.cmd SYSTEM context Lab use only

Turns off Defender real-time monitoring and (optionally) cloud reporting. Risk: leaves the machine unprotected — only use in air-gapped labs or test environments, never on internet-connected production builds.

In lab environments — automated testing, malware analysis VMs, build machines where Defender false-positives interrupt software installation — disabling real-time protection reduces friction. This should never be applied to production machines.

:: Disable Defender real-time monitoring (lab/test environments only)
powershell -NoProfile -Command "Set-MpPreference -DisableRealtimeMonitoring $true"

:: Also disable cloud-based protection and sample submission if needed:
powershell -NoProfile -Command ^
  "Set-MpPreference -MAPSReporting Disabled -SubmitSamplesConsent NeverSend"
Do not use on internet-connected production machines. Real-time protection is a primary defense against malware. Disabling it on machines that browse the web or receive email leaves them exposed. This snippet is appropriate only for air-gapped labs or tightly controlled test environments.

If your goal is to prevent Defender from scanning a specific directory (to avoid interference with a software build output folder, for example), use an exclusion instead:

powershell -NoProfile -Command ^
  "Add-MpPreference -ExclusionPath 'C:\BuildOutput'"

Disable Windows telemetry

[CMD] SetupComplete.cmd SYSTEM context

Sets AllowTelemetry to the lowest level your SKU supports and stops the DiagTrack/dmwappushservice services. Risk: level 0 requires Enterprise or Education edition — silently no-ops on Pro/Home.

Windows sends diagnostic and usage data to Microsoft by default. The telemetry level ranges from 0 (Security, Enterprise-only — minimal data) to 3 (Full). Setting it to 1 (Basic/Required) on Pro/Enterprise is the lowest level available on those editions without Group Policy. Setting it to 0 requires Enterprise or Education edition.

:: Set telemetry to Basic/Required (level 1) — works on Pro and Enterprise
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection" ^
    /v AllowTelemetry /t REG_DWORD /d 1 /f

:: Disable connected user experiences and telemetry service.
:: On Win 11 23H2+ with Tamper Protection ON (default), sc config can be blocked
:: even from SYSTEM context. Verified working on build 22631; status on 26100 is
:: build-dependent. If this no-ops, disable Tamper Protection via Intune/Defender
:: policy first, then re-run.
sc config DiagTrack start= disabled
sc stop DiagTrack

:: Disable WAP push message routing
sc config dmwappushservice start= disabled
sc stop dmwappushservice 2>NUL

For Enterprise and Education editions, setting level 0 (Security only) further reduces data collection:

:: Enterprise/Education only — minimal telemetry
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\DataCollection" ^
    /v AllowTelemetry /t REG_DWORD /d 0 /f

Install OpenSSH Server (for remote management)

[CMD] + [PS] SetupComplete.cmd SYSTEM context

Installs the OpenSSH Server optional feature, starts sshd, and opens port 22 in the firewall. Risk: default password auth — disable it in sshd_config and switch to key-based auth before any production use.

OpenSSH Server ships with Windows 10 1809 and later as an optional feature. Installing it during first setup gives you SSH access to every deployed machine immediately — before WinRM is configured, before RDP is enabled, and without requiring the GUI. Useful for automated deployments where you need to remotely verify or further configure machines after unattend finishes.

:: Install OpenSSH Server capability.
:: The capability name has a version suffix (e.g. OpenSSH.Server~~~~0.0.1.0) that
:: drifts across Windows builds -- 22H2, 23H2, and 24H2 ship different suffixes.
:: Use a wildcard match against the live capability list so the snippet survives
:: build upgrades without code changes.
powershell -NoProfile -Command ^
  "Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH.Server*' | Add-WindowsCapability -Online"

:: Set service to start automatically
sc config sshd start= auto

:: Start the service
net start sshd

:: Add firewall rule if not already present
netsh advfirewall firewall add rule ^
    name="OpenSSH Server (sshd)" ^
    dir=in ^
    action=allow ^
    protocol=TCP ^
    localport=22

By default, SSH authentication uses the local administrator's password. For key-based authentication, add your public key to C:\ProgramData\ssh\administrators_authorized_keys (for admin users) or %USERPROFILE%\.ssh\authorized_keys (for regular users), then update C:\ProgramData\ssh\sshd_config accordingly.

Firewall rule note. The netsh command above adds an inbound rule. If you have a group policy that manages firewall rules, the GP rule will take precedence. Verify the rule is in effect after deployment: netsh advfirewall firewall show rule name="OpenSSH Server (sshd)"

Enable WinRM (Windows Remote Management)

[CMD] + [PS] SetupComplete.cmd SYSTEM context HTTPS warning

Enables PowerShell remoting / Ansible-WinRM by running winrm quickconfig. Risk: quickconfig opens HTTP (5985) in cleartext — for any production fleet, configure HTTPS (5986) with a real cert.

WinRM enables remote PowerShell sessions (Enter-PSSession, Invoke-Command) and is the foundation of PowerShell remoting. Enable it when you manage machines through PowerShell remoting, Ansible with the WinRM transport, or any tool that uses the WS-Management protocol.

:: Enable WinRM with default HTTP transport (port 5985).
:: WARNING: HTTP transport is unencrypted. Use HTTPS for production.
::
:: NOTE: 'winrm quickconfig -force' alone refuses to create listeners when the
:: active connection profile is Public. Win 11 22H2+ defaults the first NIC to
:: Public until the machine is domain-joined or the user opts in. Either:
::   (a) pass -SkipNetworkProfileCheck, OR
::   (b) force the profile to Private first.
:: We do both here for reliability.
powershell -NoProfile -Command ^
  "Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private"
winrm quickconfig -force -SkipNetworkProfileCheck

:: Allow remote access from any host (adjust this to your management subnet)
winrm set winrm/config/client '@{TrustedHosts="*"}'

:: Set service to start automatically
sc config WinRM start= auto
HTTP WinRM sends credentials in cleartext on the wire. The winrm quickconfig command enables HTTP (port 5985) by default. Use this only on isolated management networks or in lab environments. For production, configure HTTPS (port 5986) with a certificate:
:: HTTPS WinRM — requires a certificate (self-signed or from CA)
$cert = New-SelfSignedCertificate -DnsName $env:COMPUTERNAME -CertStoreLocation cert:\LocalMachine\My
$thumbprint = $cert.Thumbprint

winrm create winrm/config/listener?Address=*+Transport=HTTPS "@{Hostname=`"$env:COMPUTERNAME`";CertificateThumbprint=`"$thumbprint`"}"

netsh advfirewall firewall add rule name="WinRM HTTPS" dir=in action=allow protocol=TCP localport=5986

For domain environments, distribute certificates via Group Policy Certificate Enrollment instead of using self-signed certs.

Install Office 365 from a network share

[CMD] SetupComplete.cmd SYSTEM context

Runs the Office Deployment Tool against a configuration.xml on a network share. Risk: SYSTEM context can only read shares that grant access to the machine account (MACHINENAME$) — user-permissioned shares fail silently.

The Office Deployment Tool (ODT) installs Microsoft 365 Apps silently using a configuration XML. Stage the ODT and a configuration.xml on a network share accessible from the deployed machines. This approach does not require a GUI and completes without user interaction.

:: Install Microsoft 365 Apps from a network share
:: Assumes \\fileserver\office\ contains setup.exe and configuration.xml
\\fileserver\office\setup.exe /configure \\fileserver\office\configuration.xml

The configuration.xml controls which apps are installed, the update channel, and the license type. A minimal example for Microsoft 365 Apps for Business on Current Channel:

<Configuration>
  <Add OfficeClientEdition="64" Channel="Current">
    <Product ID="O365BusinessRetail">
      <Language ID="en-us" />
      <ExcludeApp ID="Groove" />
      <ExcludeApp ID="Lync" />
      <ExcludeApp ID="Teams" />
    </Product>
  </Add>
  <Updates Enabled="TRUE" Channel="Current" />
  <Display Level="None" AcceptEULA="TRUE" />
  <Property Name="AUTOACTIVATE" Value="1" />
</Configuration>
Network share access from SYSTEM context. SetupComplete.cmd runs as SYSTEM. SYSTEM can access network shares that allow the computer account (MACHINENAME$) or Everyone. If your share uses user-based permissions, the script will fail silently. Configure share permissions to allow computer accounts, or use a local copy staged during the WinPE phase.

Join domain with djoin.exe blob

[CMD] SetupComplete.cmd SYSTEM context

Performs an offline domain join using a pre-provisioned blob — no DC connectivity needed at setup time. Risk: the blob is tied to a specific computer name; mismatch between unattend-assigned name and provisioned name silently aborts the join.

The standard unattend domain join (Section 5 of the builder) requires domain controller connectivity at the time of the specialize pass. For deployments where machines cannot reach a DC during setup — offline staging, pre-staging for shipping — use djoin.exe to pre-provision a computer account on the DC, export a blob, and join the machine using that blob without real-time DC connectivity.

Step 1: Pre-provision the computer account (on a domain member)

djoin /provision /domain contoso.com /machine TARGETPC01 /savefile C:\djoin-blobs\TARGETPC01.txt /reuse

Step 2: Copy the blob to the target machine

Include the blob file on the install media, or copy it via SetupComplete.cmd from a network share. Place it at a known local path, for example C:\Windows\Panther\djoin.txt.

Step 3: Join using the blob (in SetupComplete.cmd)

:: Join domain using pre-provisioned blob — no DC connectivity required
djoin /requestodj /loadfile C:\Windows\Panther\djoin.txt /windowspath C:\Windows /localos

:: The join takes effect after reboot. Trigger it:
shutdown /r /t 30 /c "Domain join completed. Rebooting."
Computer name must match the blob. The blob is tied to the computer name you specified in djoin /provision. If your unattend sets a different computer name (via pattern or random), pre-provision the accounts after computer names are assigned, or use the Specific computer name mode in the builder to match the pre-provisioned names exactly.

Enable Remote Desktop

[CMD] SetupComplete.cmd SYSTEM context

Sets fDenyTSConnections to 0, enables NLA, opens the firewall group, and adds the local admin to the Remote Desktop Users group. Risk: RDP exposed on port 3389 is a common brute-force target — restrict by IP at the firewall.

The builder has a dedicated Section 9 toggle for enabling Remote Desktop. If you need more granular control — enabling NLA, specifying which users get RDP access, or adjusting the port — use this snippet directly in SetupComplete.cmd.

:: Enable Remote Desktop
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" ^
    /v fDenyTSConnections /t REG_DWORD /d 0 /f

:: Enable NLA (Network Level Authentication) — required for most enterprise environments
reg add "HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp" ^
    /v UserAuthentication /t REG_DWORD /d 1 /f

:: Add firewall rule
netsh advfirewall firewall set rule group="remote desktop" new enable=yes

:: Add the admin account to Remote Desktop Users group
net localgroup "Remote Desktop Users" "admin" /add

Configure BGInfo for desktop identification

[CMD] FirstLogonCommands User context

Runs Sysinternals BGInfo at first logon to paint hostname / IP / OS info onto the desktop wallpaper. No real risk; for it to repeat on every logon, add a Run-key registry value as shown.

BGInfo (from Sysinternals) writes system information — computer name, IP address, OS version, uptime — directly onto the desktop wallpaper. In environments with many VMs or managed endpoints where you need to quickly identify which machine you are looking at, BGInfo is the low-overhead solution.

:: Stage bginfo64.exe and a config file on your share first:
:: bginfo64.exe — from https://learn.microsoft.com/en-us/sysinternals/downloads/bginfo
:: bginfo.bgi — your saved configuration file

:: Run at first logon (put this in FirstLogonCommands):
\\fileserver\tools\bginfo64.exe \\fileserver\tools\bginfo.bgi /silent /nolicprompt /timer:0

:: To run at every logon, add a scheduled task or registry run key:
reg add "HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run" ^
    /v BGInfo ^
    /t REG_SZ ^
    /d "\\fileserver\tools\bginfo64.exe \\fileserver\tools\bginfo.bgi /silent /nolicprompt /timer:0" ^
    /f

The /timer:0 flag makes it run immediately without the countdown dialog. /nolicprompt suppresses the license prompt on first run.

Set timezone via command

[CMD] SetupComplete.cmd SYSTEM context

Overrides the Windows time zone with tzutil /s. No risk; the only gotcha is the exact zone string — use tzutil /l on a reference machine to copy it verbatim.

The builder sets timezone through the unattend locale section, which is usually sufficient. Use this snippet when you need to override timezone after deployment — for example, if you have a single golden image deployed across multiple time zones and need to set the correct zone based on deployment location.

:: Set timezone — use the exact Windows timezone name
tzutil /s "Central European Standard Time"

:: List all valid timezone names:
:: tzutil /l

Common timezone names:

UTC                                -> UTC
Eastern Standard Time              -> US Eastern
Central Standard Time              -> US Central
Pacific Standard Time              -> US Pacific
GMT Standard Time                  -> UK/Ireland
Central European Standard Time     -> CET (Slovakia, Germany, France)
W. Europe Standard Time            -> Netherlands, Belgium
Romance Standard Time              -> France, Spain
E. Europe Standard Time            -> Eastern Europe, Finland

Disable automatic Windows Update restart

[CMD] SetupComplete.cmd SYSTEM context

Writes NoAutoRebootWithLoggedOnUsers and optionally sets active hours so updates do not yank the machine offline. Risk: only suppresses auto-restart, not update installation — do not confuse this with disabling Windows Update.

By default, Windows Update restarts machines automatically after installing updates if no user is logged in. For servers, kiosks, and always-on machines, this causes unplanned downtime. The Group Policy key below tells Windows Update to wait for an admin to schedule the restart.

:: Disable automatic restart after Windows Update (no user logged in)
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" ^
    /v NoAutoRebootWithLoggedOnUsers /t REG_DWORD /d 1 /f

:: Optionally set an active hours window where auto-restart is blocked (9am-5pm):
reg add "HKLM\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" ^
    /v ActiveHoursStart /t REG_DWORD /d 9 /f
reg add "HKLM\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings" ^
    /v ActiveHoursEnd /t REG_DWORD /d 17 /f
Do not combine with disabling Windows Update entirely. Preventing automatic restarts is reasonable. Disabling Windows Update on production machines means they stop receiving security patches. Budget a maintenance window for updates and restarts rather than disabling the mechanism entirely.

Clean up Windows.old and temporary files

[CMD] SetupComplete.cmd SYSTEM context

Removes C:\Windows.old (15-30 GB), clears temp folders, and runs DISM component cleanup. Risk: /resetbase is irreversible — only run on machines you are preparing for capture, never on live workstations that may need to roll back updates.

After an in-place upgrade, a feature update, or a Workflow D image refresh, Windows leaves a Windows.old directory that can consume 15–30 GB. On machines with constrained storage, removing it immediately after setup saves space. This is also one of the Workflow D cleanup defaults in the builder.

:: Remove Windows.old if present
if exist C:\Windows.old (
    takeown /F C:\Windows.old /R /A /D Y > NUL 2>&1
    icacls C:\Windows.old /reset /T /Q > NUL 2>&1
    rmdir /S /Q C:\Windows.old
    echo Windows.old removed >> %TEMP%\setup-log.log
)

:: Clean temp files
del /F /Q %TEMP%\*.* 2>NUL
del /F /Q C:\Windows\Temp\*.* 2>NUL

:: Run Disk Cleanup silently (SYSTEM context, all categories)
cleanmgr /sagerun:1 /autoclean

For a more thorough disk cleanup, DISM's component store cleanup is more reliable than cleanmgr in non-interactive contexts:

:: Clean up superseded Windows components (takes a few minutes)
dism /online /cleanup-image /startcomponentcleanup /resetbase
/resetbase is not reversible. Once the component store is reset, you cannot uninstall updates that have been consolidated. Run this only on machines you are preparing for capture or final deployment — not on machines where you may need to roll back updates.
/resetbase runtime on 24H2 can exceed Setup's pass timeout. On Win 11 24H2 (build 26100+) with a large servicing history, /resetbase routinely takes 30–60 minutes on SATA SSDs and longer on spinning disks. If you run it from SetupComplete.cmd on a freshly deployed image and the pass exceeds the 30-minute synchronous-command cap, Setup will mark the script timed out even though DISM is still working in the background. Schedule it post-OOBE via schtasks or RunOnce, or run it before sysprep on the build VM where you can wait it out interactively.