Claude Code in Remote Sessions
Disclaimer: This post was written with the assistance of Anthropic’s Claude Opus 5.5 model, which also helped write the function it describes.
At the moment, I run Claude Code sessions on remote machines over SSH: two Linux VMs and a Mac. A session often works for a long time on a task, and I do not want it to end when the SSH connection drops or when I close the laptop. I also want to come back to the same session later, from the same or from another computer.
Why dtach
The usual tools for this are tmux and screen. I use dtach instead, because it is simpler. It does only one thing: it runs a program detached from the terminal and lets you attach to it again later. There are no windows, panes or key bindings to learn, and the terminal works as usual. Each session is a socket file, and you attach to it by giving the same socket again.
What cdt Does
cdt is a small shell function. cdt NAME attaches to the Claude Code session of the project NAME, or starts it if it is not running:
NAMEis a directory under~/projects. Without a name, it uses the name of the current directory.- The session always starts in the project directory, so it does not matter where I run
cdtfrom. - Each project has its own dtach socket in
~/.dtach, so there is one session per project. - When the project already has a conversation, the new session continues the latest one. Otherwise it starts a new conversation.
- Additional arguments are passed to Claude Code, but only when a session is started.
To get back to a session, I run the same cdt command again.
Things That Were Not Obvious
- Finding the previous conversation. Claude Code keeps the conversations of a directory under
~/.claude/projects/, in a folder named after the directory’s path with every character other than letters and digits replaced by-. If this folder has a conversation,cdtstarts Claude Code with--continue. - Backgrounded sessions. When I press Ctrl+B while a tool is running, Claude Code moves the whole session to its background daemon. While the daemon holds the conversation,
--continuecannot take it. Socdtasks Claude Code for a background session in the same directory. If it is idle,cdtstops it and resumes its conversation. If it is busy,cdtattaches to it instead. - The suspend key. dtach normally suspends itself when Ctrl+Z is pressed. With
-z, Ctrl+Z goes to Claude Code instead. - Redrawing after attaching. With
-r winch, dtach sends a window size change to Claude Code when I attach, so it redraws the screen.
The Function
This is the bash version. It needs dtach and jq. On the Mac, I use a zsh port of the same code. My own version also sets the telemetry attributes I described in Keeping Track of Claude Code Usage, which I left out here.
# cdt [NAME [CLAUDE-ARGS...]]: attach to Claude Code session NAME, or start
# it under dtach so it survives a dropped SSH connection. NAME is a project
# under ~/projects, defaulting to the current directory's name. The session
# always starts in the project directory; it continues that directory's latest
# conversation when one exists, otherwise it starts a new one. CLAUDE-ARGS
# apply only when a session is started. Detach with Ctrl+\, and run the same
# cdt command again to reattach.
# A conversation held by Claude's background daemon (Ctrl+B while a tool runs
# backgrounds the whole session) blocks --continue, so cdt takes it back: an
# idle one is stopped and resumed here, a busy one is attached to.
cdt()
{
local name=${1:-$(basename "$PWD")}
local dir=$HOME/projects/$name
[ -d "$dir" ] || dir=$PWD
local sock=$HOME/.dtach/$name
local -a cmd=()
if compgen -G "$HOME/.claude/projects/${dir//[^A-Za-z0-9]/-}/*.jsonl" >/dev/null; then
cmd=(--continue)
fi
cmd+=("${@:2}")
if [ ! -S "$sock" ]; then
local id sid st pid
read -r id sid st pid < <(claude agents --json --all | jq -r --arg d "$dir" \
'first(.[] | select(.kind == "background" and .cwd == $d and .pid != null))
| "\(.id) \(.sessionId) \(.status) \(.pid)"')
if [ -n "$id" ] && kill -0 "$pid" 2>/dev/null; then
if [ "$st" = busy ]; then
echo "cdt: attaching to busy background session $id"
cmd=(attach "$id")
else
echo "cdt: stopping idle background session $id to resume it here"
claude stop "$id" && cmd=(--resume "$sid" "${@:2}")
fi
fi
fi
mkdir -p "$HOME/.dtach"
(cd "$dir" && dtach -A "$sock" -r winch -z claude "${cmd[@]}")
}
Conclusion
cdt is a small function. I start a task and come back to it later from wherever I am. The session keeps working while I am away, and one command brings me back to it.
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.