Environment
- VS Code: 1.117.0 (commit 10c8e557c8b9f9ed0a87f61f1c9a44bde731c409, 2026-04-21)
- Dev Containers extension: v0.454.0
- Host OS: Ubuntu 25.10, Linux 6.17.0-22-generic
- Remote: Debian bookworm-slim devcontainer, docker-compose backed
- Docker 29.1.3 / Compose v2.40.2
Summary
When a second VS Code window attaches to a devcontainer while the first window's postCreateCommand is still running, the second window's "Terminating old extension host agent" logic runs kill -9 -$pgrp on every /proc//cmd match of the .vscode-server/bin// regex. In a postCreate that shells out to code --install-extension (a common pattern), that kill takes down the whole script via pgid inheritance and the user sees:
postCreateCommand from devcontainer.json failed with exit code 137.
Skipping any further user-provided commands.
Root cause
dist/extension/extension.js (around the Terminating old extension host agent. string). Rewritten from the minified form:
// 1) Collect every process in the container's mount namespace whose
// cmd path matches the vscode-server/bin regex.
const candidates = procRecords.filter(p =>
p.mntNS === containerMntNS && agentPathRx.test(p.cmd)
);
// 2) Partition by cwd: "current" (cwd is the server install dir) vs "others".
const {current, others} = candidates.reduce((acc, p) => {
const isCurrent =
(p.cwd === canonicalInstallDir ||
p.cwd === serverInstallDir ||
p.cwd === alternateInstallDir)
&& agentPathRx.test(p.cmd);
acc[isCurrent ? 'current' : 'others'].push(p);
return acc;
}, {current: [], others: []});
// 3) SIGKILL the entire PGID of every "other" match.
if (shouldTerminateOthers && others.length) {
output.write('Terminating old extension host agent.\r\n');
try {
await remoteExec(
kill -9 ${others.map(p => p.pgrp ? -${p.pgrp} : p.pid).join(' ')}
);
} catch {}
}
A postCreateCommand that runs code-server --install-extension X execs node /home/dev/.vscode-server/bin//out/server-main.js --install-extension X.
That node process:
- matches agentPathRx (it is literally a vscode-server/bin/*/node),
- has cwd = /workdir (the script's cwd), not the server install dir → falls in others,
- inherits the script's pgid.
kill -9 - therefore hits the postCreate shell, not just the stale agent the scan was targeting.
Steps to reproduce
- Devcontainer with a postCreateCommand that iterates code --install-extension for a handful of extensions (a natural auto-provision pattern).
- Two windows end up attached to the same devcontainer during the postCreate window. We do not open two ourselves — installing eamodio.gitlens during the postCreate reliably triggers a second window via its install-time walkthrough/URI dispatch.
window.restoreWindows=all, a user-opened second window, or any other vscode://-URI-dispatching extension should produce the same effect.
- postCreateCommand failed with exit code 137 appears in the Dev Containers output pane within milliseconds of the second window's "Terminating old extension host agent." log line.
Evidence
From a reproduced session (only clock-relevant lines):
[window2] 13:00:57.402 scan /proc (pid/cwd/mnt/stat)
[window2] 13:00:57.671 Terminating old extension host agent.
[window2] 13:00:57.680 kill -9 -381
[window2] 13:00:57.688 (8 ms) kill -9 -381
[window1] 13:00:57.689 Stop (41332 ms): /bin/sh -c /workdir/.devcontainer/install-extensions.sh -postcreate
[window1] 13:00:57.689 postCreateCommand from devcontainer.json failed with exit code 137.
Expected behavior
The "old agent" scan should not kill transient node CLI subcommands (--install-extension, --list-extensions, --uninstall-extension, --version), and
should not take down process groups it didn't spawn.
In order of increasing robustness, any of:
- Filter by argv. Before classifying a vscode-server/bin/*/node process as an agent, inspect its argv. Only --start-server (or the equivalent
long-running mode) is an actual agent. Bare --install-extension/--list-extensions/etc. are one-shot CLIs and should be ignored by the scan.
- Kill by pid, not pgid. kill -9 p.pid (drop the - for pgid) targets only the matched process rather than every sibling that happens to share a pgid.
- Track agents by spawn-time PID (or cgroup / sessionId marker) rather than by /proc scan. Only kill what this extension recognizes it previously spawned.
(1) is the smallest safe fix. (3) is what the scan is trying to approximate.
Additional note
The 2-windows-on-same-container precondition is not rare: any extension whose install path dispatches a vscode:// URI or workbench.action.openWalkthrough during its activation (GitLens is the case we hit) will cause a second window to be created while the host window is mid-remote-resolution. So this bug is reachable from a plain single-window Rebuild Container command with no user intent to have two windows — which is how we originally hit it (and why it took so long for us to diagnose)
Environment
Summary
When a second VS Code window attaches to a devcontainer while the first window's postCreateCommand is still running, the second window's "Terminating old extension host agent" logic runs kill -9 -$pgrp on every /proc//cmd match of the .vscode-server/bin// regex. In a postCreate that shells out to code --install-extension (a common pattern), that kill takes down the whole script via pgid inheritance and the user sees:
postCreateCommand from devcontainer.json failed with exit code 137.
Skipping any further user-provided commands.
Root cause
dist/extension/extension.js (around the Terminating old extension host agent. string). Rewritten from the minified form:
// 1) Collect every process in the container's mount namespace whose
// cmd path matches the vscode-server/bin regex.
const candidates = procRecords.filter(p =>
p.mntNS === containerMntNS && agentPathRx.test(p.cmd)
);
// 2) Partition by cwd: "current" (cwd is the server install dir) vs "others".
const {current, others} = candidates.reduce((acc, p) => {
const isCurrent =
(p.cwd === canonicalInstallDir ||
p.cwd === serverInstallDir ||
p.cwd === alternateInstallDir)
&& agentPathRx.test(p.cmd);
acc[isCurrent ? 'current' : 'others'].push(p);
return acc;
}, {current: [], others: []});
// 3) SIGKILL the entire PGID of every "other" match.
if (shouldTerminateOthers && others.length) {
output.write('Terminating old extension host agent.\r\n');
try {
await remoteExec(
kill -9 ${others.map(p => p.pgrp ?-${p.pgrp}: p.pid).join(' ')});
} catch {}
}
A postCreateCommand that runs code-server --install-extension X execs node /home/dev/.vscode-server/bin//out/server-main.js --install-extension X.
That node process:
kill -9 - therefore hits the postCreate shell, not just the stale agent the scan was targeting.
Steps to reproduce
window.restoreWindows=all, a user-opened second window, or any other vscode://-URI-dispatching extension should produce the same effect.
Evidence
From a reproduced session (only clock-relevant lines):
[window2] 13:00:57.402 scan /proc (pid/cwd/mnt/stat)
[window2] 13:00:57.671 Terminating old extension host agent.
[window2] 13:00:57.680 kill -9 -381
[window2] 13:00:57.688 (8 ms) kill -9 -381
[window1] 13:00:57.689 Stop (41332 ms): /bin/sh -c /workdir/.devcontainer/install-extensions.sh -postcreate
[window1] 13:00:57.689 postCreateCommand from devcontainer.json failed with exit code 137.
Expected behavior
The "old agent" scan should not kill transient node CLI subcommands (--install-extension, --list-extensions, --uninstall-extension, --version), and
should not take down process groups it didn't spawn.
In order of increasing robustness, any of:
long-running mode) is an actual agent. Bare --install-extension/--list-extensions/etc. are one-shot CLIs and should be ignored by the scan.
(1) is the smallest safe fix. (3) is what the scan is trying to approximate.
Additional note
The 2-windows-on-same-container precondition is not rare: any extension whose install path dispatches a vscode:// URI or workbench.action.openWalkthrough during its activation (GitLens is the case we hit) will cause a second window to be created while the host window is mid-remote-resolution. So this bug is reachable from a plain single-window Rebuild Container command with no user intent to have two windows — which is how we originally hit it (and why it took so long for us to diagnose)