Terminal sessions that survive the disconnect.
Run your shells under dtach, list them in the sidebar, reattach in a click.
Your SSH link drops or you reload the window, and the shell you had running is gone along with whatever it was doing. dtach Sessions keeps it alive: every terminal runs under dtach, shows up in the sidebar, and reattaches right where you left off. The program inside never notices you were gone, which makes this a good home for a long-running coding agent like Claude.
dtach does one thing: it holds a program on a detachable pty and gets out of the way. No status bar, no window manager, no config language. This extension leans on that restraint.
Attaching a session runs dtach -a in an ordinary integrated terminal, so
selection, copy, scroll, and search behave like any other VS Code terminal.
Nothing is redrawn through a webview or a PTY proxy. The extension runs on the
remote extension host, next to your sockets and the dtach binary, so nothing
about the terminal round-trips a UI over the wire.
- Install dtach on the remote host (
apt install dtach,brew install dtach, or build from source). - Install the extension on that host (see Installing).
- Open the dtach Sessions view in the activity bar and press +. Name the session; a terminal opens running your shell under dtach.
- Close the window, drop your SSH connection, reload VS Code. Reopen the view, click the row, and you are back in the same session.
Point dtachSessions.startupCommand at claude (or any program) to launch it
automatically in every new session.
The extension host does not source your
.bashrc, so dtach may not be on itsPATH. If sessions fail to open, setdtachSessions.dtachPathto an absolute path like/home/you/.local/bin/dtach.
The sidebar lists every .dtach socket in dtachSessions.socketDir (default
~/.dtach-sessions), most recently active first. Each row shows how long ago the
session last did something. A session with a terminal open in the current window
is marked attached and gets a green icon; detached rows are dimmed so you can
tell at a glance which sessions this window is actually driving.
Clicking a row attaches the session. If a terminal for it is already open, VS Code focuses that one instead of stacking a second client on the socket. The list refreshes when you create a session, when the view becomes visible, and on the title-bar refresh button.
A socket file survives a dtach process that dies without cleaning up — most
often a host reboot, but also an OOM kill or a kill -9. Those rows stay in the
list, and clicking one restarts the session in place: a fresh dtach server on
the same socket, under the same name and id, re-running startupCommand. The
previous session's output is gone (it lived in the process that died) and the
shell's working directory with it, so the terminal opens at the default; you get
told once, and everything else about the row is unchanged. Rows like these also
stop reporting a stale Claude status, so a session cannot sit in the sidebar
showing an amber "waiting" bell that nothing is waiting on. Linux hosts only.
Available from a row's inline icons, its right-click menu, the view's … menu,
or the command palette (search "dtach Sessions").
| Command | What it does |
|---|---|
New Session (+) |
Prompt for a name and open a fresh shell under dtach. |
| Attach | Open (or focus) a terminal for the session. A session whose dtach process is gone (host reboot, OOM kill) is restarted in place instead. |
| Switch Session | Fuzzy-find and attach a session without leaving the keyboard. |
| Open in Detach Session | Right-click a folder in the Explorer: pick a listed session to attach, or type a name (defaults to the folder) and create a new one rooted there. Multiple sessions per folder are numbered like +. |
| New Session Here | Right-click a session row: create a fresh sibling rooted in that session's current working directory (resolved the same way as Restart), joining its name family. |
| Rename | Move the socket and relabel the row and its terminal. The live session survives. |
| Detach | Close this window's terminal but leave the dtach server running. |
| Restart | Terminate the server and open a fresh shell under the same name, re-running startupCommand. Scrollback does not survive. |
| Reap Stale Clients | Clear orphaned dtach clients wedged on a socket (see below). |
| Copy Socket Path / Copy Attach Command | Grab either for scripting or a plain SSH session. |
| Kill | Terminate the server and remove its socket, with confirmation. Select several rows to kill them together, or use Kill All Sessions. |
Sockets are named <prefix><name>_<hash>.dtach. The trailing _<hash> is a
stable id, so a rename moves the socket without losing track of the process, and
a kill resolves the process by id rather than by name. Renamed sessions never
end up orphaned.
A dtach client that outlives its terminal (you closed the window, the SSH link
dropped) can wedge on the socket. Two clients then share one pty with no redraw
buffer, and the wedged one ignores SIGTERM, so your next attach lands on a live
cursor over a blank screen. By default an attach reaps these orphans first
(dtachSessions.reapStaleClientsOnAttach) so the new client owns the socket
cleanly. Reaping only ever kills clients; the session and whatever it runs keep
going. Linux hosts only.
Running an agent CLI under dtach means it survives the disconnects that would otherwise kill a foreground process. dtach Sessions adds a status channel on top of that for Claude Code.
Run dtach Sessions: Install Claude Status Hooks once (or accept the one-time
prompt). It wires a small forwarder into ~/.claude/settings.json, merging
alongside any hooks you already have; Uninstall Claude Status Hooks removes
only its entries. Each session row then reflects what its Claude is doing:
| State | Row shows | Meaning |
|---|---|---|
| working / tool | spinner + working or tool: <name> |
Claude is processing your turn. |
| waiting | amber bell + waiting |
Claude is blocked on a tool-permission decision and needs you. |
| done | green check + done |
Claude finished its turn. Your move. |
| idle | plain row, age only | A fresh or quiet session. |
The amber bell means a genuine permission block and nothing else, so the
activity-bar badge counting waiting sessions stays trustworthy even with the
view collapsed. An idle prompt (Claude waiting ~60s after a finished turn) leaves
the row on done rather than ringing. On a detached row the calm done check
mutes to grey with the dimmed label, while the urgent bell keeps its colour, so a
dim row with a bright bell reads as "dormant session that needs you".
The row's relative time becomes activity-relative while status is available (time in state, or how long since Claude last acted) instead of the socket's mtime. Sessions already running Claude pick up status after a restart, since Claude reads its hooks at session start.
Linux hosts only, and the forwarder needs python3 on the host. See the
status note for how correlation works.
| Setting | Default | Description |
|---|---|---|
dtachSessions.socketDir |
~/.dtach-sessions |
Directory holding the sockets (~ expands to home). Created on first session. |
dtachSessions.socketPrefix |
(empty) | Filename prefix; files are <prefix><name>_<hash>.dtach. See the migration note below. |
dtachSessions.startupCommand |
(empty) | Command run inside a session's shell on create (not reattach), e.g. claude. |
dtachSessions.redrawMethod |
winch |
-r value on attach and create. One of winch, ctrl_l, none. See note below. |
dtachSessions.dtachPath |
dtach |
Path to the dtach binary; set an absolute path if it is not on PATH. |
dtachSessions.reflectProcessTitle |
true |
Let the running program's title drive the terminal tab (e.g. an agent CLI's live status). The session name still labels the sidebar row. Set false to pin the session name on the tab. |
dtachSessions.showClaudeStatus |
true |
Show a Claude instance's live run-state (working / tool / waiting / done / idle) on each row. Needs the status hooks. Linux only. |
dtachSessions.reapStaleClientsOnAttach |
true |
Reap orphaned dtach clients before attaching so the new client redraws cleanly. Disable if you deliberately attach one session from several windows. Never touches the session itself. Linux only. |
- dtach on the remote host.
- For Kill:
lsof(preferred) orpgrep. Kill finds the owning process withlsof -t <socket>, falls back topgrep -f, then removes the socket. With neither available it removes the socket without confirming the process is gone, so a live session could be left orphaned; on any Linux host that has dtach at least one of these is effectively always present. - For live Claude status (optional):
python3and a Linux host, since the forwarder reads/proc. Other hosts show no status and everything else works.
Build the .vsix:
npm install
npm run compile # tsc -p ./ -> out/
npx @vscode/vsce package # -> dtach-sessions-<version>.vsixThen, in a Remote-SSH window, open the Command Palette, run Extensions: Install from VSIX…, and pick the file. VS Code uploads and installs it on the remote host. Reload the remote window. Tagged builds are also attached to the GitHub releases.
Some behaviour is inherent to running programs under a detached pty. None of it harms the program itself.
winch repaints on reattach only when the terminal size differs from the size at
detach, so reattaching at the same size can leave a TUI blank until the next
resize. ctrl_l forces a redraw regardless of size, but it sends a literal
Ctrl-L, which some TUIs (Claude among them) read as a clear-screen keystroke.
Pick the trade-off that suits you.
VS Code only honours an escape-set tab title from a process it recognises as an agent CLI (Claude Code, Copilot, Gemini), so the extension cannot seed the tab itself. Resume an idle agent and the tab shows the session-name fallback until the agent next changes state and re-emits its title.
Separately, an agent CLI running under dtach may log a VS Code IPC error and lose
editor integration after you reattach from a different window: dtach freezes the
program's environment at creation, so the VSCODE_IPC_HOOK_CLI socket it
inherited goes stale. Set reflectProcessTitle: false to just pin the session
name on the tab.
The forwarder maps each Claude session to its row by walking /proc from the
firing hook up to the dtach master, then reading the socket path and its
_<hash> id from that process's command line. Status follows a session across
rename and reattach, and works for sessions created outside the extension. It is
a no-op when nothing runs under dtach, so the host-global hook stays harmless to
your other Claude sessions. A session that exits without a clean stop (a crash, a
killed connection) decays from working back to its age after a couple of
minutes instead of sticking. Sessions whose socket predates the _<hash> scheme
show no status.
The default socketPrefix is now empty (it was .claude-). Sockets from older
versions are named .claude-*.dtach and will not appear under the new default.
Set dtachSessions.socketPrefix back to .claude- to keep seeing them, or kill
and recreate those sessions under the new naming.
No unit suite. Run through these against a build:
- + →
webcreates~/.dtach-sessions/web_<hash>.dtach, opens a shell, and awebrow appears with a relative age. - Click the row: a live TUI renders immediately and the row shows as attached (green icon).
- With
startupCommandset toclaude, a freshly created session auto-runs it. - Rename
web→api: the socket becomesapi_<hash>.dtach, the row and terminal relabel, and the session stays live. - Kill
api: the process is gone (pgrep -f _<hash>.dtachfinds nothing) and the socket is removed. Renaming did not orphan it. - Reload the remote window, click a session: it reattaches.
- Select several rows → Kill, or Kill All from the
…menu: all gone. - Drag-select and right-click copy work natively in the attached terminal.
- Kill a session's dtach process without removing its socket
(
pkill -9 -f _<hash>.dtach): the row keeps listing, any Claude status badge on it clears, and clicking it opens a working shell on the same_<hash>.dtachsocket with a one-line notice — no warning aboutdtachSessions.dtachPath. - Right-click a folder with no session: the QuickPick shows only "New
session"; accepting the prefilled name creates it, rooted there. Right-click
again: an Attach row for it now appears above "New session"; picking
"New session" a second time creates
<folder>-2. Edit the input to a custom name before accepting "New session": the Attach row(s) stay visible and selectable throughout.
MIT