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 sudo password, 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:

  1. Install WSL and Ubuntu โ€” the base system.
  2. Add Ubuntu to Windows Terminal (optional) โ€” a nicer console than the default.
  3. Install Claude Code (optional) โ€” the agent you hand Phases 4โ€“7 to.
  4. Install Android Studio + SDK โ€” the IDE, plus a command-line-provisioned SDK (no GUI wizard).
  5. Forward a physical device โ€” USB passthrough so adb sees your phone.
  6. GitHub CLI (optional) โ€” lets Claude Code open PRs and manage issues.
  7. 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.

  1. Windows 10 or 11 (64-bit) with hardware virtualization available โ€” WSL2 runs on a lightweight utility VM.
  2. Administrator access โ€” several install steps run from an elevated PowerShell.
  3. A physical Android device with USB debugging enabled, needed only for the device-passthrough phase.
  4. 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.

  1. 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
  1. Update the distro: Bring the base image current before installing anything else.
1sudo apt update && sudo apt upgrade -y
  1. Confirm you are on WSL2: Ubuntu should report VERSION 2. If it shows 1, convert it.
1wsl --list --verbose
2wsl --set-version Ubuntu 2
  1. 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.

  1. Open Windows Terminal settings with Ctrl+,, then Add a new profile โ†’ New empty profile.
  2. Set the profile fields:
  • Name: Ubuntu
  • Command line: wsl.exe -d Ubuntu
  • Starting directory: \\wsl.localhost\Ubuntu\home\<your-username>
  1. 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.

  1. Install the CLI: The installer drops the claude binary at ~/.local/bin/claude and 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
  1. Authenticate and verify: Run claude from 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.

  1. ๐Ÿค– Install the X client libraries and unzip: Studio needs the X client libs to draw through WSLg; unzip unpacks 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
  1. ๐Ÿค– Create an owned /opt/android tree: The IDE and SDK live side by side here, and because you own it, nothing below needs sudo and Android Studio can self-update without root. The setgid bit makes everything created inside inherit the users group.
1sudo mkdir -p /opt/android
2sudo chown "$USER:users" /opt/android
3sudo chmod 2775 /opt/android   # setgid: studio/ and sdk/ inherit the users group
  1. ๐Ÿค– Download and extract Android Studio: Grab the Linux .tar.gz URL 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 โ€” no sudo needed.
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).

  1. ๐Ÿค– Set the environment and put everything on PATH: Append one block to ~/.bashrc. JAVA_HOME points at Android Studio’s bundled JBR โ€” which is also the JDK we’ll run sdkmanager under, so there’s no separate JDK to install. ANDROID_HOME is 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.”

  1. ๐Ÿค– 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/latest path sdkmanager expects, 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.

  1. ๐Ÿค– 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
  1. ๐Ÿค– 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
  1. ๐Ÿง‘ 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).

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.

  1. ๐Ÿง‘ 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.

  1. ๐Ÿค– Install the ADB udev rules in WSL: This is what lets a non-root user claim the device once it is attached. usbutils comes along too โ€” it provides lsusb, 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
  1. ๐Ÿง‘ 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 bind or attach fails, 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>
  1. ๐Ÿง‘ Attach to WSL (per session): Forward the bound device into WSL.
1usbipd attach --wsl --busid <BUSID>
  1. ๐Ÿค– 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.

  1. ๐Ÿง‘ 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:

  • usbipd is a Windows command โ€” run it from PowerShell, not the WSL shell. (The Linux linux-tools-common package ships an unrelated usbip; do not install it by mistake.)
  • adb devices reporting “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 attach command after wsl --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.

  1. ๐Ÿค– 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
  1. ๐Ÿง‘ Authenticate with GitHub: Run gh auth login and 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.

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.

  1. 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.

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
  1. Free up port 22 on Windows: WSL’s sshd needs 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
  1. Install and harden sshd in WSL: Install the server, then pin it to localhost only 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

Confirm the listener:

1ss -tlnp | grep :22
  1. 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"
  1. 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"
  1. Authorize the public key in WSL: Append the .pub contents to authorized_keys as 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
  1. 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 the localhost hostname 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
  1. 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
  1. 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; the ssh2 library 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_hosts entry (localhost vs 127.0.0.1). Restart the app fully after editing known_hosts; it caches connection state.
  • Permission denied (publickey) in the smoke-test โ€” the public key landed in the wrong authorized_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.