Created
January 24, 2026 00:37
-
-
Save jeffmccune/fca394413347a423771e2d60955d5d19 to your computer and use it in GitHub Desktop.
Attach to Claude Code
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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