Turns a fresh Windows + WSL machine into a cozy, themed shell: an Ubuntu distro running zsh + Starship + modern CLI tools, surfaced through a Windows Terminal profile in Cascadia Code Nerd Font. Everything is opt-in — pick what you want, skip the rest.
The Windows half (install.ps1) handles WSL, the distro, the font, and the terminal profile. The Linux half (comfort-shell-bootstrap.sh) configures the shell itself, and also runs standalone — copy it onto any Ubuntu host and run it directly. setup.ps1 is the one-line entry point: it downloads and verifies both, then runs install.ps1.
- Quick start
- Advanced setup
- Scripts
- setup.ps1 (one-line setup)
- install.ps1 (Windows side)
- comfort-shell-bootstrap.sh (Linux side)
- Customization
- Philosophy
- Known caveats
Open any PowerShell window and run:
irm https://aka.ms/devconfig/wsl-comfort/setup.ps1 | iexNothing to clone. It's interactive by default: pick a distro, then choose each component as it runs. See setup.ps1 for what it downloads and verifies.
What this configures
- WSL + an Ubuntu distro (installs both if missing, with a reboot + auto-resume in between).
- Default login shell set to zsh (or bash, if you opt in).
- starship prompt with a minimal config.
- ~12 apt packages for modern CLI tools:
fzf,ripgrep,fd-find,bat,jq, pluseza/btop/tmuxwhen available. - Homebrew with
gh,direnv,zoxideformulae layered on top. - Clipboard /
openshims in~/bin:pbcopy,pbpaste,open,xdg-open— bridged toclip.exe, PowerShellGet-Clipboard, andcmd /c start. - Managed dotfile blocks in
~/.zprofile+~/.zshrc(or~/.profile+~/.bashrc) for PATH, brew shellenv, prompt init, aliases, and (zsh) keybindings. - Git defaults:
init.defaultBranch=main,pull.rebase=false,core.autocrlf=input. - Cascadia Code Nerd Fonts both mono and not.
- A Windows Terminal profile fragment named
Comfort Shell - <distro>, with a custom Catppuccin-ish dark scheme, the sunglasses icon, andwsl.exe -d <distro>as the command line.
git clone https://github.com/microsoft/WindowsDeveloperConfig.git
cd WindowsDeveloperConfig\wsl-comfort
.\install.ps1# Pass install.ps1 parameters through the one-liner
& ([scriptblock]::Create((irm https://aka.ms/devconfig/wsl-comfort/setup.ps1))) -NonInteractive
& ([scriptblock]::Create((irm https://aka.ms/devconfig/wsl-comfort/setup.ps1))) -Distro Ubuntu-24.04 -BootstrapArgs '--shell=bash','--no-brew'# The same parameters from a clone
.\install.ps1 -NonInteractive
# Example of passing params to the bootstrap
.\install.ps1 -BootstrapArgs '--shell=bash','--no-brew'
# Re-target an existing distro
.\install.ps1 -Distro Ubuntu-24.04# Example of bootstrap params (run directly inside an existing Ubuntu shell)
./comfort-shell-bootstrap.sh --non-interactive
./comfort-shell-bootstrap.sh --dry-runsetup.ps1 accepts -NonInteractive, -Distro, and -BootstrapArgs too, and passes them through to install.ps1.
| Flag | Default | Effect |
|---|---|---|
--shell=zsh|bash |
zsh |
Pick the default login shell. |
--non-interactive |
off | Accept all defaults; no prompts. |
--no-brew |
off | Skip Homebrew (and its formulae). |
--no-shims |
off | Skip pbcopy/pbpaste/open/xdg-open. |
--no-prompt |
off | Skip starship. |
--no-tools |
off | Skip the apt CLI tools list. |
--minimal |
off | Shorthand for --no-brew --no-shims --no-tools. |
--force |
off | Overwrite existing configs (e.g. ~/.config/starship.toml) instead of preserving them. |
--dry-run |
off | Print what would happen; make no changes. |
--help / -h |
— | Print usage. |
| Script | Runs on | Role |
|---|---|---|
setup.ps1 |
Windows (PowerShell 5.1 or 7) | One-line entry point: downloads the signed release copy of this folder, verifies it, and runs install.ps1 from a per-user copy. |
install.ps1 |
Windows (PowerShell 5.1 or 7) | Orchestrator: ensures WSL, picks/installs a distro, invokes the bootstrap inside it, installs the font, drops the WT profile. |
comfort-shell-bootstrap.sh |
Ubuntu (inside WSL or bare-metal) | Installer: configures the shell, prompt, tools, shims, Homebrew, git defaults, and dotfiles. |
content-hashes.psd1 |
— (data) | Signed SHA-256 list for files that can't carry a signature, such as comfort-shell-bootstrap.sh. setup.ps1 won't run a file that doesn't match. |
irm https://aka.ms/devconfig/wsl-comfort/setup.ps1 | iex runs this launcher. It:
- Stops early if organization policy sets PowerShell script execution to
Restricted. - Downloads
install.ps1andcontent-hashes.psd1from the signedwsl-comfort/release copy at a pinned commit, and checks that both carry a valid Microsoft Authenticode signature. - Downloads each file listed in
content-hashes.psd1and checks its SHA-256, after converting CRLF line endings to LF. - Stages the verified files in a new folder under
%LOCALAPPDATA%\DevConfig\wsl-comfort. Later runs never change it, so the reboot auto-resume can come back to it. - Runs
install.ps1from that folder in the same console and PowerShell edition, with process-scopedRemoteSigned, passing through-NonInteractive,-Distro, and-BootstrapArgs. From 32-bit PowerShell, which can't seewsl.exe, it uses 64-bit Windows PowerShell instead.
If any check fails, nothing runs and the new folder is removed. The launcher doesn't elevate: the font, Terminal profile, distro, and resume entry all belong to the signed-in user, and wsl --install asks for administrator approval itself when WSL is missing.
The one-liner always downloads the published files. They're checked when downloaded; a resume after the reboot reuses them without checking again. To run a customized comfort-shell-bootstrap.sh, use a clone.
The script runs five labeled steps and updates the console title with the current step:
| # | Step | What it does | Key behavior |
|---|---|---|---|
| 1 | Ensuring WSL platform | wsl.exe --status probe. If WSL is missing, runs wsl.exe --install --no-distribution and exits via reboot. |
Registers a RunOnce auto-resume so the script picks up where it left off after the reboot. |
| 2 | Choosing Ubuntu distro | Lists installed Ubuntu* distros from wsl -l -q. If none, offers the 4 supported LTS lines. |
Install-NewDistro retries up to 3 times (5s → 15s backoff) and gives actionable hints on failure (DNS, proxy, VPN). |
| 3 | Running Comfort Shell bootstrap | Stages comfort-shell-bootstrap.sh to %TEMP%, copies it into the distro's $HOME, strips CRLFs, makes it executable, runs it. |
Uses Invoke-NativeConsole so the child sees a real TTY (needed for /dev/tty prompts in the bootstrap). |
| 4 | Installing Cascadia Code Nerd Fonts | Downloads fonts from GitHub release; extracts and registers them. | Detects "already installed" and skips if so. |
| 5 | Installing Windows Terminal profile | Writes a JSON fragment under %LOCALAPPDATA%\Microsoft\Windows Terminal\Fragments\ComfortShell\comfort-shell-<slug>.fragment.json. |
Deterministic per-distro GUID (MD5 of comfort-shell:<distro>) so re-runs update in place and multiple distros coexist. Touches settings.json mtime to nudge WT's hot-reload. |
Parameters are listed in Advanced setup.
When wsl.exe --install requires a reboot, the script:
- Registers
HKCU\...\RunOnce\ComfortShellResumewith an encoded launcher that re-opens Windows Terminal and re-invokesinstall.ps1from where it ran: your clone, or that run's folder under%LOCALAPPDATA%\DevConfig\wsl-comfortwhen you used the one-liner. - Round-trips
NonInteractive,Distro, andBootstrapArgsas base64-JSON via-ResumeEncodedArgsso the post-reboot run picks up the original intent. - Optionally reboots immediately (10-second cancellable countdown) or hands control back to the user.
On a successful run the RunOnce key is cleared at the top of the script (so an unrelated subsequent reboot doesn't re-fire the installer).
The script also calls Reset-TerminalInputMode after every native console invocation. wsl.exe enables Win32 Input Mode and focus reporting on the parent console and doesn't always restore them; without this, later Read-Host calls echo escape sequences like ^[[I and ^[[9;15;9;0;0;1_.
The bootstrap shows a plan + asks for confirmation, then runs N labeled steps (count depends on enabled modules). All steps are idempotent.
| # | Step | Function | What it does |
|---|---|---|---|
| 1 | Shell | install_shell |
Installs zsh (with zsh-autosuggestions + zsh-syntax-highlighting) and chsh's the user to it. In skel mode, edits /etc/adduser.conf DSHELL= instead. |
| 2 | Prompt | install_prompt |
Installs starship via the upstream install.sh. Writes ~/.config/starship.toml with a minimal config. |
| 3 | CLI tools | install_cli_tools |
apt-get install for the required list (build-essential, pkg-config, fzf, ripgrep, fd-find, bat, jq, unzip, curl, wget, git, ca-certificates) and optional list (eza, btop, tmux). Drops ~/bin/fd and ~/bin/bat shims when the Debian binary names differ. |
| 4 | CLI shims | install_cli_shims |
Writes ~/bin/pbcopy (→ clip.exe), ~/bin/pbpaste (→ Get-Clipboard | tr -d "\r"), ~/bin/open (→ cmd.exe /c start), ~/bin/xdg-open (→ open). Marked # comfort-shell shim so the script can recognize its own files. |
| 5 | Homebrew | install_homebrew |
Runs the upstream install.sh with NONINTERACTIVE=1 CI=1. Then brew install gh direnv zoxide. In skel mode this is deferred — see Skel mode. |
| 6 | Git defaults | install_git_defaults |
git config --global for init.defaultBranch=main, pull.rebase=false, core.autocrlf=input. Only sets values that aren't already set (or with --force). |
| 7 | Shell config | install_shell_config |
Replaces a # >>> comfort-shell >>> managed block in ~/.zprofile + ~/.zshrc (or ~/.profile + ~/.bashrc) with PATH, brew shellenv, prompt init, aliases (ls/ll/lt/cat/grep/find + git shortcuts), persistent zsh history (~/.zsh_history) with duplicate suppression, /etc/zsh_command_not_found integration when available, and zsh keybindings for Windows Terminal (including Up/Down history search by prefix, Ctrl+Left/Right, Home/End, Ctrl+Backspace, etc.). |
Before the steps run, heal_wsl_issues cleans NUL bytes from /etc/wsl.conf (a known WSL corruption that produces Invalid key name warnings on every shell launch).
All output is teed to ~/.comfort-shell-install.log (using stdbuf -o0 tee when available, so partial lines flush in real time).
Options are listed in Advanced setup.
This lets install.ps1 bootstrap a fresh distro as root before any user exists — the real account is created on first Windows Terminal launch, inherits the dotfiles, and Homebrew installs itself on that first shell. When comfort-shell-bootstrap.sh runs as root (typical for pre-baking a distro image), it switches to skel mode:
HOMEis redirected to/etc/skelso dotfiles land in the template every new user is created from.- Default shell is set via
/etc/adduser.conf'sDSHELL=, notchsh. - starship installs to
/usr/local/bin(PATH-visible to every user). - Homebrew is deferred to the new user's first interactive shell via a first-run hook in the skel
.zshrc, since its installer needs a real (non-root) user.
- Change the apt package lists. Edit
COMFORT_APT_REQUIREDandCOMFORT_APT_OPTIONALnear the top ofcomfort-shell-bootstrap.sh. - Change the prompt. Edit the
write_file "$cfg" 644 ...block ininstall_prompt(or setINSTALL_PROMPT="no"and bring your own). - Change the Terminal theme or font. Edit
Install-TerminalProfileininstall.ps1— the color scheme is inline, andfont.face/font.sizeare right there too.
These edits apply when you run .\install.ps1 from a clone; the one-liner always downloads the published files. Contributing a change to comfort-shell-bootstrap.sh? Update its hash in src\wsl-comfort\content-hashes.psd1 too — src\tests\comfort-shell\unit.ps1 checks it and prints the expected value.
- Opt-in, not all-or-nothing. Every piece — starship, the CLI bundle, clipboard shims, Homebrew, git defaults — can be skipped independently, or all at once with
--minimal. - One command end-to-end. The one-liner (or
.\install.ps1from a clone) takes a fresh Windows machine to a fully configured, themed Windows Terminal profile. - Idempotent. Re-running is always safe — managed dotfile blocks are replaced in place, the WT fragment is rewritten with a deterministic GUID, and already-installed packages are skipped.
- Standalone halves.
comfort-shell-bootstrap.shdoesn't needinstall.ps1— drop it on any Ubuntu host (WSL or not) and it works on its own.
| Decision | Rationale |
|---|---|
| Two scripts instead of one | The bootstrap is genuinely useful on its own (any Ubuntu host). Splitting it means install.ps1 is a thin orchestrator and the actual shell setup is portable. |
Separate one-line launcher (setup.ps1) |
Matches the repo's other one-liners: a pinned payload commit and Microsoft signature checks before anything runs. Unlike Windows Dev Config's bootstrap.ps1, it doesn't elevate or install under %ProgramData%, because Comfort Shell configures the signed-in user's profile. Each run stages into its own folder, so a later run never changes files a pending resume will use. |
| Signed hash list for the bootstrap | .sh files can't carry an Authenticode signature, so the signed content-hashes.psd1 pins their SHA-256. Hashing with LF line endings keeps the result independent of Git checkout settings. |
| Auto-resume via RunOnce + base64-JSON | wsl --install always requires a reboot on a fresh machine. Manually re-running the script with the same arguments is friction; RunOnce + a base64-encoded payload restores the exact original invocation. |
Win32 Input Mode reset after every wsl.exe |
wsl.exe enables Win32 Input Mode and focus reporting on the parent console and doesn't restore them. Without Reset-TerminalInputMode, follow-up Read-Host prompts echo ^[[I etc. |
Invoke-NativeConsole (Start-Process -NoNewWindow -Wait) |
We need the child to see a real TTY so /dev/tty reads inside the bootstrap (interactive prompts) work. Plain & wsl.exe over a pipeline breaks this. |
| Deterministic per-distro WT profile GUID | MD5(comfort-shell:<distro>) gives stable GUIDs: re-runs update in place; different distros (Ubuntu vs Ubuntu-24.04) coexist as separate profiles. |
Touch settings.json mtime |
Windows Terminal re-scans Fragments\\*.json when settings.json changes. Touching it makes the new profile appear without a WT restart. |
| Managed blocks in dotfiles | # >>> comfort-shell >>> / # <<< comfort-shell <<< markers let us own a slice of .zshrc without trampling user edits. |
| Skel mode for fresh distros | Lets the Windows-side flow run the bootstrap as root before any user exists. The user is created on first WT launch, inherits the dotfiles, and triggers the deferred brew install. |
stdbuf -o0 tee for logging |
Without it, tee buffers partial lines and interactive prompts can render in the middle of banners. |
Prompts on stderr (not /dev/tty) |
Routing them to stderr keeps prompts in tee'd order with banner output, instead of racing the buffered banner to the terminal. |
| Ubuntu-only | The bootstrap calls apt-get directly and relies on package names that are Debian-family specific. Other distros are rejected at preflight to fail loudly rather than half-install. |
This system was inspired by Scott Hanselman's WSL Comfort shell.
