Skip to content

Instantly share code, notes, and snippets.

@jeffmccune
Created January 24, 2026 00:37
Show Gist options
  • Select an option

  • Save jeffmccune/fca394413347a423771e2d60955d5d19 to your computer and use it in GitHub Desktop.

Select an option

Save jeffmccune/fca394413347a423771e2d60955d5d19 to your computer and use it in GitHub Desktop.
Attach to Claude Code
#!/bin/zsh
#
# Attach to or create a "coding" tmux session with proper ssh-agent setup.
#
# ssh-agent lifecycle:
# - Agent is started when creating a new tmux session
# - Agent persists across detach/reattach (managed by tmux, not this script)
# - Agent is killed via tmux session-closed hook when the session ends
# - A trap handles cleanup if the script is interrupted before session creation
SESSION_NAME="coding"
WORKSPACE_DIR="${HOME}/workspace/holos-run"
AGENT_SOCK="${HOME}/.ssh/agent.sock"
# Setup GPG agent for remote SSH
# On Debian 13, gpg-agent is managed by systemd user service (supervised)
# We just need to ensure GPG_TTY is set and update the agent's TTY
export GPG_TTY=$(tty)
# Update GPG_TTY in case of reconnection (works with systemd-managed agent)
gpg-connect-agent updatestartuptty /bye > /dev/null 2>&1 || true
# Check if tmux session exists
if tmux has-session -t "$SESSION_NAME" 2>/dev/null; then
# Session exists - validate that symlink exists and points to a valid socket
# The symlink should point to the local ssh-agent started when the session was created
if [ -L "$AGENT_SOCK" ] || [ -S "$AGENT_SOCK" ]; then
# Check if the symlink target (or socket itself) is valid
if [ -S "$AGENT_SOCK" ]; then
# Socket exists and is valid - use it
export SSH_AUTH_SOCK="$AGENT_SOCK"
else
# Symlink exists but target is invalid (local agent died)
# For now, we'll proceed but the socket won't work
# Future enhancement: could restart the agent here
export SSH_AUTH_SOCK="$AGENT_SOCK"
fi
fi
# Update tmux's global environment with the symlink path
# This ensures all new windows will inherit the correct SSH_AUTH_SOCK
if [ -S "$AGENT_SOCK" ]; then
tmux set-environment -g SSH_AUTH_SOCK "$AGENT_SOCK"
# Update all existing windows' SSH_AUTH_SOCK environment variable
# Get list of windows in the session
for window in $(tmux list-windows -t "$SESSION_NAME" -F '#{window_index}'); do
tmux set-environment -t "$SESSION_NAME:$window" SSH_AUTH_SOCK "$AGENT_SOCK"
done
fi
# Re-attach to existing session and switch to window 0
# Don't clean up agents since we didn't start them
trap - EXIT INT TERM HUP
tmux attach-session -t "$SESSION_NAME" \; select-window -t 0
else
# Session doesn't exist - start ssh-agent and create session
eval "$(ssh-agent -s)" > /dev/null
# Trap cleans up agent if script is interrupted before tmux session is created
# Once the session exists, tmux's session-closed hook takes over cleanup
trap 'kill $SSH_AGENT_PID 2>/dev/null' EXIT INT TERM HUP
# Force update the symlink to point to the new socket
# Remove old link if it exists, then create new one
[ -L "$AGENT_SOCK" ] && rm -f "$AGENT_SOCK"
[ -S "$AGENT_SOCK" ] && rm -f "$AGENT_SOCK"
ln -sf "$SSH_AUTH_SOCK" "$AGENT_SOCK"
# Export the canonical path
export SSH_AUTH_SOCK="$AGENT_SOCK"
# Set SSH_AUTH_SOCK in tmux's global environment so all windows inherit it
tmux set-environment -g SSH_AUTH_SOCK "$AGENT_SOCK"
# Create temporary init script for claude (window 0)
# Use exec so claude is the primary process - window closes when claude exits
CLAUDE_INIT=$(mktemp /tmp/claude-init-XXXXXX.sh)
cat > "$CLAUDE_INIT" << 'EOF'
clear
export GPG_TTY=$(tty)
gpg-connect-agent updatestartuptty /bye > /dev/null 2>&1
exec claude --allow-dangerously-skip-permissions --dangerously-skip-permissions
EOF
chmod +x "$CLAUDE_INIT"
# Create new tmux session with two windows
# Window 0: claude code (exec so window closes on exit, clear screen first)
# Window 1: zsh shell (no commands sent, GPG setup handled by default-command)
#
# The session-closed hook kills ssh-agent and removes the socket symlink when
# the tmux session ends. This replaces script-based cleanup - tmux owns the
# agent lifecycle, so detaching doesn't kill the agent but closing does.
#
# Disable script trap before attaching - tmux hook now owns cleanup
trap - EXIT INT TERM HUP
tmux new-session -d -s "$SESSION_NAME" -c "$WORKSPACE_DIR" -n "claude" \; \
set-environment SSH_AGENT_PID "$SSH_AGENT_PID" \; \
set-hook session-closed "run-shell 'kill $SSH_AGENT_PID 2>/dev/null; rm -f $AGENT_SOCK 2>/dev/null'" \; \
send-keys -R \; \
send-keys ". $CLAUDE_INIT" C-m \; \
new-window -c "$WORKSPACE_DIR" -n "shell" \; \
send-keys -t 1 -R \; \
set-environment -t "$SESSION_NAME:1" SSH_AUTH_SOCK "$AGENT_SOCK" \; \
select-window -t 0 \; \
set-option -g default-command "cd '$WORKSPACE_DIR' && export GPG_TTY=\$(tty) && export SSH_AUTH_SOCK='$AGENT_SOCK' && gpg-connect-agent updatestartuptty /bye > /dev/null 2>&1 && exec zsh" \; \
attach-session -t "$SESSION_NAME"
# Clean up temp file after a delay (tmux needs to read it first)
(sleep 2 && rm -f "$CLAUDE_INIT" 2>/dev/null) &
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment