Using VS Code Remote Tunnels for Headless Remote Development
The best development machine isn't always the one under your hands. Laptops are wonderful daily drivers, but they're also thermally constrained, battery-bound, and constantly moving between networks. For serious build and test work, especially on Go projects that need real Linux, Windows, and macOS coverage, I keep the compute somewhere stable and let my laptop act as the control plane.
With VS Code Remote Tunnels, you run the VS Code CLI on a remote machine, authenticate it with your GitHub account, and connect to that machine from VS Code or vscode.dev without opening inbound firewall ports, maintaining a VPN profile, or teaching dynamic DNS yet another way to disappoint you.
Remote development used to mean one of two things: SSH into a server and live in a terminal, or carry around a fragile stack of VPNs, jump boxes, port forwards, editor extensions, and local configuration. That worked, but it made the developer's laptop the place where every concern collided.
Modern remote development is a cleaner architectural split:
- The laptop is the interface. It provides the screen, keyboard, editor chrome, GitHub sign-in, and whatever local ergonomics make you productive.
- The remote node is the execution environment. It owns the CPU, memory, disk, operating system, SDKs, build cache, test dependencies, and long-running processes.
- The identity provider brokers access. With Remote Tunnels, GitHub authentication gives you a familiar access path without requiring a public SSH endpoint.
It lets you treat development environments more like durable infrastructure and less like whatever happened to be installed on the laptop.
For my open source work, the benefits are immediate. The builds are fast, but fast is relative when you're compiling repeatedly, running integration tests, linting large module graphs, or validating cross-platform behavior against platform-specific dependencies. A headless machine with more cores, more memory, a warm module cache, and a stable network turns the edit-build-test loop into something predictable. The laptop stays cool. The battery lasts longer. The build cache stays where the builds happen.
Remote Tunnels is useful balance because they avoid several common failure modes in home lab and small team environments.
| Concern | Traditional Approach | Remote Tunnels Approach |
|---|---|---|
| Inbound Access | Open SSH or RDP, publish a port, or maintain a VPN | No open inbound port required on the host |
| Identity | Local accounts, SSH keys, VPN credentials | GitHub-backed authentication |
| Client Setup | Per-machine networking and editor configuration | VS Code desktop or vscode.dev |
| Mobility | Breaks when networks, IPs, or DNS change | Host makes an outbound tunnel |
| Hardware Use | Laptop does most local build work | Remote node owns compile and test load |
I don't want my development servers listening on the public internet just because I want to connect from anywhere. I also don't want to manage a complex VPN dependency for a workflow that mostly needs editor, terminal, and filesystem access.
Remote Tunnels invert the connection. The host initiates an outbound connection to the VS Code tunnel service. The client then attaches through that service after authentication. That isn't a substitute for all secure remote access patterns, and it doesn't remove the need to patch the host, protect your GitHub account, or think about repository secrets. But it's a very sensible default for developer machines that should be reachable by you and invisible to everyone else.
The Nodes¶
My development setup is intentionally simple in the places where it helps.
The primary project behind my pattern is the Packer Plugin for VMware Desktop Hypervisors, the Go-based plugin that supports VMware Workstation and VMware Fusion hypervisors.
This project is exactly the kind of codebase where the remote development earns its keep. Unit tests are only part of the story. The useful validation happens on the platforms where the desktop hypervisors actually run, with the host operating system, filesystem behavior, process model, and hypervisor installation in the loop.
| Node | Operating System | Hypervisor | Primary Role |
|---|---|---|---|
artemis-i | macOS Tahoe | VMware Fusion | macOS builds and VMware Fusion validation |
artemis-ii | Ubuntu 26.04 | VMware Workstation for Linux | Linux builds and VMware Workstation validation |
artemis-iii | Windows 11 | VMware Workstation for Windows | Windows builds and VMware Workstation validation |
The three machines are remote development nodes for the same project, not three separate hobbies wearing matching name tags. They exist so I can compile, test, and troubleshoot the plugin on the operating systems and VMware desktop hypervisors it supports.
Remote Tunnels make those nodes feel like selectable backends for the same editor. From a travel laptop, I can open VS Code, pick an endpoint and work against the right host platform without dragging that platform around with me.
Prerequisites¶
You need a few things before installing the tunnel service:
- A GitHub account with MFA enabled.
- VS Code on your client machine, or access to
https://vscode.dev. - Outbound HTTPS access from each host.
- Local administrator or service installation rights on the host.
Install the Standalone VS Code CLI on the Hosts¶
For the tunnel service itself, the important piece is the code CLI. You can get that from the standalone VS Code CLI archive, or from a full VS Code desktop installation that places code on your PATH.
The examples below use the standalone CLI archive because it's explicit and works well for service setup. On my Artemis nodes, I also keep the full desktop application installed for the remote desktop cases described above.
Note
The standalone VS Code CLI is enough to run Remote Tunnels. If the machine is truly headless and all you ever need is editor, terminal, and filesystem access, that's the smallest reasonable install.
That's not quite my setup. I install the full VS Code desktop application on the nodes too, because these machines are also desktop hypervisor test hosts. Most days, I connect through a tunnel and let VS Code run the remote editor pieces where the code and toolchains live. But there are times when I want to connect over a remote desktop protocol, such as RDP, and use the desktop directly.
That matters if not every useful test is a clean headless operation. Sometimes I need to see VMware Fusion or VMware Workstation behavior on the host desktop: VM lifecycle, prompts, UI state, guest console behavior, permissions dialogs, or the awkward little edge where automation meets a desktop product. Remote Tunnels handle the coding path. A remote desktop session gives me eyes on the hypervisor when the behavior is visual or interactive.
Download the CLI¶
Download the macOS CLI archive that matches the host architecture:
mkdir -p ~/.local/bin
case "$(uname -m)" in
arm64)
vscode_cli_os="cli-darwin-arm64"
;;
x86_64)
vscode_cli_os="cli-darwin-x64"
;;
*)
echo "Unsupported macOS architecture: $(uname -m)"
exit 1
;;
esac
curl -L "https://code.visualstudio.com/sha/download?build=stable&os=${vscode_cli_os}" \
--output /tmp/vscode-cli.zip
unzip -o /tmp/vscode-cli.zip -d ~/.local/bin
chmod +x ~/.local/bin/code
Then confirm the CLI is available:
Download the Linux CLI archive and place the binary somewhere on PATH:
mkdir -p ~/.local/bin
curl -L "https://code.visualstudio.com/sha/download?build=stable&os=cli-alpine-x64" \
--output /tmp/vscode-cli.tar.gz
tar -xzf /tmp/vscode-cli.tar.gz -C ~/.local/bin
chmod +x ~/.local/bin/code
Then confirm the CLI is available:
Download the Windows CLI archive from PowerShell:
New-Item -ItemType Directory -Force -Path "$env:LOCALAPPDATA\Programs\VSCodeCLI" | Out-Null
Invoke-WebRequest `
-Uri "https://code.visualstudio.com/sha/download?build=stable&os=cli-win32-x64" `
-OutFile "$env:TEMP\vscode-cli.zip"
Expand-Archive `
-Path "$env:TEMP\vscode-cli.zip" `
-DestinationPath "$env:LOCALAPPDATA\Programs\VSCodeCLI" `
-Force
Add that directory to the user PATH if it isn't already there:
$cliPath = "$env:LOCALAPPDATA\Programs\VSCodeCLI"
[Environment]::SetEnvironmentVariable(
"Path",
[Environment]::GetEnvironmentVariable("Path", "User") + ";$cliPath",
"User"
)
Open a new PowerShell session and verify the CLI:
Use the Official Download Endpoint
The code.visualstudio.com/sha/download endpoint tracks the stable VS Code CLI build. That keeps this setup simple on headless machines because you're installing one small CLI archive, not the full desktop editor.
Authenticate the Tunnel with GitHub¶
The tunnel host needs to be associated with your account. I use GitHub because it's already the identity provider behind my repositories, Settings Sync, and most of my development workflow.
Run this on each host:
The CLI prints a device code and a GitHub login URL. Open the URL in a browser, enter the code, and approve the authorization. This is the same device-flow pattern used by many CLIs: the headless host doesn't need a browser, and you don't need to paste a GitHub password into a remote shell.
After authentication, check the tunnel user:
Install the Tunnel as a Background Service¶
Running code tunnel by hand is fine for a quick test, but it isn't how I wanted this development infrastructure to behave. Instead I setup a persistent service that starts at boot and survives logouts.
The VS Code CLI handles the platform-specific service wiring for you. On Ubuntu it configures a systemd service. On macOS it configures a launchd service. On Windows 11 it configures a Windows Service.
Install the Service¶
Install the tunnel service and give the machine a stable tunnel name:
Check the service through the VS Code CLI:
Reboot once and confirm the tunnel comes back:
Install the tunnel service and give the machine a stable tunnel name:
Check the service status:
If your server uses lingering user services so they start before an interactive login, enable lingering for your account:
Reboot once and confirm the tunnel comes back:
Some Useful Service Commands¶
These commands are worth keeping close:
- The
uninstallcommand removes the service registration. - The
logoutcommand removes the account association.
Those are separate operations, which is what you want when rotating credentials, renaming hosts, or rebuilding a node.
Connect from VS Code¶
Once the service is running, the client side is straightforward.
- Open VS Code on your laptop.
- Sign in with the same GitHub account.
- Open the Command Palette.
- Run Remote Tunnels: Connect to Tunnel.
- Choose a tunnel name.
- Open a folder on the remote host.
From there, VS Code behaves like a remote-aware editor. The Explorer shows remote files. The integrated terminal runs on the selected host. Extensions that need to execute near the code install on the remote side. Language servers run where the toolchains and source trees live.
When I'm validating cross-platform behavior, I connect to the host that matches the mode.
Same editor muscle memory, different execution environment.
Connect from vscode.dev¶
The browser path is the part that still feels slightly futuristic, mostly because it's so useful when you need it.
- Go to
https://vscode.dev. - Sign in with GitHub.
- Open the Command Palette.
- Run Remote Tunnels: Connect to Tunnel.
- Pick the remote host.
This isn't my preferred way to spend an entire day writing code. A native VS Code desktop session still feels better for long work. But vscode.dev is a very credible console for development work.
What This Doesn't Replace¶
Remote Tunnels are useful, but they aren't a universal remote access strategy.
Use a VPN, private network overlay, or traditional bastion pattern when you need broad network reachability into an environment, private service access, or strict enterprise routing controls. Use SSH when you need a simple terminal session and already have a well-managed key and network model. Use Remote Tunnels when the main thing you need is a secure editor-centered development path to a machine you control.
Also remember where trust concentrates:
- Your GitHub account becomes part of the access path, so MFA matters.
- The remote host can access whatever credentials and repositories you place on it.
- Extensions running remotely deserve the same scrutiny as any other code running on that machine.
- Repository secrets should stay in a secret manager, not in a convenient file because the machine is "just a dev box."
The tunnel removes a lot of networking friction. It doesn't remove basic operational hygiene.
The pattern is pretty simple:
- Keep the laptop light.
- Keep the build machines close to the supported platform.
- Authenticate through GitHub.
- Let each host make an outbound tunnel.
- Run the tunnel as a native service.
- Connect from wherever you happen to be working.
That gives me a consistent development experience without turning my home lab into a public service. The nodes are always there when I need Linux, Windows, or macOS build and hypervisor validation capacity, but they don't require me to expose SSH or setup a VPN.
For me it's less ceremony and a great outcome.
Open VS Code, pick the right remote node, and get back to the work.