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) <noreply@anthropic.com>
This commit is contained in:
Emma Thorpe
2026-06-10 11:30:09 +01:00
committed by lyrathorpe
parent 27fc7ae6d3
commit 860d4ccaa9
3 changed files with 51 additions and 29 deletions
-22
View File
@@ -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
'';
}
+46 -6
View File
@@ -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;
}
+5 -1
View File
@@ -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;