{
    "title": "How to Launch WSL VS Code Projects from the Windows Start Menu",
    "slug": "how-to-launch-wsl-vs-code-projects-from-the-windows-start-menu",
    "excerpt": "A small PowerShell script for Windows that finds named WSL projects and creates Start Menu shortcuts to open them directly in Visual Studio Code using the WSL remote URI.",
    "body": "Following on from my [GNOME](https://blog.philipnewborough.co.uk/posts/gnome-menu-entries-for-visual-studio-code-projects) and [macOS](https://blog.philipnewborough.co.uk/posts/how-to-launch-vs-code-projects-via-macos-spotlight) scripts, this Windows PowerShell version does the same job for Windows. It detects your default WSL distro, scans your projects directory inside WSL for `.projectname` files, and creates Start Menu shortcuts, with optional desktop shortcuts, that open each named WSL project directly in Windows VS Code via the WSL remote URI.\r\n\r\nI do not use Windows very much, but it is nice to know that when I do, I can keep the same easy workflow for opening projects. And, having seen a few reviews of the latest Qualcomm Snapdragon-powered laptops, it does not seem impossible that I might end up using a Windows laptop at some point.\r\n\r\nAnyway, here is the script. Be sure to change the `$ProjectsDirInWsl` value before using it. **Full disclosure:**  I vibecoded most of this with Claude, because unlike Linux shell scripting, PowerShell is not something I have much experience with.\r\n\r\n```\r\n#Requires -Version 5.1\r\nSet-StrictMode -Version Latest\r\n$ErrorActionPreference = \"Stop\"\r\n\r\n# ── Configuration ──────────────────────────────────────────────────────────────\r\n$ProjectsDirInWsl           = \"/home/username/Projects\"\r\n$LauncherFolderName         = \"VS Code WSL Projects\"\r\n$AlsoCreateDesktopShortcuts = $false\r\n\r\n# ── Helpers ────────────────────────────────────────────────────────────────────\r\nfunction Get-DefaultWslDistro {\r\n    # wsl --list outputs UTF-16 LE; temporarily switch console encoding so\r\n    # PowerShell decodes the bytes correctly.\r\n    $prev = [Console]::OutputEncoding\r\n    [Console]::OutputEncoding = [System.Text.Encoding]::Unicode\r\n    try {\r\n        $lines = & wsl.exe --list --verbose 2>&1\r\n    } finally {\r\n        [Console]::OutputEncoding = $prev\r\n    }\r\n\r\n    if (-not $lines) {\r\n        throw \"No output from 'wsl --list --verbose'. Is WSL installed and enabled?\"\r\n    }\r\n\r\n    foreach ($line in $lines) {\r\n        # Strip residual null bytes that can survive the UTF-16 decode, then trim.\r\n        $clean = ($line -replace '\\x00', '').Trim()\r\n\r\n        # The default distro line begins with an asterisk.\r\n        if ($clean -notmatch '^\\*') { continue }\r\n\r\n        # Remove the \"* \" marker, then split on the multi-space column separator\r\n        # to isolate the NAME column. This correctly handles distro names that\r\n        # contain spaces (e.g. \"Ubuntu 22.04\").\r\n        $name = ($clean -replace '^\\*\\s+', '') -split '\\s{2,}' | Select-Object -First 1\r\n        $name = $name.Trim()\r\n        if ($name) { return $name }\r\n    }\r\n\r\n    throw \"No default WSL distro found. Set one with: wsl --set-default <DistroName>\"\r\n}\r\n\r\nfunction Invoke-WslScript {\r\n    # Writes the bash script to a temp file inside WSL and executes it there,\r\n    # avoiding stdin-truncation and CRLF issues that affect PowerShell pipes.\r\n    param(\r\n        [Parameter(Mandatory)][string]$Distro,\r\n        [Parameter(Mandatory)][string]$Script\r\n    )\r\n\r\n    # Normalise to LF so bash doesn't see stray CR characters.\r\n    $lfScript = $Script -replace \"`r`n\", \"`n\" -replace \"`r\", \"`n\"\r\n\r\n    # Write to a Windows temp file with UTF-8 no-BOM encoding.\r\n    $tmpWin = [System.IO.Path]::GetTempFileName()\r\n    $output = $null\r\n    try {\r\n        [System.IO.File]::WriteAllText($tmpWin, $lfScript, (New-Object System.Text.UTF8Encoding $false))\r\n\r\n        # Convert the Windows path to a WSL path.\r\n        $prev = [Console]::OutputEncoding\r\n        [Console]::OutputEncoding = [System.Text.Encoding]::UTF8\r\n        try {\r\n            $tmpWslRaw = & wsl.exe -d $Distro -- wslpath -u ($tmpWin -replace '\\\\', '/')\r\n            if ($LASTEXITCODE -ne 0 -or -not $tmpWslRaw) {\r\n                throw \"wslpath failed (exit $LASTEXITCODE) for: $tmpWin\"\r\n            }\r\n            $tmpWsl = \"$tmpWslRaw\".Trim()\r\n            $output = & wsl.exe -d $Distro -- bash $tmpWsl\r\n        } finally {\r\n            [Console]::OutputEncoding = $prev\r\n        }\r\n    } finally {\r\n        Remove-Item -LiteralPath $tmpWin -Force -ErrorAction SilentlyContinue\r\n    }\r\n\r\n    if ($LASTEXITCODE -ne 0) {\r\n        throw \"WSL bash exited with code $LASTEXITCODE.\"\r\n    }\r\n\r\n    return $output\r\n}\r\n\r\nfunction Get-VsCodeExe {\r\n    $candidates = @(\r\n        \"$env:LOCALAPPDATA\\Programs\\Microsoft VS Code\\Code.exe\",\r\n        \"$env:ProgramFiles\\Microsoft VS Code\\Code.exe\",\r\n        \"${env:ProgramFiles(x86)}\\Microsoft VS Code\\Code.exe\"\r\n    )\r\n\r\n    foreach ($path in $candidates) {\r\n        if ($path -and (Test-Path -LiteralPath $path)) { return $path }\r\n    }\r\n\r\n    $cmd = Get-Command code -ErrorAction SilentlyContinue\r\n    if ($cmd) { return $cmd.Source }\r\n\r\n    throw \"VS Code not found. Please install VS Code for Windows first.\"\r\n}\r\n\r\nfunction Get-SafeWindowsFileName {\r\n    param([Parameter(Mandatory)][string]$Name)\r\n    $safe = ($Name -replace '[<>:\"/\\\\|?*]', '_').Trim()\r\n    if (-not $safe) { throw \"Project name '$Name' resolves to an empty Windows filename.\" }\r\n    return $safe\r\n}\r\n\r\nfunction New-VsCodeShortcut {\r\n    param(\r\n        [Parameter(Mandatory)][string]$LnkPath,\r\n        [Parameter(Mandatory)][string]$CodeExe,\r\n        [Parameter(Mandatory)][string]$FolderUri,\r\n        [Parameter(Mandatory)][string]$Description\r\n    )\r\n    $wsh             = New-Object -ComObject WScript.Shell\r\n    $sc              = $wsh.CreateShortcut($LnkPath)\r\n    $sc.TargetPath       = $CodeExe\r\n    $sc.Arguments        = \"--folder-uri `\"$FolderUri`\"\"\r\n    $sc.WorkingDirectory = $env:USERPROFILE\r\n    $sc.Description      = $Description\r\n    $sc.IconLocation     = \"$CodeExe,0\"\r\n    $sc.Save()\r\n}\r\n\r\n# ── Discover environment ────────────────────────────────────────────────────────\r\nif ($env:OS -ne 'Windows_NT') { throw \"This script must be run on Windows.\" }\r\n\r\n$Distro      = Get-DefaultWslDistro\r\n$CodeExe     = Get-VsCodeExe\r\n$LauncherDir = Join-Path ([Environment]::GetFolderPath('Programs')) $LauncherFolderName\r\n$DesktopDir  = [Environment]::GetFolderPath('Desktop')\r\n\r\nWrite-Host \"WSL distro    : $Distro\"\r\nWrite-Host \"Projects dir  : $ProjectsDirInWsl\"\r\nWrite-Host \"VS Code       : $CodeExe\"\r\nWrite-Host \"Launcher dir  : $LauncherDir\"\r\nWrite-Host \"\"\r\n\r\nif ([string]::IsNullOrWhiteSpace($LauncherDir) -or $LauncherDir.Length -lt 5) {\r\n    throw \"Launcher directory path looks wrong: '$LauncherDir'\"\r\n}\r\n\r\n# ── Find all projects in a single WSL call ─────────────────────────────────────\r\n# Output format per project: <absolute-wsl-dir> TAB <project-name>\r\n# Using .Replace() (not -replace) to avoid regex interpretation of the path.\r\n$discoveryScript = @'\r\nPROJECTS_DIR=\"__PROJECTS_DIR__\"\r\n\r\nif [ ! -d \"$PROJECTS_DIR\" ]; then\r\n    echo \"Directory not found in WSL: $PROJECTS_DIR\" >&2\r\n    exit 1\r\nfi\r\n\r\nfind \"$PROJECTS_DIR\" -type f -name '.projectname' 2>/dev/null | sort |\r\nwhile IFS= read -r f; do\r\n    dir=\"${f%/*}\"\r\n    name=$(head -n1 \"$f\" 2>/dev/null | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')\r\n    [ -n \"$name\" ] && printf '%s\\t%s\\n' \"$dir\" \"$name\"\r\ndone\r\n'@\r\n\r\n$discoveryScript = $discoveryScript.Replace('__PROJECTS_DIR__', $ProjectsDirInWsl)\r\n$projectLines    = @(Invoke-WslScript -Distro $Distro -Script $discoveryScript |\r\n                     Where-Object { $_ -match \"`t\" })\r\n\r\nWrite-Host \"Projects found: $($projectLines.Count)\"\r\nWrite-Host \"\"\r\n\r\n# ── Prepare the Start Menu folder ──────────────────────────────────────────────\r\nif (Test-Path -LiteralPath $LauncherDir) {\r\n    Write-Host \"Clearing existing launchers...\"\r\n    Get-ChildItem -LiteralPath $LauncherDir -Filter '*.lnk' | Remove-Item -Force\r\n} else {\r\n    $null = New-Item -ItemType Directory -Path $LauncherDir\r\n}\r\n\r\n# ── Create shortcuts ───────────────────────────────────────────────────────────\r\n$count = 0\r\n\r\nforeach ($line in $projectLines) {\r\n    $tab = $line.IndexOf(\"`t\")\r\n    if ($tab -lt 1) { continue }\r\n\r\n    $projectDir  = $line.Substring(0, $tab).Trim()\r\n    $projectName = $line.Substring($tab + 1).Trim()\r\n    if (-not $projectDir -or -not $projectName) { continue }\r\n\r\n    $safeName  = Get-SafeWindowsFileName $projectName\r\n    $folderUri = \"vscode-remote://wsl+$Distro$projectDir\"\r\n    $desc      = \"Open $projectName in VS Code (WSL: $Distro)\"\r\n    $lnkPath   = Join-Path $LauncherDir \"$safeName.lnk\"\r\n\r\n    New-VsCodeShortcut -LnkPath $lnkPath -CodeExe $CodeExe -FolderUri $folderUri -Description $desc\r\n    Write-Host \"  + $safeName.lnk\"\r\n    $count++\r\n\r\n    if ($AlsoCreateDesktopShortcuts) {\r\n        New-VsCodeShortcut `\r\n            -LnkPath      (Join-Path $DesktopDir \"$safeName.lnk\") `\r\n            -CodeExe      $CodeExe `\r\n            -FolderUri    $folderUri `\r\n            -Description  $desc\r\n    }\r\n}\r\n\r\nWrite-Host \"\"\r\nWrite-Host \"$count launcher(s) written to: $LauncherDir\"\r\n\r\nif ($AlsoCreateDesktopShortcuts -and $count -gt 0) {\r\n    Write-Host \"Desktop shortcuts also written to: $DesktopDir\"\r\n}\r\n```\r\n\r\n### Bonus: Turn the script into an `.exe`\r\n\r\nIf you prefer, you can wrap the script as a standalone executable and drop it somewhere convenient.\r\n\r\n```\r\nInstall-Module PS2EXE -Scope CurrentUser\r\nInvoke-PS2EXE .\\Generate-VSCodeWSLLaunchers.ps1 .\\Generate-VSCodeWSLLaunchers.exe\r\n```\r\n\r\nAfter that, you can just run the `.exe` directly.",
    "tags": [],
    "published_at": "2026-04-23 20:40:00",
    "url": "https://blog.philipnewborough.co.uk/posts/how-to-launch-wsl-vs-code-projects-from-the-windows-start-menu",
    "featured_image": "https://blog.philipnewborough.co.uk/media/og-ef17fd78-12dd-4379-bda5-1458fdb41771.png"
}