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.
| Location | When it runs | Context | Has network? | Use for |
|---|---|---|---|---|
SetupComplete.cmd | After install completes, before first logon | SYSTEM | Yes (usually) | Machine-level config, software installs, registry edits that need no user context |
FirstLogonCommands | At first user logon, as that user | Logged-in user | Yes | Per-user config, tasks that need a user profile to exist |
RunSynchronousCommand (specialize) | During the specialize pass, before OOBE | SYSTEM | Driver-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.
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 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
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
) Installing packages immediately after
:: After the Chocolatey install block above:
choco install firefox googlechrome vlc -y --no-progress 2>&1 >> %TEMP%\choco-install.logSet execution policy to RemoteSigned
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 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)
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" 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
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 /fInstall OpenSSH Server (for remote management)
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.
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)
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 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
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> 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
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." 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
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" /addConfigure BGInfo for desktop identification
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
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, FinlandDisable automatic Windows Update restart
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 Clean up Windows.old and temporary files
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.