I want to do Android development on a Windows machine without giving up a real Linux toolchain underneath it โ fast Gradle builds on a native Linux filesystem, systemd, udev, the lot โ and without dual-booting or using a “heavy” VM. WSL2 delivers exactly that: Ubuntu running under Windows, Android Studio drawn through WSLg, a physical phone forwarded over USB, and Claude Code in the terminal. Windows becomes just the host.
This guide stands the whole thing up from scratch. A few honest tradeoffs before you start: there’s no emulator here (it’s slow and finicky under nested virtualization โ we use a real device instead), USB has to be forwarded into WSL rather than just working, and a couple of steps need an elevated PowerShell on the Windows side. If you’re comfortable in a terminal, the payoff is a dev box that builds like Linux and lives like Windows.
And, as to be expected in the Cesspool of Knowledge, there’s a twist! About a third of the way in, we install Claude Code โ Anthropic’s terminal agent โ and hand the rest of the setup to it. So from Phase 4 on, every step is tagged with who drives it:
- ๐ง You drive it โ anything on the Windows side (PowerShell), a GUI, or physically plugging in the phone.
- ๐ค Claude Code can drive it โ a plain WSL command it runs for you, pausing to ask whenever it hits a ๐ง moment (a
sudopassword, a Windows command, the phone).
Skipping Claude Code is fine โ the post is 100% doable by a human; you just play both roles and run the ๐ค steps yourself. Everything before the handoff is yours by necessity (you can’t delegate to an agent you haven’t installed yet), so the tags start at Phase 4.
The phases at a glance โ the real goal is Phases 4โ5 (Android Studio + a physical ADB device); the rest is convenience:
- Install WSL and Ubuntu โ the base system.
- Add Ubuntu to Windows Terminal (optional) โ a nicer console than the default.
- Install Claude Code (optional) โ the agent you hand Phases 4โ7 to.
- Install Android Studio + SDK โ the IDE, plus a command-line-provisioned SDK (no GUI wizard).
- Forward a physical device โ USB passthrough so
adbsees your phone. - GitHub CLI (optional) โ lets Claude Code open PRs and manage issues.
- Claude Code in the desktop app (optional) โ a GUI over the CLI, via localhost SSH.
Everything lives inside the Linux filesystem; treat Windows purely as the host.
Phase 0: Prerequisites
A few things need to be in place before starting.
- Windows 10 or 11 (64-bit) with hardware virtualization available โ WSL2 runs on a lightweight utility VM.
- Administrator access โ several install steps run from an elevated PowerShell.
- A physical Android device with USB debugging enabled, needed only for the device-passthrough phase.
- A Claude account (optional) โ only if you want to hand the back half of the setup to Claude Code.
Phase 1: Install WSL and Ubuntu
Install the WSL2 platform, lay down an Ubuntu distro, and turn on systemd. The one-command install below assumes Windows 10 (version 2004 or later) or Windows 11; for older builds, manual feature enablement, or other platform-specific cases, Microsoft’s official WSL installation guide is the canonical reference.
- Install WSL with Ubuntu: From PowerShell as Administrator, install the platform and the Ubuntu distro in one step, then reboot when prompted. On first launch, Ubuntu prompts for a UNIX username and password โ these are local to WSL and need not match your Windows credentials.
1wsl --install -d Ubuntu
- Update the distro: Bring the base image current before installing anything else.
1sudo apt update && sudo apt upgrade -y
- Confirm you are on WSL2: Ubuntu should report
VERSION 2. If it shows1, convert it.
1wsl --list --verbose
2wsl --set-version Ubuntu 2
- Enable systemd: A systemd-managed init makes the rest of the environment behave like a normal Linux box (services,
systemctl, udev). Add the following to/etc/wsl.conf:
1[boot]
2systemd=true
Then restart the WSL backend from PowerShell and reopen Ubuntu. Confirm systemd is actually running as init with systemctl is-system-running (expect running, or degraded if a unit failed โ both mean systemd is PID 1) or ps -p 1 -o comm= (expect systemd). Note that systemctl --version is not a valid check โ that binary prints a version string even when systemd is not the init system.
1wsl --shutdown
Phase 2: Add Ubuntu to Windows Terminal
Optional โ skip if you’re happy launching Ubuntu with wsl or from the default console.
Windows Terminal usually detects a new distro automatically. If it does not, add a profile by hand.
- Open Windows Terminal settings with
Ctrl+,, then Add a new profile โ New empty profile. - Set the profile fields:
- Name:
Ubuntu - Command line:
wsl.exe -d Ubuntu - Starting directory:
\\wsl.localhost\Ubuntu\home\<your-username>
- Or edit the JSON directly โ add an entry to
profiles.list:
1{
2 "name": "Ubuntu",
3 "commandline": "wsl.exe -d Ubuntu",
4 "startingDirectory": "\\\\wsl.localhost\\Ubuntu\\home\\<your-username>"
5}
Phase 3: Install Claude Code
Optional โ but this is the handoff point. Install it and you can delegate the WSL-side of everything below; skip it and you simply run the ๐ค steps yourself.
Anthropic’s Claude Code CLI runs natively in the terminal. The native installer bundles its own runtime โ no Node, npm, or nvm required.
- Install the CLI: The installer drops the
claudebinary at~/.local/bin/claudeand enables background auto-updates.
1curl -fsSL https://claude.ai/install.sh | bash
If the installer reports that ~/.local/bin is not on your PATH:
1echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
2source ~/.bashrc
- Authenticate and verify: Run
claudefrom inside a project directory โ on first launch it opens a browser to sign in with your Claude account. Then confirm the install is healthy.
1claude --version
2claude doctor
Hand the rest off to Claude Code
From here, steps are tagged ๐ง (you drive) or ๐ค (Claude Code can drive). To hand off the ๐ค work, open claude in a scratch directory and paste:
1Read https://blog.banasiak.com/2026/06/wsl2-android-development/ and set up this
2WSL machine by running every ๐ค step. Stop and ask me whenever a ๐ง step is
3needed โ a Windows PowerShell command, a GUI action, or plugging in the phone.
It runs the WSL-side work and pauses for your Windows/physical bits (and the occasional sudo password). Prefer to do it by hand? Every ๐ค step is an ordinary WSL command โ just run them yourself.
Phase 4: Install Android Studio + SDK
This is the core of the whole exercise. Android Studio runs inside WSL via WSLg (the GUI layer WSL2 provides), and we provision the SDK entirely from the terminal โ so Android Studio’s first-run setup wizard has nothing to do and never gets in the way. The emulator stays off; physical devices are forwarded in Phase 5.
- ๐ค Install the X client libraries and
unzip: Studio needs the X client libs to draw through WSLg;unzipunpacks the SDK command-line tools. Most libs are present on a fresh Ubuntu, but install the full set to be safe.
1sudo apt update && sudo apt install -y unzip \
2 libxrender1 libxtst6 libxi6 libfreetype6 fontconfig libgl1 libnss3 \
3 libxcomposite1 libxcursor1 libxdamage1 libxfixes3 libxrandr2 libpulse0
- ๐ค Create an owned
/opt/androidtree: The IDE and SDK live side by side here, and because you own it, nothing below needssudoand Android Studio can self-update without root. The setgid bit makes everything created inside inherit theusersgroup.
1sudo mkdir -p /opt/android
2sudo chown "$USER:users" /opt/android
3sudo chmod 2775 /opt/android # setgid: studio/ and sdk/ inherit the users group
- ๐ค Download and extract Android Studio: Grab the Linux
.tar.gzURL from developer.android.com/studio (Claude Code can read the current URL off the page; by hand, copy it from the download button). Then unpack it into the owned tree โ nosudoneeded.
1# Example shows the current build โ check the page for the latest:
2curl -fL -o /tmp/android-studio.tar.gz \
3 "https://edgedl.me.gvt1.com/android/studio/ide-zips/2026.1.1.9/android-studio-quail1-patch1-linux.tar.gz"
4mkdir -p /opt/android/studio
5tar -xzf /tmp/android-studio.tar.gz --strip-components=1 -C /opt/android/studio
The --strip-components=1 drops the tarball’s leading android-studio/ directory so the contents land directly in /opt/android/studio (launcher at /opt/android/studio/bin/studio).
- ๐ค Set the environment and put everything on PATH: Append one block to
~/.bashrc.JAVA_HOMEpoints at Android Studio’s bundled JBR โ which is also the JDK we’ll runsdkmanagerunder, so there’s no separate JDK to install.ANDROID_HOMEis the SDK root (which the next step populates).
1cat >> ~/.bashrc <<'EOF'
2
3# --- Android development (WSL2) ---
4export JAVA_HOME="/opt/android/studio/jbr"
5export ANDROID_HOME="/opt/android/sdk"
6export PATH="$PATH:/opt/android/studio/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools"
7EOF
8source ~/.bashrc
That single line wires up studio, sdkmanager, avdmanager, adb, and fastboot for every new shell. The source refreshes the current shell only โ any terminals you already had open won’t see the new PATH until you restart them (or source ~/.bashrc there too). If Claude Code made this edit, your own terminal is one of those stale ones: restart it before you run studio or adb yourself, or you’ll get “command not found.”
- ๐ค Provision the SDK from the terminal: This is what lets us skip the GUI wizard. Download the “Command line tools only” package (same download page), arrange it at the
cmdline-tools/latestpathsdkmanagerexpects, accept the licenses non-interactively, then install platform-tools, a platform, and build-tools.
1# ~/.bashrc only loads in interactive shells, so re-export here โ this block
2# must work even when an agent runs it in a fresh, non-interactive shell.
3export JAVA_HOME="/opt/android/studio/jbr"
4export ANDROID_HOME="/opt/android/sdk"
5SDKMGR="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
6
7curl -fL -o /tmp/cmdline-tools.zip \
8 "https://dl.google.com/android/repository/commandlinetools-linux-14742923_latest.zip"
9mkdir -p "$ANDROID_HOME/cmdline-tools"
10unzip -q /tmp/cmdline-tools.zip -d /tmp/clt
11mv /tmp/clt/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest"
12chgrp -R users "$ANDROID_HOME/cmdline-tools" # mv preserves the old group; restore users
13
14yes | "$SDKMGR" --licenses >/dev/null
15"$SDKMGR" "platform-tools" "platforms;android-36" "build-tools;36.0.0"
16
17"$ANDROID_HOME/platform-tools/adb" --version # confirm the SDK works
Bump android-36 / build-tools;36.0.0 to the current API level as needed. The chgrp is there because mv preserves the source group rather than inheriting the setgid users group โ without it, cmdline-tools would be the odd directory out. The exports repeat step 4 on purpose: ~/.bashrc is skipped in the non-interactive shells an agent runs in, so this step sets the vars itself and calls sdkmanager/adb by full path rather than trusting PATH.
- ๐ค Set sane Gradle defaults: In
~/.gradle/gradle.properties, give the daemon enough heap and turn on the usual speedups. Adjust the heap to your machine’s memory.
1mkdir -p ~/.gradle
2cat > ~/.gradle/gradle.properties <<'EOF'
3org.gradle.jvmargs=-Xmx8g -Dfile.encoding=UTF-8
4org.gradle.daemon=true
5org.gradle.parallel=true
6org.gradle.caching=true
7org.gradle.configuration-cache=true
8EOF
- ๐ค Fix the window-shadow rendering artifact: Out of the box the JetBrains Runtime draws through XWayland under WSLg, which triggers a known compositor bug: the window shadow is left behind as a stale rectangle on resize and monitor-to-monitor moves. Two options clear it โ switch the runtime to its native Wayland toolkit, and disable the JBR shadow that is the actual artifact. The GUI route is Help โ Edit Custom VM Options; the terminal route writes the same file the GUI would, at a version-specific path Studio records in
product-info.json:
1CFG=~/.config/Google/$(grep -o '"dataDirectoryName": *"[^"]*"' \
2 /opt/android/studio/product-info.json | cut -d'"' -f4)
3mkdir -p "$CFG"
4cat > "$CFG/studio64.vmoptions" <<'EOF'
5-Dawt.toolkit.name=WLToolkit
6-Dsun.awt.wl.Shadow=false
7EOF
To tune further, append any of these to the same file (one per line) โ bigger heap for large projects, a HiDPI scale, or a fix for a separate JCEF embedded-browser GPU artifact:
1-Xms2g
2-Xmx8g
3-Dsun.java2d.uiScale=1.5
4-Dide.browser.jcef.gpu.disable=true
- ๐ง Launch Android Studio: Everything it needs already exists, so this is mostly a formality.
1studio
It detects the SDK at ANDROID_HOME and has nothing to download โ no setup-wizard components, no AVD. If a brief wizard appears at all it’s a no-network confirm-and-finish. From inside a project, studio . opens that project (the trailing . is the operative part; bare studio just reopens the welcome screen).
Keep projects on the Linux filesystem
Use a path under ~/workspace/ โ never on /mnt/c/โฆ, and never reached from Windows via \\wsl.localhost\โฆ. Cross-filesystem Gradle is dramatically slower.
Phase 5: Forward a Physical Device with usbipd-win
WSL2 has no native USB stack. The supported path is usbipd-win, which forwards a USB device from Windows into WSL so it appears as a real Linux device โ ADB then talks to the phone directly, with no Windows-side ADB server in between.
- ๐ง Install usbipd-win on Windows: In PowerShell as Administrator.
1winget install --interactive --exact dorssel.usbipd-win
Open a fresh PowerShell afterward, then confirm with usbipd --version and usbipd list.
- ๐ค Install the ADB udev rules in WSL: This is what lets a non-root user claim the device once it is attached.
usbutilscomes along too โ it provideslsusb, which step 5 uses to confirm the passthrough reached the kernel.
1sudo apt install -y android-sdk-platform-tools-common usbutils
2sudo udevadm control --reload-rules
3sudo udevadm trigger
Confirm plugdev membership with groups (you’re usually already a member). If not, add yourself and reopen WSL so the new group takes effect:
1sudo usermod -aG plugdev $USER
- ๐ง Bind the device on Windows (one-time): Plug in the phone and enable Developer Options โ USB Debugging. usbipd can only forward a device Windows recognizes, so the phone’s ADB interface needs a Windows driver โ usually installed on first connect, but the host may lack it here since the SDK lives in WSL. If
bindorattachfails, install one on Windows first (Google USB Driver for Pixel/Nexus, OEM driver otherwise), then find the bus ID and bind. The bind persists across reboots.
1usbipd list
2usbipd bind --busid <BUSID>
- ๐ง Attach to WSL (per session): Forward the bound device into WSL.
1usbipd attach --wsl --busid <BUSID>
- ๐ค Confirm ADB sees it: In WSL, restart the ADB server so it picks up the freshly-attached device.
1export PATH="$PATH:/opt/android/sdk/platform-tools" # ~/.bashrc PATH may not be loaded in an agent shell
2lsusb
3adb kill-server
4adb start-server
5adb devices
Accept the “Allow USB debugging?” prompt on the phone (๐ง โ WSL is a new ADB host from the phone’s perspective, so it re-prompts). The device should now show as device in adb devices.
- ๐ง Auto-attach (optional): To skip the per-session attach, run a persistent PowerShell window that reattaches whenever the device reappears.
1usbipd attach --wsl --busid <BUSID> --auto-attach
Notes:
usbipdis a Windows command โ run it from PowerShell, not the WSL shell. (The Linuxlinux-tools-commonpackage ships an unrelatedusbip; do not install it by mistake.)adb devicesreporting “no permissions (missing udev rules?)” means the udev rules did not load โ re-run the commands in step 2 and re-attach.- Attachments do not survive a WSL restart; re-run the
usbipd attachcommand afterwsl --shutdown.
Phase 6: GitHub CLI for Pull Requests
Optional โ needed only if you want Claude Code (or you) to open pull requests and manage issues from the CLI.
With the GitHub CLI authenticated, Claude Code can open pull requests, check CI status, and manage issues on your behalf โ it detects gh on the PATH automatically.
- ๐ค Install from the official apt repository: Run this block in WSL to add GitHub’s repository and install
gh.
1(type -p wget >/dev/null || (sudo apt update && sudo apt install wget -y)) \
2 && sudo mkdir -p -m 755 /etc/apt/keyrings \
3 && out=$(mktemp) && wget -nv -O$out https://cli.github.com/packages/githubcli-archive-keyring.gpg \
4 && cat $out | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null \
5 && sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \
6 && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \
7 && sudo apt update \
8 && sudo apt install gh -y
Don't install the Snap build of gh
The Snap gh runs in a sandbox that blocks access to your SSH keys and .gitconfig โ use the apt repository above instead.
- ๐ง Authenticate with GitHub: Run
gh auth loginand choose GitHub.com โ SSH โ No (do not generate a new key) โ Login with a web browser. Enter the one-time code in the browser, then verify.
1gh auth login
2gh auth status
Phase 7: Claude Code in the Desktop App over Localhost SSH
Optional โ a GUI convenience over the Phase 3 CLI, not new capability. If you’re happy in the terminal, the CLI already does everything here; skip the phase entirely.
This phase is human-driven, pointedly so
Every step here is ๐ง. It edits Windows files, runs PowerShell, and calls wsl --shutdown โ which terminates the very WSL instance Claude Code runs in. An agent that “helpfully finishes” this phase would unplug itself mid-sentence. Drive it yourself.
The Claude desktop app (from claude.com/download) bundles Claude Code in its Code tab and adds a GUI on top of the CLI โ visual diff review, parallel sessions, and drag-and-drop attachments. The Code tab offers Local, Remote, and SSH environments. On Windows the Local environment runs against the Windows filesystem, so to drive a WSL2 project from the GUI you use the SSH environment pointed at your own WSL instance over localhost โ technically a remote connection that happens to terminate on the same machine.
One thing to know before you start: the desktop app’s SSH connection uses a bundled Node ssh2 client that does not honor the Windows SSH agent, so it authenticates against a real private key file on disk rather than through an agent. That key has no passphrase by design โ it only ever authorizes localhost-to-localhost SSH on your own machine, so it adds no remote attack surface. The file ACL is the only thing protecting it, which is why Step 5 locks it down.
Switch WSL to mirrored networking: This is what lets the Windows side reach WSL services on
localhost. Add the following to%USERPROFILE%\.wslconfig(merge with any existing[wsl2]block), then restart WSL.Mirrored mode is a global WSL change
It affects every distro and some Docker
--network hostworkflows, not just this one.
1[wsl2]
2networkingMode = mirrored
1wsl --shutdown
Reopen Ubuntu and confirm eth0 now shares the Windows host IP rather than a 172.x NAT address:
1ip -4 addr show eth0 | grep inet
- Free up port 22 on Windows: WSL’s
sshdneeds the port. If the Windows OpenSSH Server is running, stop and disable it.
1Get-Service sshd -ErrorAction SilentlyContinue
2Stop-Service sshd
3Set-Service sshd -StartupType Disabled
- Install and harden sshd in WSL: Install the server, then pin it to
localhostonly and disable password auth via a config drop-in.
1sudo apt update && sudo apt install -y openssh-server
2sudo tee /etc/ssh/sshd_config.d/99-claude-code.conf > /dev/null <<'EOF'
3ListenAddress 127.0.0.1
4Port 22
5PermitRootLogin no
6PasswordAuthentication no
7PubkeyAuthentication yes
8KbdInteractiveAuthentication no
9EOF
10sudo systemctl enable --now ssh
The 127.0.0.1 bind matters
Under mirrored networking the default 0.0.0.0 bind would expose this dev sshd to your whole LAN.
Confirm the listener:
1ss -tlnp | grep :22
- Generate a dedicated key on Windows: In PowerShell โ the key lives on the Windows side where the app runs. Leave the passphrase empty (rationale above).
1ssh-keygen -t ed25519 -C "claude-code-wsl" -f "$env:USERPROFILE\.ssh\claude_code_wsl_ed25519"
- Lock down the key and back it up: Because the key has no passphrase, the file ACL is the only barrier. Restrict it to your user, and keep a copy in your password manager as the authoritative source.
1$keyPath = "$env:USERPROFILE\.ssh\claude_code_wsl_ed25519"
2icacls $keyPath /inheritance:r
3icacls $keyPath /grant:r "${env:USERNAME}:F"
- Authorize the public key in WSL: Append the
.pubcontents toauthorized_keysas your normal user, not root โ a common mistake is adding it to/root/.ssh.
1mkdir -p ~/.ssh && chmod 700 ~/.ssh
2echo "PASTE_PUBLIC_KEY_HERE" >> ~/.ssh/authorized_keys
3chmod 600 ~/.ssh/authorized_keys
- Pre-populate known_hosts for
localhost: The desktop app has no trust-on-first-use prompt โ it rejects any host not already in~/.ssh/known_hosts. Scan the IPv4 address and mirror the entries to thelocalhosthostname the app will use.
1ssh-keyscan -4 -t ed25519,ecdsa 127.0.0.1 2>&1 | Add-Content "$env:USERPROFILE\.ssh\known_hosts"
2$kh = "$env:USERPROFILE\.ssh\known_hosts"
3Get-Content $kh | Where-Object { $_ -match '^127\.0\.0\.1\s' } | Select-Object -Unique |
4 ForEach-Object { $_ -replace '^127\.0\.0\.1\b', 'localhost' } | Add-Content -Path $kh
- Smoke-test from PowerShell first: Always confirm passwordless SSH from the standard client before pointing the app at it โ failures are far easier to debug here. You should land in a WSL shell with no password prompt.
1ssh -i "$env:USERPROFILE\.ssh\claude_code_wsl_ed25519" -o IdentitiesOnly=yes <wsl-username>@localhost
- Add the SSH connection in the desktop app: Open the Code tab, choose the SSH environment, and add a connection:
- SSH Host:
<wsl-username>@localhost - SSH Port:
22 - Identity File: the full Windows path to the private key (e.g.
C:\Users\<you>\.ssh\claude_code_wsl_ed25519) โ~/is not expanded here.
On first connect, the app installs its remote server into ~/.claude/remote/ inside WSL; later connects reuse it. Pick a project folder under your WSL home and the GUI is now working against the Linux filesystem.
If the connection fails:
privateKey value does not contain a (valid) private keyโ the key file is on a single line; thessh2library needs newline-wrapped PEM. Reformat it into standard-----BEGIN/END OPENSSH PRIVATE KEY-----blocks with ~70-character body lines.- Host denied / verification failed โ the hostname in the SSH Host field must exactly match a
known_hostsentry (localhostvs127.0.0.1). Restart the app fully after editingknown_hosts; it caches connection state. Permission denied (publickey)in the smoke-test โ the public key landed in the wrongauthorized_keys(often root’s). Confirm it is in your user’s file.
Wrapping up
That’s the whole stack: Ubuntu under WSL2 with systemd, Android Studio drawn natively through WSLg, the SDK provisioned headlessly so there’s no wizard to fight, your physical phone forwarded over USB so adb and the IDE talk to it directly, and โ if you took the optional path โ Claude Code driving the WSL-side of it all while you covered the Windows and physical bits.
From here, drop a project under ~/workspace/, run studio ., and build. The only ceremony you’ll repeat after a reboot is a single usbipd attach to re-forward the phone (Phase 5) โ everything else persists. And if something goes quiet, the per-phase verification commands are the place to start: each one tells you which half of the bridge, Windows or WSL, has dropped the connection.
