VS Code Remote-SSH#
podbench’s transport is a complete, ordinary OpenSSH connection whose only
carrier is kubectl exec. Remote-SSH does not know or care: it sees a host in
your ssh config and connects. This page is the client setup, and the numbers you
need to size a pod before you attach.
Note
Commands here are written podbench <verb> — the only spelling there is. If you
have not installed the launcher, run each as uvx podbench <verb>. See
Setup.
Warning
A real VS Code GUI client has now connected — and the numbers still have not
been taken. On 2026-08-17 a Remote-SSH client reached a seat, started an
extension host, unpacked ms-vscode.cpptools and drove gdb through the C++
adapter into a live IOC. The transport was verified at the protocol level
besides — a real vscode-server completed an HTTP 200 plus a WebSocket
101 Switching Protocols handshake through ssh -L, with no port-forward and
no pod IP — and the server was driven headlessly. Every
memory figure below is therefore a lower bound: no extension host and no
language server has been measured. Treat this page as the best available
guidance, not as a proven result.
Warning
In Observe mode, a breakpoint on a probed pod is on a timer. Sitting on a
breakpoint stops the app answering its probes, and the kubelet cannot tell that
from a hang: the readiness budget takes the pod out of its Service quietly, and
the liveness budget restarts the container and kills the seat with it — an
ephemeral container cannot be restarted, so the session and the seat’s name both
go. podbench attach prints both deadlines for the pod you name, computed from
its spec; on the demo Deployment in tests/e2e/apps/ they are 11–16 s and
21–31 s.
VS Code’s own tools are the way to stay inside them: logpoints (right-click the gutter → Add Logpoint) print and carry on without stopping the process, and a conditional breakpoint stops only on the iteration you care about. For an unlimited pause, debug in a dev pod (Iterate on Python), which has no probes by construction. Debug with gdb has the arithmetic and the measurements.
Size the pod first#
Disk, not memory, is the binding constraint. Measured:
amd64 |
arm64 (RK3588) |
|
|---|---|---|
Server tarball |
213.6 MiB |
205.4 MiB |
Download |
2.17 s |
2.26 s |
Extract |
5.62 s |
5.56 s |
Extracted server on disk |
680.8 MiB |
638.3 MiB |
Server idle RSS |
~97 MiB |
~92 MiB |
|
330 MiB |
261 MiB |
cpptools install |
8.35 s |
8.07 s |
Cold bootstrap over ssh |
5.76 s |
~10 s |
~/.vscode-server reached 995 MiB with exactly one extension, and 2.2 GB
with two server versions and six extensions. data/data/CachedExtensionVSIXs is
another 190 MiB after six extensions (safe to delete once they are installed).
Plan for 1.1–1.3 GB of node disk per Observe-mode session, and ~1.5 GB if you want headroom. The design brief’s “~1 GB” budget is exceeded by the stock server alone.
arm64 is not slower here: the RK3588 extracted 646 MiB in 5.56 s and downloaded 205 MiB in 2.26 s, statistically identical to the x86 NUC. That old claim is about image pulls.
Which pod that lands in decides how much it matters:
Iterate mode (
podbench dev) — the sidecar has its own memory and ephemeral-storage requests and a workspace volume. Ask for what you need.Observe mode (
podbench attach,podbench vscode) — every byte competes with the live workload’s limits. Exceeding memory OOM-kills something in the pod cgroup; exceeding ephemeral storage evicts the whole pod.podbench vscoderaises the target’s memory limit in place for you when the headroom is short; read the caveats on Attach to a pod before you rely on it. Ephemeral storage cannot be raised in place at all — that one needs apodbench-homevolume in the chart.
Declaring the volume#
spec.volumes is immutable, so this is a chart change and not something attach
can do:
spec:
securityContext:
fsGroup: 1000 # without this the volume arrives root-owned
volumes: # and unwritable, and the seat can chown nothing
- name: podbench-home
emptyDir: {} # or a claim, to survive a restart
No volumeMount on the application container: only the seat mounts it. One
caveat (#42): a root seat takes $HOME from its passwd record and ignores
the volume, so the storage is bought with --max-rung degraded — which is also
what gives up the live attach.
Client setup#
Install the Remote - SSH extension (
ms-vscode-remote.remote-ssh).Make sure ssh can see podbench’s generated stanzas. podbench writes one file per pod into
~/.podbench/config.d/and never edits~/.ssh/config, so add the include once, above anyHost *block:Include ~/.podbench/config.d/*.conf
podbench doctorchecks that line is there and in that position;podbench doctor --fixadds it.Land a seat and note the alias it prints. Both modes write the same kind of stanza to the same place, and both print the alias on the last line:
# Iterate mode — a dev pod whose sidecar is the seat podbench dev api-5f6c9b7d8-qz4tn -n demo --port 8080 # Observe mode — a seat beside a live workload podbench attach pod/api-5f6c9b7d8-qz4tn -n demo
Both take
--identity(which key is authorised in the container, default~/.ssh/id_ed25519),--config-dirand--host-alias.Remote-SSH: Connect to Host…, pick the alias, and wait out the first connect while the server downloads.
Or let podbench vscode do all four#
One verb lands the seat, sizes the pod, makes the target debuggable and opens the window:
podbench vscode pod/api-5f6c9b7d8-qz4tn -n demo
That is the whole command for the common case. It is a separate verb rather
than a flag on attach because two of those steps change the workload, and
attach’s contract is that it does not: choosing this verb is asking for an
editor and for everything an editor costs.
It proves the alias first — one ssh <alias> true, before anything is
written or downloaded — and if that does not reach the seat, it prints ssh’s own
words and stops rather than opening a window that will fail. This is the one
thing VS Code cannot be asked: code --remote returns as soon as a window has
the argv, so the connection happens in the GUI afterwards, and a
--install-extension that never connected still exits 0. The successful probe
also leaves a ControlMaster behind, so the window’s own connect is the fast
one.
It writes .vscode/settings.json, .vscode/launch.json and
.vscode/extensions.json into the folder it is about to open, installs only the
extensions this target’s debugger needs in the remote window, and opens the
seat’s home. Those are the two steps most easily got wrong by hand, and both
fail quietly: the wrong folder can end the seat, and a locally installed
extension runs the debug adapter on your laptop. See
the CLI reference for the order and the refusals.
It sizes the pod. vscode-server measured 1215 MiB live with one extension,
and the headroom that decides is read on every attach anyway — so where this pod
has less, the target’s memory limit is raised by the shortfall before the seat
lands, rounded up to the next whole GiB, and the number and the reading are both
printed. --resize MEMORY chooses the number yourself; --no-resize declines
the raise and keeps the warning. Read Attach to a pod on
what an in-place resize costs — chiefly that it lives on the pod and not on its
controller, so the next rollout takes it away.
It provisions the target when the target says it needs it — see the next
section. With one exception: in a dev pod it provisions nothing and says so.
Iterate mode launches the application from the seat, so debugpy is already
where the launch configuration needs it and the workload container has been
idled to sleep — injecting into that would succeed against sleep and report
a debugger nobody can reach.
It uses the seat that is already there, whichever of the three modes made
it. A pod you have already attached is reconnected to; a dev pod is
reconnected to through its podbench sidecar rather than by landing an ephemeral
seat beside it, which is what used to happen and cost a permanent container name
for a strictly worse view — in a dev pod the application runs as a child of the
sidecar, so a seat in the idled workload container sees nothing. The reconnect
says which mode the seat is, because that decides what the debugger is looking
at. --new still lands an Observe-mode seat, which is worth the name only where
the sidecar is non-root and the cluster admits SYS_PTRACE.
It names the other two modes once, on the run that landed a seat where there was none, and asks nothing:
other modes are their own verbs: `podbench hotfix init` for a venv on a
claim that survives restarts, `podbench dev` for a clone the application
relaunches from. Both change the workload in ways this verb was given no
arguments for, so neither is offered as a choice here.
Said rather than asked, because with no seat in the pod there is nothing
ambiguous to resolve: attach is the only one of the three this verb could
carry out, and the other two answers would both have been go and run a
different command. A reconnect does not print it — the mode was settled
whenever the seat was landed, and the KIND column reports it.
It needs code on your PATH — VS Code’s Command Palette has Shell Command:
Install ‘code’ command in PATH — and the local Remote - SSH extension,
without which --remote cannot resolve anything. It drives code only;
cursor, codium and windsurf take the same flags but have not been tried,
and a flatpak VS Code cannot put code on the host PATH at all.
podbench attach is still there and unchanged, for a seat with no editor in it.
What it prints, and where the detail went#
Two blocks: a checklist of what it did, and a short list of what you might do next.
editor
[ok] ssh reaches the seat, so Remote-SSH will too
[ok] wrote settings.json, launch.json, extensions.json in /root/.vscode
[ok] installing ms-python.python, ms-python.debugpy in the seat; the first
bootstraps vscode-server, so this is a download (1215 MiB measured, on
the workload's ephemeral-storage budget in Observe mode)
[ok] ms-python.python, ms-python.debugpy unpacked in the seat
[warn] a window already connected to this seat needs Command Palette ->
Developer: Reload Window, or the debug adapter stays unregistered.
[ok] asked VS Code to open /root over Remote-SSH
next
ssh config written to ~/.podbench/config.d/demo-api-5f6c9b7d8-qz4tn-1.conf
add this to ~/.ssh/config once: Include ~/.podbench/config.d/*.conf
or let podbench check and add it: podbench doctor --fix
reconnect later with: ssh podbench-demo-api-5f6c9b7d8-qz4tn-1
if the window says 'could not establish connection', the local VS Code has
no Remote-SSH extension (ms-vscode-remote.remote-ssh); ssh itself reached
the seat a moment ago with the same config.
[ok] is a step that happened, [warn] one that wants something from you, and
[FAIL] one that did not happen at all. Every step is one line, and the
mechanism behind each of them is on this page rather than in the terminal —
this block used to say all of it inline, and the reliably-skipped part of a
report is the part written as prose.
Lines with no tick are the seat’s own stderr, relayed exactly as it
arrived. debug-config is the only thing that can see the target, so its
account of what is missing is the diagnosis; it also carries the injection
command, whose first line ends in a \ that means nothing once anything
follows it, which is why nothing on this side rewraps or reflows it.
next is printed whether or not the editor step succeeded. A run that ends at
“ssh does not reach the seat” still landed a seat, and podbench dbg and
podbench pids reach it over kubectl exec regardless — so the alias, the
Include and the stanza’s path are exactly what that reader needs.
“unpacked in the seat” is a claim about the seat, not about code#
code --remote ssh-remote+<alias> --install-extension exits 0 for “installed”,
for “already installed” and for “never reached the remote” — and it answers
from the laptop’s install list, so an extension you hold locally is reported
already installed and the seat is never contacted, with or without --force.
Measured at Diamond on 2026-08-21, against a seat holding no matching path
anywhere on its filesystem. It fails worst for the people most likely to be
here: anyone who debugs Python already has the Python extension locally.
So podbench asks the seat — ls -1 ~/.vscode-server/extensions, matched by id
prefix, since the directory carries a version and a platform triple — and only
then says unpacked in the seat. Unpacked, not installed, because the
listing proves presence and not that this run put it there.
Whatever is still missing is installed a second time through the seat’s own
code-server, which is the code path the “Install in SSH: <alias>” button
takes; that one goes via the extension service the window is connected to, so
the adapter is live with no reload. It can only run after the window has opened,
because until then there is no server to install through.
If a [warn] says an extension did not land, install it from the Extensions
view of the remote window — and check the button reads Install in SSH:
<alias>, never the plain one. A local install runs the debug adapter on your
laptop, where none of the /proc/<pid>/root paths in launch.json exist, and
the failure reads as a bad configuration (program path is missing or invalid)
rather than as a wrong machine.
A stock Python workload needs debugpy, and this is where it gets it#
podbench vscode does not compute the debug configuration itself: it asks the
seat, and debug-config is the only thing that can see the target. On a Python
app whose image has no debugpy that ask refuses, because the injection
bootstrap runs in the target’s own interpreter and therefore needs debugpy
importable there.
The seat says so in its own words, and names --provision in the refusal — and
it names it for debugpy and for no other flavour, since there is no
--provision for a missing delve. So the answer is already in hand when the
refusal arrives, and the verb acts on it: it installs debugpy into the target
with uv, starts the debugpy server inside the app, and authors the
configuration against it. F5 works when the command finishes.
This used to be a round trip: podbench printed “re-run with --provision”,
asking you to retype a fact it had just measured.
It is still a mutation and it is still refusable. It writes ~15 MB into the workload’s writable layer, on an ephemeral-storage budget the seat shares with the workload and cannot reserve; it needs egress from the pod, since uv downloads from an index; starting the server ptraces the app, which stops answering probes for the few seconds that takes (~3 s measured — compare it against the deadlines the report prints); and a restart of the target container ends the debugging.
podbench vscode pod/api-5f6c9b7d8-qz4tn -n demo --no-provision
declines it, and you get the offer printed where the act would have been: the
excludes, the folder and the alias, and no launch.json.
A target that already has a debugger is never provisioned. The consent the verb carries is spent only where the seat said debugpy is the blocker.
The two halves do not expire together. The server never survives a restart:
it is a live process inside the one that died. The install survives one only
where --provision-dest names a volume mounted into the target — an emptyDir
is pod-scoped and outlives a container. At the default /opt/podbench-debugpy
it does not: that is the container’s own writable layer, which a restart
rebuilds from the image. Either way you are running podbench vscode again,
since without the server there is nothing to connect to.
Baking debugpy.listen() into the app is the durable answer, and the only one
that survives a restart. Provisioning is for the pod that is already
misbehaving.
A bare debug-config in the seat, with no --provision, still only prints
the injection command rather than running it: that really is authoring a
launch.json and nothing more, and ptracing the workload is not something it
may do on its own. The verb relays the seat’s own output either way, so the
command is printed with the rest, along with every mechanism that said no.
Re-running it on a window that is already connected? Reload it#
--install-extension unpacks into the seat’s ~/.vscode-server. A window that
is already connected started its extension host before that, and does not pick
it up: the extension is installed, the debug adapter is not registered, and its
launch.json entry cannot run. Nothing on the remote side says so — the
debugger is simply not there.
The first run is unaffected, because the install finishes before the window
opens. A later run needs Command Palette → Developer: Reload Window only if
it actually put a new extension in the seat — but podbench cannot tell that
from “already installed” (code exits 0 for both), and cannot tell an open
window from a fresh one either, so it prints the reminder whenever an install
succeeded. On the runs where nothing changed, reloading costs a few seconds and
nothing else.
Run it from a terminal on the machine your VS Code runs on. Inside a Remote-SSH
window, a devcontainer or a Codespace, code on the PATH is the remote CLI,
which talks to the window you are already in: it would install the extensions
into that machine instead of into the seat. podbench refuses that code by name
rather than driving it.
Warning
podbench vscode has not been driven against a real VS Code GUI client. The
flags it uses were verified by hand on 2026-08-16; the sequence podbench runs
them in has unit tests and no live proof.
Do not reach an Iterate-mode dev pod with podbench attach. It works, but
it lands a second, ephemeral container inside the dev pod and ignores the
sidecar that is already there — a second copy of the image, a second
vscode-server on the pod’s disk budget, and a container name burnt for the
pod’s lifetime. podbench dev gives its own sidecar the seat.
If Remote-SSH does not offer the alias, it is reading a different config file.
Set remote.SSH.configFile to the file that has the Include, or point it
straight at ~/.podbench/config.d/<namespace>-<pod>-<n>.conf, where <n> is the
seat’s number (a dev sidecar, named exactly podbench, gets no suffix).
From home, over a VPN that only forwards ssh#
podbench needs no port-forward, no pod IP and no Service — the seat is reached
through kubectl exec — so a reachable API server is the whole requirement.
One ssh tunnel supplies it, and k8s/vpn-api-tunnel.sh builds both halves:
$ ./k8s/vpn-api-tunnel.sh you@ws001.example.ac.uk:beamline-claude-you.kubeconfig
==> read you@ws001.example.ac.uk:beamline-claude-you.kubeconfig over ssh (not kept here)
--ssh-host defaults to you@ws001.example.ac.uk, the host it came from
==> you@ws001.example.ac.uk:beamline-claude-you.kubeconfig -> k8s/beamline-claude-you-tunnel.kubeconfig
context claude-you (namespace beamline)
API k8s-api.example.ac.uk:6443 -> 127.0.0.1:6443
TLS verified as k8s-api.example.ac.uk, through the source's own CA
It copies the kubeconfig, points the copy at a local port, and forwards that
port to the API server. The token, the CA and the namespace are carried over
untouched; tls-server-name is what keeps the certificate valid once the
address is 127.0.0.1, so the CA the file already carries stays usable and
there is no reason to reach for insecure-skip-tls-verify.
Name the source scp-style and it never lands here. The kubeconfig is only
ever input to the copy the script writes, and it holds a live bearer token —
so [user@]host:path reads it over ssh into a temporary file that is removed on
exit, rather than leaving a credential on your laptop to go stale. A local path
works exactly as before; the rule for telling them apart is scp’s own, a colon
before the first slash.
That host is also the default --ssh-host, since a machine holding a cluster’s
kubeconfig is usually a machine that can reach the API server it names. Pass
--ssh-host explicitly to exit somewhere else.
Two things about it are worth knowing before you rely on it.
Run podbench on the machine your VS Code runs on. podbench vscode refuses
a code that resolves under /remote-cli/ or /.vscode-server/, which is what
you get from the integrated terminal of a Remote-SSH window or a devcontainer.
--install-extension there installs into the machine you are already on, and
the seat ends up with .vscode files, no extensions and breakpoints that never
bind. The tunnel is what makes running it locally possible.
The generated ProxyCommand does not carry --kubeconfig. It runs
kubectl exec, and it resolves its kubeconfig from the environment of whatever
spawns it. The VS Code that podbench launches inherits your export KUBECONFIG,
so the first session works; a VS Code started later from a desktop icon does not,
and its ProxyCommand reads ~/.kube/config instead. For a setup that survives
that, merge the tunnelled config in and select it by name:
$ KUBECONFIG=~/.kube/config:k8s/beamline-claude-you-tunnel.kubeconfig \
kubectl config view --flatten > ~/.kube/config.new
$ mv ~/.kube/config.new ~/.kube/config
$ uvx podbench vscode <pod> -n beamline --context claude-you
The --context is embedded in the stanza, so it then resolves with no
environment at all.
The tunnel exits from --ssh-host, so that machine’s address is what the API
server sees. Where API access is allow-listed by source IP, it is that address
that has to be allowed and not your VPN one.
Close it with --stop, and pass --config-only if you run the forward yourself
from autossh or a systemd unit.
The generated stanza, and why each line is there#
# Generated by podbench. Regenerated on every attach; do not edit.
# target: demo/web-7d9f8c5b4-x2k9p[podbench-1]
Host podbench-demo-web-7d9f8c5b4-x2k9p-1
HostName web-7d9f8c5b4-x2k9p
User root
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
ProxyCommand kubectl -n demo exec -i web-7d9f8c5b4-x2k9p -c podbench-1 -- /usr/sbin/sshd -i -e -f /etc/podbench/sshd_config -o LogLevel=ERROR
ServerAliveInterval 15
ServerAliveCountMax 3
ControlMaster auto
ControlPath /tmp/podbench-cm/%C-2cbae7bf1f9161c8
ControlPersist 10m
HostKeyAlias podbench-3f2c1a90-7b6d-4e21-9a55-0c1e2f3a4b5c-podbench-1
UserKnownHostsFile ~/.podbench/known_hosts
StrictHostKeyChecking yes
Note what the ProxyCommand names: kubectl, not podbench. The launcher’s
whole job is to land the seat and write this file, so the seat outlives the
process that created it. Land one with uvx podbench attach, which installs
nothing, and Remote-SSH keeps connecting for as long as the pod lives — with the
launcher no longer on the machine at all.
Do not hand-edit it — it is regenerated on every attach, and three of those lines are load-bearing in ways that fail silently:
Line |
If you change it |
|---|---|
|
|
|
keeps sshd’s stderr byte-free without closing it, which is what satisfies both constraints at once. Anything chattier lands on the ssh client’s stderr, which Remote-SSH parses |
no |
from a script kubectl silently degrades to non-tty and appears to work; with a real TTY forced onto the ProxyCommand the ssh client hangs indefinitely |
|
|
|
a stalled transport — what an apiserver or konnectivity hiccup looks like — hangs ssh forever without them, and fails in 19 s with them. A hard kill or pod deletion is detected instantly either way |
|
podbench manages its own |
Transport budget, for reference: ~10–11 MB RSS per live session, 26 MB/s pod→client, 13 MB/s client→pod, 0 failures in 30 connect/disconnect cycles.
The stanza above is an Observe-mode one. A dev pod’s differs in exactly two places, both derived from what the sidecar actually is rather than assumed:
User podbench
ProxyCommand kubectl -n demo exec -i api-…-podbench -c podbench -- /usr/sbin/sshd -i -e -f /workspace/.podbench/sshd_config -o LogLevel=ERROR
User is the login name the sidecar reports for the uid it runs as — root
for a plain dev pod, and whatever the podbench-identity passwd record names
(podbench) where the origin declares that volume. The sshd config path
follows the same rule the agent uses inside the container: a non-root seat
keeps its files under $HOME, which for the sidecar is /workspace; a root
seat keeps them in /etc/podbench and /root whatever $HOME says.
podbench dev --delete removes the stanza and its known_hosts entry along
with the pod. An attach seat’s stanza is left in place instead, because that
seat is reconnectable for as long as its pod lives.
First connect#
On first connect Remote-SSH downloads a server build matching your client’s
exact commit and extracts it into the container. The version check is a hard
handshake rejection — {"type":"error","reason":"Client refused: version mismatch"} — with no negotiation and no minor-version tolerance, which is why
podbench does not bake a server into the image: a baked server would be
correct for about four weeks and would break Insiders and stale clients
immediately.
Practical consequences:
Connecting with two different VS Code versions puts two servers in the container. That is 1.3 GB before extensions.
The container needs egress on first connect. Four host groups, not two:
update.code.visualstudio.com→vscode.download.prss.microsoft.com(the tarball);marketplace.visualstudio.com(the extension gallery);*.vscode-unpkg.net,main.vscode-cdn.net(extension assets);crl.microsoft.com/www.microsoft.com(VSIX signature verification).
An offline bundle must also ship the full
extensionDependenciesclosure — extensions such asms-python.debugpystill reach the marketplace even when installed from a local.vsix. Air-gapped operation is unspiked.After a pod restart or an OOM the ephemeral container’s rootfs is gone and the server re-downloads. That is the documented reconnect path (~6 s), not a malfunction.
Extensions, and staying slim#
Install as few as you can live with — each one is disk in a budget you do not control in Observe mode.
Extension |
For |
Disk |
|---|---|---|
|
C/C++ attach configs, gdb via MI |
330 / 261 MiB |
|
Rust attach configs |
~90 MiB |
|
Python, and debugpy attach |
Pylance alone is a 117 MiB install |
Two reclaims that are known to work, and one caveat:
# after the server extracts
rm -rf ~/.vscode-server/bin/*/extensions/{copilot,copilot-chat,mermaid-markdown-features}
# once extensions are installed
rm -rf ~/.vscode-server/data/data/CachedExtensionVSIXs
The first takes the server from 646 MiB to 428 MiB (−34 %). It was verified
only by “the server still starts and serves /version” — a real GUI client may
want what was deleted, so treat it as a reclaim you can try, not a default.
Also: do not add --enable-remote-auto-shutdown to Remote-SSH’s server
arguments. It kills the server after exactly five minutes idle.
Once you are connected#
Open
/workspacein Iterate mode — that is the checkout, the venv and the sidecar’s own$HOME.An ssh session’s
$HOMEis not/workspace, and this is worth knowing before the server unpacks 700 MB somewhere you did not expect: sshd puts a session in the home the passwd record names, so it is/home/podbenchwhere the origin declares thepodbench-identityandpodbench-homevolumes, and/rooton a plain dev pod. Akubectl execshell is the other answer — it inherits the container’s environment and lands in/workspace. Only the sidecar’s tooling (uv’s caches, toolchains and venvs) is pinned to the workspace volume;~/.vscode-serverfollows the passwd home. Declaring apodbench-homevolume is what keeps that off the container’s writable layer.In Observe mode open the seat’s home —
/root, or/home/podbenchwhere the pod declares apodbench-homevolume — and reach the workload’s filesystem through/proc/<pid>/rootfrom there.podbench pidstells you which pid.Do not open
/. Opening a file under/procis fine; opening a folder at/points the file watcher and the search indexer at/proc, where every/proc/<pid>/rootis a symlink into another container’s rootfs and the walk has no bottom. A seat cannot reserve memory of its own, and an OOM-killed ephemeral container cannot be restarted — the seat is gone and its name is burnt for the pod’s lifetime. The seat ships the settings that make this survivable (below), which is a second line of defence and not a reason to try it..vscode/launch.jsonlives in the remote window; every path in it is a path in the debug container. Templates for gdb, CodeLLDB and debugpy are in Debug with gdb and Iterate on Python.Terminals are ordinary ssh sessions with the container’s
PATH, so every in-pod verb is there aspodbench <verb>—podbench pids,podbench dbg,podbench capreport,podbench debug-config,podbench dev-bootstrap,podbench run,podbench stop.
What the seat configures for you#
podbench agent writes VS Code’s machine-level settings into the seat as
part of the same idempotent start-up that writes the host key and the authorized
keys, at ~/.vscode-server/data/Machine/settings.json — where ~ is the home
the passwd record names, so on a podbench-home volume they persist across
re-attaches. Machine scope is the only scope that applies to every folder you
open without you having configured anything, which matters because the folder
that kills a seat is the first one.
Setting |
Why |
|---|---|
|
the walk with no bottom, above. |
the same three for |
the seat’s own home is a folder you are told to open, and |
|
ripgrep is given |
|
Pylance walks separately from search, and spells its excludes as a list of absolute globs |
files.exclude is deliberately not set: that would hide /proc from the
explorer, and reading the workload’s files through /proc/<pid>/root is what
Observe mode is for.
podbench vscode writes all of those a second time, into the
.vscode/settings.json of the folder it opens. It opens a single folder, so
that file is VS Code’s workspace settings, where window- and resource-scoped
keys are both honoured — including C_Cpp.files.exclude, which is the only one
that stops cpptools’ tag parser, and cpptools is what it installs for a
C/C++ target. The folder copy matters because it is the one podbench fully
controls: ~/.vscode-server belongs to the client, and Kill/Uninstall VS Code
Server on Host takes the machine file with it. Inside a home the entry that
earns its place first is **/.vscode-server/** — 700 MiB before a single
extension, in the folder podbench is about to open.
The /proc and /sys entries in that copy are belt-and-braces rather than the
working guard. search.exclude patterns are matched workspace-relative, so
from a folder at $HOME its **/proc/** cannot match anything;
files.watcherExclude and python.analysis.exclude are matched against
absolute paths and do work from either scope. Machine scope is what covers the
folder you open next — including the / that starts the walk with no bottom —
which is why both copies exist.
Settings you have written yourself are never overwritten. The agent adds only
the keys that are missing, so a deliberate "**/proc/**": false survives, and a
file it cannot parse — VS Code allows comments in settings.json, json does
not — is left exactly as it is, with the reason reported by
podbench agent --self-check and in the container’s start-up output.
The one thing that removes them is Remote-SSH’s Kill/Uninstall VS Code Server
on Host, which deletes ~/.vscode-server wholesale. Nothing rewrites the file
until the agent next starts, so re-attach (podbench attach --new) or re-create
the dev pod before opening a folder again.
When it goes wrong#
Symptom |
Likely cause |
|---|---|
Remote-SSH cannot find the host |
the |
“Could not establish connection”, |
the ProxyCommand was edited; |
connection hangs with no output |
keepalives removed, or a genuinely stalled apiserver path |
|
the ProxyCommand names sshd’s config where the seat’s agent did not write it. The seat’s uid decides that path — root keeps it in |
server download stalls |
the container has no egress to the four host groups above |
session dies and the workload restarts |
the pod hit its memory limit. This is the Observe-mode footgun; an OOM inside an ephemeral container is unrecoverable |
session dies and the workload restarts while you were stopped at a breakpoint |
the liveness budget expired — same symptom, different cause. |
the app stops answering through the Service while you are stopped, and is fine again after you continue |
the readiness budget expired. Nothing is broken and nothing restarted; the pod left the Service’s routing and re-joined |
everything is gone after a reconnect |
the container restarted, or the pod did. Fresh rootfs, fresh host key. Re-attach (Observe) or make the dev pod again and re-bootstrap (Iterate) |
|
your ssh agent holds that key, so ssh asked the agent to sign and it refused — nothing in the pod is involved. |
|
the key is authorised from the sidecar’s environment, which is fixed when the pod is created — so a dev pod made with a different |
Behaviour through konnectivity or an API gateway is unknown — every measurement here comes from a flat k3s exec path — as is Remote-SSH’s own reconnect behaviour when the pod is deleted mid-session.