Skip to content

Instantly share code, notes, and snippets.

@fstanis
Created May 14, 2026 09:16
Show Gist options
  • Select an option

  • Save fstanis/5f454d15b2455ff361072e9dc3945d65 to your computer and use it in GitHub Desktop.

Select an option

Save fstanis/5f454d15b2455ff361072e9dc3945d65 to your computer and use it in GitHub Desktop.
Bash script to install Windows to a USB drive (Windows To Go)
#!/bin/bash
# win2usb.sh — install Windows to a USB drive from an ISO, on Linux.
#
# Usage: win2usb.sh [--wtg] <iso-path> <target-device> [wim-index]
#
# Partitions the target as GPT (ESP + MSR + NTFS), applies the selected
# WIM index, and sets up UEFI boot via a BCD store built from the ISO's
# BCD-Template.
#
# Pass --wtg to build as a "Windows To Go" drive. In practice, the only
# difference is that this disables recovery.
#
# Dependencies:
# Arch: sudo pacman -S gptfdisk dosfstools ntfs-3g wimlib parted hivex
# Debian: sudo apt install gdisk dosfstools ntfs-3g wimtools parted hivex
# Copyright (c) 2026 Filip Stanis
#
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
# PERFORMANCE OF THIS SOFTWARE.
set -euo pipefail
readonly ESP_SIZE_MIB=260
readonly MSR_SIZE_MIB=16
readonly ESP_TYPE_GUID=C12A7328-F81F-11D2-BA4B-00A0C93EC93B
readonly MSR_TYPE_GUID=E3C9E316-0B5C-4DB8-817D-F92DF00215AE
readonly WIN_TYPE_GUID=EBD0A0A2-B9E5-4433-87C0-68B6B72699C7
readonly BCD_TEMPLATE_PATH=Windows/System32/config/BCD-Template
readonly WIN_EFI_DIR=Windows/Boot/EFI
readonly WIN_FONTS_DIR=Windows/Boot/Fonts
readonly BCD_BOOTMGR_GUID="{9dea862c-5cdd-4e70-acc1-f32b344d4795}"
readonly BCD_OSLOADER_GUID="{b012b84d-c47c-4ed5-b722-c0c42163e569}"
readonly REQUIRED_TOOLS=(
sgdisk mkfs.fat mkfs.ntfs wimlib-imagex mount umount udevadm
partprobe wipefs lsblk mountpoint mktemp awk hivexregedit blkid
)
iso_path=""
target_device=""
wim_index=""
iso_mount=""
esp_mount=""
wim_path=""
wtg_mode=false
main() {
parse_args "$@"
require_root
require_tools
assert_removable_target
confirm_target
trap cleanup EXIT
prepare_workdirs
mount_iso
resolve_wim_path
resolve_wim_index
partition_target
format_partitions
apply_windows_image
populate_efi_system_partition
log "Done. Unplug ${target_device} and boot from it."
}
log() {
echo "[wtg] $*"
}
die() {
echo "Error: $*" >&2
exit 1
}
parse_args() {
if [[ "${1:-}" == "--wtg" ]]; then
wtg_mode=true
shift
fi
if (( $# < 2 || $# > 3 )); then
echo "Usage: $0 [--wtg] <iso-path> <target-device> [wim-index]" >&2
exit 1
fi
iso_path="$1"
target_device="$2"
if (( $# == 3 )); then
wim_index="$3"
fi
[[ -f "${iso_path}" ]] || die "ISO not found: ${iso_path}"
[[ -b "${target_device}" ]] || die "Not a block device: ${target_device}"
}
require_root() {
(( EUID == 0 )) || die "Must run as root (try: sudo $0 ...)"
}
require_tools() {
local tool
for tool in "${REQUIRED_TOOLS[@]}"; do
command -v "${tool}" >/dev/null 2>&1 \
|| die "Missing required tool: ${tool}"
done
}
assert_removable_target() {
assert_not_root_device
assert_usb_or_removable
}
assert_not_root_device() {
local mp
while read -r mp; do
[[ "${mp}" != "/" ]] \
|| die "Refusing to operate: ${target_device} contains the root filesystem."
done < <(lsblk -lnp -o MOUNTPOINT "${target_device}" 2>/dev/null)
}
assert_usb_or_removable() {
local tran rm_flag
tran="$(lsblk -dnp -o TRAN "${target_device}" 2>/dev/null | awk 'NF{print $1}')"
rm_flag="$(lsblk -dnp -o RM "${target_device}" 2>/dev/null | awk 'NF{print $1}')"
[[ "${tran}" == "usb" || "${rm_flag}" == "1" ]] \
|| die "${target_device} is not a removable/USB device" \
"(transport=${tran:-unknown}, removable=${rm_flag:-unknown})."
}
confirm_target() {
local reply
echo "About to DESTROY all data on ${target_device}:" >&2
lsblk -o NAME,SIZE,MODEL "${target_device}" >&2
read -r -p "Type 'yes' to continue: " reply
[[ "${reply}" == "yes" ]] || die "Aborted."
}
prepare_workdirs() {
iso_mount="$(mktemp -d)"
esp_mount="$(mktemp -d)"
}
cleanup() {
unmount_if_mounted "${esp_mount}"
unmount_if_mounted "${iso_mount}"
remove_dir_if_exists "${esp_mount}"
remove_dir_if_exists "${iso_mount}"
}
unmount_if_mounted() {
[[ -n "${1:-}" ]] || return
mountpoint -q "$1" 2>/dev/null && umount "$1" || true
}
remove_dir_if_exists() {
[[ -n "${1:-}" && -d "$1" ]] && rmdir "$1" 2>/dev/null || true
}
mount_iso() {
mount -o loop,ro "${iso_path}" "${iso_mount}"
}
resolve_wim_path() {
local candidate
for candidate in sources/install.wim sources/install.esd; do
if [[ -f "${iso_mount}/${candidate}" ]]; then
wim_path="${iso_mount}/${candidate}"
log "Using image: ${candidate}"
return
fi
done
die "No install.wim or install.esd found in ISO."
}
resolve_wim_index() {
[[ -n "${wim_index}" ]] && return
echo "Available Windows editions:" >&2
wimlib-imagex info "${wim_path}" \
| awk '/^Index:|^Name:|^Display Name:/' >&2
local reply
read -r -p "Select WIM index: " reply
[[ "${reply}" =~ ^[0-9]+$ ]] || die "Invalid index: ${reply}"
wim_index="${reply}"
}
partition_target() {
unmount_all_on_target
wipefs --all --force "${target_device}"
sgdisk --zap-all "${target_device}"
create_gpt_partitions
partprobe "${target_device}"
udevadm settle
}
unmount_all_on_target() {
local mp
while read -r mp; do
[[ -z "${mp}" ]] && continue
umount "${mp}" || die "Failed to unmount ${mp}; target is busy."
done < <(lsblk -lnp -o MOUNTPOINT "${target_device}")
}
create_gpt_partitions() {
sgdisk \
"--new=1:0:+${ESP_SIZE_MIB}MiB" \
"--typecode=1:${ESP_TYPE_GUID}" \
"--change-name=1:EFI" \
"--new=2:0:+${MSR_SIZE_MIB}MiB" \
"--typecode=2:${MSR_TYPE_GUID}" \
"--change-name=2:Microsoft reserved" \
"--new=3:0:0" \
"--typecode=3:${WIN_TYPE_GUID}" \
"--change-name=3:Windows" \
"${target_device}"
}
partition_path() {
if [[ "${target_device}" =~ [0-9]$ ]]; then
echo "${target_device}p$1"
else
echo "${target_device}$1"
fi
}
format_partitions() {
mkfs.fat -F 32 -n "ESP" "$(partition_path 1)"
mkfs.ntfs --quick --label "Windows" "$(partition_path 3)"
}
apply_windows_image() {
log "Applying image to $(partition_path 3) (this may take a while)..."
wimlib-imagex apply "${wim_path}" "${wim_index}" "$(partition_path 3)"
}
populate_efi_system_partition() {
mount "$(partition_path 1)" "${esp_mount}"
install_boot_binaries
install_boot_fonts
install_bcd
install_fallback_loader
umount "${esp_mount}"
}
install_boot_binaries() {
local staging
staging="$(mktemp -d)"
extract_wim_path "/${WIN_EFI_DIR}" "${staging}"
mkdir -p "${esp_mount}/EFI/Microsoft/Boot"
cp -a "${staging}/EFI/." "${esp_mount}/EFI/Microsoft/Boot/"
rm -rf "${staging}"
}
install_boot_fonts() {
local staging
staging="$(mktemp -d)"
if ! extract_wim_path "/${WIN_FONTS_DIR}" "${staging}" 2>/dev/null; then
rm -rf "${staging}"
return
fi
mkdir -p "${esp_mount}/EFI/Microsoft/Boot/Fonts"
cp -a "${staging}/Fonts/." "${esp_mount}/EFI/Microsoft/Boot/Fonts/"
rm -rf "${staging}"
}
extract_wim_path() {
wimlib-imagex extract "${wim_path}" "${wim_index}" \
"$1" --dest-dir="$2" --no-acls --no-attributes
}
install_fallback_loader() {
local src="${esp_mount}/EFI/Microsoft/Boot/bootmgfw.efi"
[[ -f "${src}" ]] || die "bootmgfw.efi missing after boot binary install."
mkdir -p "${esp_mount}/EFI/Boot"
cp "${src}" "${esp_mount}/EFI/Boot/bootx64.efi"
}
install_bcd() {
local dst="${esp_mount}/EFI/Microsoft/Boot/BCD"
extract_bcd_template "${dst}"
populate_bcd "${dst}"
}
extract_bcd_template() {
try_extract_bcd_template "${wim_path}" "${wim_index}" "$1" && return
try_extract_bcd_template "${iso_mount}/sources/boot.wim" 1 "$1" && return
die "No BCD-Template found in install image or boot.wim."
}
try_extract_bcd_template() {
local wim="$1" index="$2" dst="$3" staging
[[ -f "${wim}" ]] || return 1
staging="$(mktemp -d)"
if ! wimlib-imagex extract "${wim}" "${index}" \
"/${BCD_TEMPLATE_PATH}" --dest-dir="${staging}" \
--no-acls --no-attributes 2>/dev/null; then
rm -rf "${staging}"
return 1
fi
if [[ ! -f "${staging}/BCD-Template" ]]; then
rm -rf "${staging}"
return 1
fi
cp "${staging}/BCD-Template" "${dst}"
rm -rf "${staging}"
}
populate_bcd() {
merge_bcd_static_elements "$1"
merge_bcd_device_element "$1" "${BCD_BOOTMGR_GUID}" 11000001 1
merge_bcd_device_element "$1" "${BCD_OSLOADER_GUID}" 11000001 3
merge_bcd_device_element "$1" "${BCD_OSLOADER_GUID}" 21000001 3
mark_bcd_as_system_store "$1"
log "Populated BCD store."
}
mark_bcd_as_system_store() {
local reg
reg="$(mktemp --suffix=.reg)"
cat > "${reg}" <<'EOF'
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\BCD\Description]
"KeyName"="BCD00000001"
"System"=dword:00000001
EOF
hivexregedit --merge --prefix 'HKEY_LOCAL_MACHINE\BCD' "$1" "${reg}"
rm -f "${reg}"
}
merge_bcd_static_elements() {
local reg
reg="$(mktemp --suffix=.reg)"
generate_bcd_static_reg > "${reg}"
hivexregedit --merge --prefix 'HKEY_LOCAL_MACHINE\BCD' "$1" "${reg}"
rm -f "${reg}"
}
generate_bcd_static_reg() {
generate_bcd_bootmgr_reg
generate_bcd_osloader_reg
if [[ "${wtg_mode}" == true ]]; then
generate_bcd_recovery_disable_reg
fi
}
generate_bcd_bootmgr_reg() {
local display_order
display_order="$(guid_to_multisz_hex "${BCD_OSLOADER_GUID}")"
cat <<EOF
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\\BCD\\Objects\\${BCD_BOOTMGR_GUID}\\Elements\\23000003]
"Element"="${BCD_OSLOADER_GUID}"
[HKEY_LOCAL_MACHINE\\BCD\\Objects\\${BCD_BOOTMGR_GUID}\\Elements\\24000001]
"Element"=hex(7):${display_order}
EOF
}
generate_bcd_osloader_reg() {
cat <<EOF
[HKEY_LOCAL_MACHINE\\BCD\\Objects\\${BCD_OSLOADER_GUID}\\Elements\\12000002]
"Element"="\\\\windows\\\\system32\\\\winload.efi"
[HKEY_LOCAL_MACHINE\\BCD\\Objects\\${BCD_OSLOADER_GUID}\\Elements\\12000004]
"Element"="Windows 11"
EOF
}
generate_bcd_recovery_disable_reg() {
cat <<EOF
[HKEY_LOCAL_MACHINE\\BCD\\Objects\\${BCD_OSLOADER_GUID}\\Elements\\26000024]
"Element"=hex:00
EOF
}
merge_bcd_device_element() {
local bcd="$1" object_guid="$2" element_id="$3" part_num="$4"
local reg dev
reg="$(mktemp --suffix=.reg)"
dev="$(build_gpt_device_blob "${part_num}")"
cat > "${reg}" <<EOF
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\\BCD\\Objects\\${object_guid}\\Elements\\${element_id}]
"Element"=hex:${dev}
EOF
hivexregedit --merge --prefix 'HKEY_LOCAL_MACHINE\BCD' "${bcd}" "${reg}"
rm -f "${reg}"
}
build_gpt_device_blob() {
local disk_guid part_guid
disk_guid="$(blkid -o value -s PTUUID "${target_device}")"
part_guid="$(blkid -o value -s PARTUUID "$(partition_path "$1")")"
[[ -n "${disk_guid}" && -n "${part_guid}" ]] \
|| die "Could not read GPT GUIDs from ${target_device}."
local d p
d="$(guid_to_le_hex "${disk_guid}")"
p="$(guid_to_le_hex "${part_guid}")"
printf '%s' \
"00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00," \
"06,00,00,00,00,00,00,00,48,00,00,00,00,00,00,00," \
"${p}," \
"00,00,00,00,00,00,00,00," \
"${d}," \
"00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00"
}
guid_to_le_hex() {
local g="${1//[\{\}-]/}"
printf '%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s' \
"${g:6:2}" "${g:4:2}" "${g:2:2}" "${g:0:2}" \
"${g:10:2}" "${g:8:2}" "${g:14:2}" "${g:12:2}" \
"${g:16:2}" "${g:18:2}" \
"${g:20:2}" "${g:22:2}" "${g:24:2}" "${g:26:2}" "${g:28:2}" "${g:30:2}"
}
guid_to_multisz_hex() {
local i out=""
for (( i = 0; i < ${#1}; i++ )); do
printf -v out '%s%02x,00,' "${out}" "'${1:i:1}"
done
printf '%s' "${out}00,00,00,00"
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment