Cross-platform Windows deployment toolkit - PXE boot, an image library, driver packs and task sequences, driven from a desktop app that runs on macOS and Windows.
No MDT. No WDS. No Windows Server. The deployment server is your laptop.
Status: early development. The app builds and runs, and the sidecar answers every panel command. Several panels are still placeholders and the WinPE client is being rewritten - not usable for production imaging yet.
AGENT_NOTES.mdrecords exactly what works today.
Microsoft retired the Microsoft Deployment Toolkit, and on 6 January 2026 removed it from the Download Center entirely. MDT-based workflows still run in thousands of organisations, but the toolkit can no longer be obtained, and the successor story (Autopilot / Intune) assumes cloud-managed, internet-connected devices.
WinDeployKit rebuilds the part that mattered - get a bare machine to a working, domain-joined, driver-complete Windows install over the network - without the retired product, a Windows Server, or an on-prem WDS role.
It is also deliberately cross-platform: a technician with a MacBook can be the deployment server on a site that has no server at all.
| Area | Capability |
|---|---|
| Netboot | ProxyDHCP + TFTP (dnsmasq), HTTP (Caddy), iPXE -> wimboot chain, Secure Boot shim path, hidden read-only SMB Deploy$ share |
| Linux installers | Debian and Ubuntu from the Operating Systems node. Debian keeps no ISO: Add fetches the mirror's netboot kernel and initrd (about 95 MB) and the installer takes everything else from the Debian mirror. Ubuntu 22.04+ has no netboot installer, so Add downloads the live-server ISO into the library and the installer boots off it (packages from the Ubuntu archive). Task sequences compile to a d-i preseed or a Subiquity autoinstall and appear as a submenu under the matching entry (Secure Boot off) |
| Boot Images | Pull boot.wim out of any Windows ISO and overlay it with wimlib - no ADK on the imaging machine (one prior ADK export per WinPE build; see Requirements) |
| Operating Systems | Windows ISOs mounted read-only and served zero-copy - install.wim straight out of the ISO, never extracted. Acquisition from Microsoft Evaluation Center, a torrent catalog, or a URL |
| Out-of-Box Drivers | Vendor driver-pack catalogs (Dell, HP, Lenovo, Acer, Microsoft Surface) resolved by model, hash-verified on download, injected offline before first boot |
| Task Sequences | Named deployment recipes, one per platform. Windows: compile to unattend.xml - computer naming, domain join, machine OU, product key - plus ordered first-boot steps (registry, command, PowerShell, or a Script by URL streamed with `irm |
| Runs in the menu bar | Closing the window hides WinDeployKit to the macOS menu bar (Windows: the notification area) and PXE and the deployment share keep serving. The icon's menu has Open and Quit; File > "Close to menu bar" turns the behaviour off |
| Monitoring | Live PXE activity log (which client fetched which boot file) and per-device imaging logs streamed back during deployment. Both clearable |
| Transfers | The download client - torrents and HTTP, with progress, cancel and hash verification |
+-------------------------------------------------------------+
| Desktop app (Tauri 2 + React + TypeScript) |
| MDT-Workbench-style console tree, one panel per node |
+---------------------------+---------------------------------+
| NDJSON over stdio
+---------------------------v---------------------------------+
| Sidecar (PowerShell 7, long-lived) |
| dispatch loop -> Handle-<Command> |
+---------------------------+---------------------------------+
| manages
+---------------------+----------------------+
v v v
dnsmasq Caddy SMB share
TFTP :69 HTTP :8080 Deploy$
ProxyDHCP wimboot + WIMs (hidden, read-only)
|
v
PXE client -> iPXE -> wimboot -> WinPE agent
|
+- partition (GPT: EFI / MSR / OS / Recovery)
+- apply install.wim (DISM)
+- inject drivers (offline)
+- plant unattend.xml
+- stream log back to Monitoring
The WinPE side is headless and server-driven: it collects a job from the
server, executes it, and streams status back. The wizard lives in the app, not on
the machine being imaged - a deliberate departure from MDT's LiteTouch model.
Rationale in AGENT_NOTES.md section Locked decisions.
To run the app
- macOS 11+ or Windows 10/11
- PowerShell 7.4+ (
pwshon PATH) -brew install --cask powershell - Windows ISOs you are licensed to deploy
To develop
- Node.js 20+, Rust (stable)
- On Windows: VS 2022 with Desktop development with C++
One-time, on a Windows machine with the ADK
Putting PowerShell into a WinPE boot image requires exporting the WinPE optional
components once per WinPE build family
(scripts/prepare-fieldiso-wim-inject.ps1). Once that export exists, every later
boot-image operation - overlay, driver injection, rebuild - runs on macOS with
wimlib. The imaging machine never needs the ADK.
cd app
npm install
npm run tauri:dev # Vite on :42410, Tauri window; sidecar spawns on first panel call| Command | Does |
|---|---|
npm run typecheck |
tsc --noEmit |
npm run build |
Production frontend build |
npm run tauri:build |
Packaged app |
The sidecar speaks NDJSON on stdio and can be exercised without the UI:
printf '{"id":1,"cmd":"GetPxeBootPluginStatus","params":{}}\n' \
| pwsh -NoProfile -File sidecar/windeploykit-sidecar.ps1Two rules that are enforced, not suggested. Both exist because breaking them has already cost real debugging time.
The sidecar runs Set-StrictMode -Version Latest, and every dot-sourced lib
inherits it. Under StrictMode, reading a property that does not exist throws
instead of returning $null. ConvertFrom-Json omits absent keys entirely, so
every optional field read directly is a latent crash.
The trap that catches people is that a null check does not protect you - the property is read before the comparison happens:
if ($null -ne $json.maybe) { ... } # THROWS if 'maybe' is absent
if ($json.maybe) { ... } # THROWS - same reason
$json.maybe # THROWS
$hashtable.maybe # THROWS too (Latest covers hashtables)Read every optional field through the accessor instead:
$value = Get-AppSidecarJsonProp -Item $json -Name 'maybe' # $null when absentOther StrictMode facts worth knowing:
| Expression | Result |
|---|---|
$null.Count |
throws |
@($null).Count |
1, not 0 - wrapping does not give you an empty array |
@($x) where $x is absent-property |
still throws; wrap the accessor, not the read |
uninitialised $variable |
throws |
| array index past the end | throws |
Run pwsh -File scripts/test-strictmode.ps1 before committing sidecar changes.
It checks the accessor, proves the IPC dispatch loop answers malformed requests
rather than hanging, and fails on any new unsafe null-guard. The allowlist in
that script requires a written reason per entry, so silencing it is a decision
rather than a reflex.
No non-ASCII characters anywhere in the repository - no em dashes, no en dashes, no smart quotes, no box-drawing characters, no arrows, no emoji. This applies to documentation, code, comments, commit messages and UI strings alike.
Use the ASCII equivalent:
| Instead of | Write |
|---|---|
| em dash, en dash | - |
| arrows | ->, <-, => |
| ellipsis | ... |
| section sign | the word section |
| box drawing | -, ` |
| check / cross glyphs | OK, x |
| emoji | nothing, or a bracketed word such as [key] |
Check with:
pwsh -File scripts/test-ascii.ps1It scans every tracked text file, skipping binaries by content sniffing rather
than by extension, and exits non-zero naming file, line and character. Do not
reach for grep -P here: BSD grep on macOS has no -P.
In .tsx, remember that a bare > is invalid in JSX text: write -> inside
an element body, and plain -> inside attributes, strings and comments.
Every scripts/test-*.ps1 runs with pwsh -NoProfile -File. Run the lot before a
push; the two live boot tests are opt-in and need Homebrew QEMU.
| Gate | What it proves |
|---|---|
test-strictmode.ps1, test-strictmode-shapes.ps1 |
The sidecar under StrictMode: accessors, IPC dispatch, null guards. Spawns a real sidecar that adopts Caddy - never during a live boot test |
test-ascii.ps1 |
ASCII-only repository |
test-linux-menu.ps1 |
Linux PXE entries, task-sequence submenu, kernel arguments, no store or mount needed |
test-task-sequence-debian.ps1 / -ubuntu.ps1 |
Preseed / autoinstall compile, shell and YAML quoting, vault first user, the Scripts folder import |
test-task-sequence-library.ps1 / -accounts.ps1 |
The Windows step library and the unattend account, first-boot batch and Script by URL step |
test-linux-install-report.ps1 |
Install feedback: the GET form of the imaging-log ingest against the real listener, and the Linux reporter script driven under sh |
test-deploy-overlay.ps1, test-install-images.ps1, test-eval-iso-catalog.ps1, test-server-eval-conversion.ps1, test-storage-policy.ps1 |
Boot-image overlay, install.wim inventory, evaluation catalog and conversion, the storage split |
test-linux-iso-boot-qemu.sh |
Live: boots the generated menu in QEMU, tiers up to an unattended Debian or Ubuntu install from a published sequence |
test-boot-chain-vm.sh |
Live: the Windows iPXE -> wimboot chain in a VM |
Frontend: cd app && npm run typecheck; host: cd app/src-tauri && cargo check.
| Path | What |
|---|---|
app/ |
Tauri 2 + React frontend |
app/src/components/navConfig.ts |
The console tree - MDT node names, in deployment order |
app/src/panels/ |
One thin file per tree node |
app/src/workspaces/ |
Shared state containers whose sections the panels render |
app/src-tauri/ |
Rust host: window, sidecar process manager (NDJSON framing, id correlation, per-command timeouts) |
sidecar/windeploykit-sidecar.ps1 |
Sidecar entry point - dispatch loop and core handlers |
sidecar/lib/ |
PXE services, task sequences, aria2, five vendor driver catalogs |
sidecar/handlers/ |
The Handle-<Command> IPC surface |
sidecar/pxe/ |
Boot assets: iPXE, wimboot, FieldIso WinPE overlay, boot-file recipes |
vendor/binaries/ |
dnsmasq, wimlib, wimboot, tftpd64, swiftDialog helper |
packaging/ |
Runtime-asset manifests and the bundled vendor driver catalogs |
scripts/ |
Build, packaging and boot-image maintenance |
docs/ |
Style guide, UI direction, data-freshness policy |
Two paths are gitignored and local-only: PSD/ (a FriendsOfMDT/PSD clone kept
purely as a format reference) and usm-reference/ (the originals this was
extracted from, plus the porting notes).
The UI deliberately mirrors the MDT Deployment Workbench: console tree on the left, one panel per node, dense square chrome, right-aligned toolbars. Anyone who has used MDT or WDS should be able to work it out without documentation.
The rules are enforced, not suggested - see
docs/WINDEPLOYKIT_App_StyleGuide.md:
light and dark only (no theme packages), square corners, 32px chrome, 26px rows,
flat surfaces, tooltips instead of paragraphs.
Panels show live machine state, not cached data -
docs/DATA_FRESHNESS.md sets out where caching is still
legitimate (vendor catalogs) and where it would lie (everything else).
The sibling project PSOpenAD-FE shares this exact design system. Style-guide changes belong in both.
WinDeployKit is not affiliated with, endorsed by, or sponsored by Microsoft. Windows, Windows PE, MDT and WDS are trademarks of Microsoft Corporation, used here only to describe what this tool interoperates with. No Microsoft branding, logos or typefaces are used.
MIT - see LICENSE. Use it, borrow from it, improve it; the one
condition is that the copyright notice travels with the copy.
See NOTICE.md for third-party components (dnsmasq, wimlib, Caddy,
aria2, iPXE/wimboot) and their licences.
Microsoft components are never committed to this repository - boot.wim,
install.wim, bootmgfw.efi, BCD, boot.sdi and the WinPE optional components
all come from your own licensed media and ADK installation, and their use is
governed by your Windows and ADK licence terms.