Skip to content

Using VS Code Remote Tunnels for Headless Remote Development

VS Code

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:

code --version

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:

code --version

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:

code --version

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:

code tunnel user login --provider github
code tunnel user login --provider github
code tunnel user login --provider github

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:

code tunnel user show
code tunnel user show
code tunnel user show

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:

code tunnel service install --name artemis-i

Check the service through the VS Code CLI:

code tunnel service log

Reboot once and confirm the tunnel comes back:

sudo reboot

Install the tunnel service and give the machine a stable tunnel name:

code tunnel service install --name artemis-ii

Check the service status:

systemctl --user status code-tunnel

If your server uses lingering user services so they start before an interactive login, enable lingering for your account:

sudo loginctl enable-linger "$USER"

Reboot once and confirm the tunnel comes back:

sudo reboot

Run PowerShell as the account that should own the tunnel, then install the service:

code tunnel service install --name artemis-iii

Check the Windows Service:

Get-Service | Where-Object Name -Like "*tunnel*"

You can also inspect it through Services if you prefer the graphical console.

Some Useful Service Commands

These commands are worth keeping close:

code tunnel service log
code tunnel service uninstall
code tunnel user logout
code tunnel service log
code tunnel service uninstall
code tunnel user logout
code tunnel service log
code tunnel service uninstall
code tunnel user logout
  • The uninstall command removes the service registration.
  • The logout command 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.

  1. Open VS Code on your laptop.
  2. Sign in with the same GitHub account.
  3. Open the Command Palette.
  4. Run Remote Tunnels: Connect to Tunnel.
  5. Choose a tunnel name.
  6. 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.

  1. Go to https://vscode.dev.
  2. Sign in with GitHub.
  3. Open the Command Palette.
  4. Run Remote Tunnels: Connect to Tunnel.
  5. 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:

  1. Keep the laptop light.
  2. Keep the build machines close to the supported platform.
  3. Authenticate through GitHub.
  4. Let each host make an outbound tunnel.
  5. Run the tunnel as a native service.
  6. 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.