From 829f20930002c064f41b4d7d5bcaa68056c848fd Mon Sep 17 00:00:00 2001 From: Emma Thorpe Date: Wed, 10 Jun 2026 11:30:09 +0100 Subject: [PATCH] feat(shell): start tmux in every terminal; ssh-agent with auto-add Move the tmux auto-start out of the graphical-only desktop layer into the shared shell config so it also covers WSL, iTerm2 and the Linux console (folded into programs.zsh.initContent via mkMerge alongside the SSH PS1 block). Same guards: interactive, not-already-in-tmux, not-SSH, not-VS-Code, tmux-present. ssh: run a user ssh-agent on Linux (macOS uses launchd) and add keys on first use (addKeysToAgent), so the passphrase is entered once per login session instead of per commit/push -- which also feeds commit signing. macOS additionally caches in the login keychain (UseKeychain). The work box keeps its own ~/.ssh/config (programs.ssh forced off there); its ssh-agent still runs via the work module. Co-Authored-By: Claude Opus 4.8 (1M context) --- lyrathorpe/home/desktop.nix | 22 -------------- lyrathorpe/home/shell.nix | 52 +++++++++++++++++++++++++++++---- system/modules/work/default.nix | 6 +++- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/lyrathorpe/home/desktop.nix b/lyrathorpe/home/desktop.nix index 8bfd10c..77e374d 100644 --- a/lyrathorpe/home/desktop.nix +++ b/lyrathorpe/home/desktop.nix @@ -6,7 +6,6 @@ pkgs, config, inputs, - lib, username, ... }: @@ -96,25 +95,4 @@ }; }; }; - - # Auto-start tmux in graphical terminals (foot): opening a terminal drops - # straight into a session named "main" (attach if it exists, else create). - # Panes inside run a plain non-login zsh (tmux's default-command "${SHELL}"). - # Order 200 runs this before oh-my-zsh/compinit so the exec replaces the shell - # before that setup is wasted. Guards (each prevents a real breakage): - # interactive only -> don't hijack scp/ssh-command/non-interactive shells - # $TMUX empty -> a pane's zsh won't re-exec tmux (infinite loop) - # not SSH -> don't force inbound SSH logins into a server tmux - # not VS Code -> its integrated terminal manages itself - # tmux on PATH -> exec failure would otherwise kill the login shell - # Lives here (graphical hosts only); the WSL work box never imports this. - programs.zsh.initContent = lib.mkOrder 200 '' - if [[ $- == *i* ]] \ - && [[ -z "$TMUX" ]] \ - && [[ -z "$SSH_CONNECTION" && -z "$SSH_TTY" ]] \ - && [[ "$TERM_PROGRAM" != "vscode" ]] \ - && command -v tmux >/dev/null 2>&1; then - exec tmux new-session -A -s main - fi - ''; } diff --git a/lyrathorpe/home/shell.nix b/lyrathorpe/home/shell.nix index 259aacf..385ede8 100644 --- a/lyrathorpe/home/shell.nix +++ b/lyrathorpe/home/shell.nix @@ -54,12 +54,34 @@ theme = "robbyrussell"; }; syntaxHighlighting.enable = true; - # Prefix the prompt with the hostname over SSH. - initContent = lib.mkOrder 1500 '' - if [ "$SSH_CLIENT" ] || [ "$SSH_TTY" ]; then - export PS1="%M $PS1" - fi - ''; + initContent = lib.mkMerge [ + # Auto-start tmux in every interactive terminal -- foot, iTerm2, the WSL + # shell, the Linux console -- so a new terminal lands straight in the + # multiplexer (session "main": attach if present, else create). Panes run + # a plain non-login zsh (tmux's default-command "${SHELL}"). Order 200 + # runs before oh-my-zsh/compinit so the exec replaces the shell before + # that setup is wasted. Guards, each preventing a real breakage: + # interactive only -> don't hijack scp / `ssh host cmd` / scripted shells + # $TMUX empty -> a pane's zsh won't re-exec tmux (infinite loop) + # not SSH -> don't force inbound SSH logins into a server tmux + # not VS Code -> its integrated terminal manages itself + # tmux on PATH -> a failed exec would otherwise kill the login shell + (lib.mkOrder 200 '' + if [[ $- == *i* ]] \ + && [[ -z "$TMUX" ]] \ + && [[ -z "$SSH_CONNECTION" && -z "$SSH_TTY" ]] \ + && [[ "$TERM_PROGRAM" != "vscode" ]] \ + && command -v tmux >/dev/null 2>&1; then + exec tmux new-session -A -s main + fi + '') + # Prefix the prompt with the hostname over SSH (mkAfter). + (lib.mkOrder 1500 '' + if [ "$SSH_CLIENT" ] || [ "$SSH_TTY" ]; then + export PS1="%M $PS1" + fi + '') + ]; shellAliases = { # eza's zsh integration also defines these; set explicitly so the # icons/git intent is obvious. @@ -189,4 +211,22 @@ set -ag status-right "#{E:@catppuccin_status_session}" ''; }; + + # Add the key to the agent on first use, so the passphrase is typed once per + # login session rather than per commit/push (commit signing uses this agent). + # The work box keeps its own ssh config (see work/default.nix), so this only + # manages ~/.ssh/config on the personal hosts. + programs.ssh = { + enable = true; + addKeysToAgent = "yes"; + # macOS: also cache in the login keychain (no prompt after first unlock). + # UseKeychain is unknown to non-Apple openssh, so only emit it on Darwin. + extraConfig = lib.optionalString pkgs.stdenv.hostPlatform.isDarwin '' + UseKeychain yes + ''; + }; + + # Run a user ssh-agent on Linux (macOS provides one via launchd). EDaaS also + # enables this in the work module; both being true merges cleanly. + services.ssh-agent.enable = lib.mkIf pkgs.stdenv.hostPlatform.isLinux true; } diff --git a/system/modules/work/default.nix b/system/modules/work/default.nix index 936468e..854ee9c 100644 --- a/system/modules/work/default.nix +++ b/system/modules/work/default.nix @@ -1,6 +1,10 @@ -{ pkgs, ... }: +{ pkgs, lib, ... }: { + # The work box keeps its own (corporate) ~/.ssh/config; don't let the personal + # programs.ssh (shell.nix) take it over. The ssh-agent below still runs. + programs.ssh.enable = lib.mkForce false; + programs.git = { settings = { commit.gpgsign = true;