Appearance
Shell & Workspace Performance
You open a new terminal tab and wait. And wait. A second and a half later, the prompt appears. You type cd ~/projects/my-agent and the shell freezes for another half-second while some auto-activation script runs. You do this forty times a day, across half a dozen terminal windows, and those pauses add up to minutes of your life spent staring at a blinking cursor. The worst part: most of that delay comes from tools you aren't even using in that particular session. Your shell is loading Node version manager, a Python environment activator, a cloud CLI, and three other things, every single time, for every single terminal window, whether you need them or not.
This isn't about squeezing out the last millisecond of performance. It's about not being annoyed by your tools. A terminal should feel instant — type a command, get a response. When it doesn't, you start avoiding it, and that's a problem when every AI tool you use lives at the command line.
The fix has three layers: strip out what doesn't need to load at startup, speed up what you do repeatedly, and keep your long-running work alive when your connection drops. That's aliases, lazy-loading, and tmux.
What you'll learn
- Shell startup time matters because you open terminals dozens of times a day — each wasted second compounds
- Lazy-loading defers heavy tool initialization until the first time you actually use the tool
- Aliases and shell functions replace repeated typing with muscle memory: two or three keystrokes for commands you run constantly
- tmux keeps your sessions alive when your terminal closes, your SSH drops, or your laptop goes to sleep
The problem: your terminal opens slow and every command feels heavy
Your shell reads a series of configuration files when it starts. For bash, the main one is ~/.bashrc. For zsh, it's ~/.zshrc. Every line in that file runs at shell startup — and if any of those lines call out to the network, scan a large directory, or source another file that does the same, you pay that cost on every new terminal.
Here's how to see what's slow:
bash
# Time your shell startup from scratch
time bash -i -c exitOn a clean system, this returns in under 0.05 seconds. On a development machine that's accumulated years of tool installations, it's often 0.5 to 2 seconds. A half-second doesn't sound like much until you remember that every split pane in tmux, every new terminal tab, every SSH session pays it again.
The usual suspects: Node Version Manager (nvm), cloud CLI auto-completion scripts, Python environment managers that run at shell init, and anything that calls a network service during startup. None of these need to run when you open a terminal to run ls.
Options & when to use each
There are three layers to a fast shell, and they stack. You do not need all three on day one, but each one addresses a different kind of friction.
| Layer | What it solves | Cost | When to apply it |
|---|---|---|---|
| Aliases | Typing the same long commands repeatedly | None — a few lines in your shell config | Immediately. Takes two minutes to set up and pays back every session |
| Lazy-loading | Shell startup time when heavy tools (nvm, conda, cloud CLIs) initialize eagerly | Slightly more typing the first time you use a lazy-loaded command | When your shell takes more than half a second to start |
| tmux | Losing your work when the terminal closes or SSH drops | Learning a handful of keystroke combinations | When you start running long processes (model downloads, training runs, multi-hour agent tasks) |
Build it: aliases, lazy-loading, and tmux
Aliases and functions: type less, do more
An alias is a short name you give to a longer command. Put these in ~/.bashrc (or ~/.zshrc if you use zsh) and they're available in every new terminal.
bash
# Navigation shortcuts
alias ..='cd ..'
alias ...='cd ../..'
alias ll='ls -lah'
# Git shortcuts — you'll type these hundreds of times
alias gs='git status'
alias ga='git add'
alias gc='git commit -m'
alias gp='git push'
alias gl='git log --oneline --graph --all'
# Docker shortcuts
alias dps='docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"'
alias dcu='docker compose up -d'
alias dcd='docker compose down'
# Python / uv
alias uvrun='uv run'
alias uvsync='uv sync'Apply changes without logging out: source ~/.bashrc.
Aliases are for simple substitutions — one command becomes a shorter name. When you need logic (arguments, conditionals, multiple steps), reach for a shell function instead. Functions go in the same file, same as aliases.
bash
# Create a directory and cd into it in one motion
mkcd() {
mkdir -p "$1" && cd "$1"
}
# Activate a Python venv if it exists, create it if not
activate() {
if [ -f "$1/bin/activate" ]; then
source "$1/bin/activate"
else
echo "No venv at $1. Create one with: uv venv $1"
fi
}
# Extract any archive format
extract() {
if [ -f "$1" ]; then
case "$1" in
*.tar.bz2) tar xjf "$1" ;;
*.tar.gz) tar xzf "$1" ;;
*.zip) unzip "$1" ;;
*.tar.xz) tar xf "$1" ;;
*) echo "Don't know how to extract $1" ;;
esac
fi
}The test for whether something should be an alias: have you typed the full command three times this week and will you type it again tomorrow? If yes, alias it. The cost is one line in a config file. The payoff is saving your brain from remembering docker compose --profile gpu up -d --build every time.
Lazy-loading: don't pay for what you're not using
The biggest startup time sinks are tools that need to source large shell scripts. Node Version Manager's init script is thousands of lines. Conda's is similar. If you load these eagerly — meaning they run every single time a terminal opens — you're paying for them whether you're writing Node or running Python or doing neither.
The pattern is called a stub. You replace the real command with a thin shell function. The first time you invoke it, the function loads the real tool and then calls it. Every subsequent invocation in the same session uses the now-loaded real version. You never use the tool in that terminal session? You never pay for it.
Here's the pattern, written generically — adapt it for whatever slow-loading tool you have:
bash
# Pattern: lazy-load any command that sources a large init script
slowtool() {
unset -f slowtool # Remove this stub
source /path/to/slowtool/init.sh # Load the real thing
slowtool "$@" # Run it with the original arguments
}For a concrete example, here's a lazy-load stub for nvm. If you don't use nvm, this pattern works identically for pyenv, asdf, sdkman, or any other version manager:
bash
# Lazy-load nvm — only pays the cost when you actually use node
nvm() {
unset -f nvm
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm "$@"
}For Python tooling, the landscape has shifted. If you're using uv (and you should be — it's orders of magnitude faster than pip and doesn't need a heavy environment activator at shell startup), there's nothing to lazy-load. uv starts instantly and doesn't source anything into your shell. But if you have legacy Conda environments, stub it:
bash
# If you still need conda, stub it — don't let conda init slow every terminal
conda() {
unset -f conda
eval "$(conda shell.bash hook)"
conda "$@"
}What about auto-activating Python environments when you cd into a project? Some tools offer "auto-switch" features that detect a .python-version or venv/ and activate it on directory change. These are convenient but they run on every cd, and if the check itself is slow, every directory change gets a hiccup. The tradeoff: if you toggle between projects constantly, auto-activation saves friction. If you spend hours in one project, the per-cd cost isn't worth it. I recommend starting without auto-activation and adding it only if you find yourself manually running source .venv/bin/activate ten times a day.
.bashrc organization
As you add aliases, functions, and stubs, your .bashrc becomes a scrolling mess. A simple organizational pattern:
bash
# ~/.bashrc
# === Environment variables ===
export EDITOR=nano
export UV_CACHE_DIR="$HOME/.cache/uv"
# === PATH additions ===
export PATH="$HOME/.local/bin:$PATH"
# === Aliases ===
alias ll='ls -lah'
alias gs='git status'
# ... rest of aliases
# === Functions ===
mkcd() { mkdir -p "$1" && cd "$1"; }
# === Lazy-load stubs ===
# (nvm, conda, or whatever you need stubs for)
# === Prompt customization ===
# (optional: a Git-aware prompt, colors, etc.)Keep sections separated by comment headers. When you add something, you know where it goes. When you're debugging slow startup, you can comment out entire sections with a single # at the section header to isolate the culprit.
tmux: your session survives, no matter what
You're running a model training script that's been going for three hours. Your SSH connection drops. Without tmux, the script dies with it. The terminal emulator receives a SIGHUP (hangup signal), the shell exits, and every child process — your training run, your database migration, your long-running test suite — gets terminated.
tmux is a terminal multiplexer. It runs as a server in the background. Terminal windows attach to it as clients. When a client disconnects — you close the window, your SSH drops, your laptop lid closes — the server keeps running, and all your processes inside it keep running too. You reconnect later and everything is exactly where you left it.
The tmux mental model
A tmux session is a collection of windows. A window is a full-screen terminal — think of it as a tab. A pane is a split within a window — left and right, or top and bottom. You can have one session with ten windows, each window with four panes, and detach from the whole thing without any of it stopping.
The commands you'll actually use
bash
# Start a new named session
tmux new-session -s agent
# Detach from the current session (keep everything running)
# Press: Ctrl+b, then d
# List all running sessions
tmux ls
# Reattach to a session
tmux attach -t agent
# Kill a session when you're done
tmux kill-session -t agentThe keyboard shortcuts that matter
The prefix key is Ctrl+b — you press it, release both keys, then press the next key. (If you remap it to something more ergonomic like Ctrl+a, even better, but the defaults work fine.)
| Shortcut | What it does |
|---|---|
Ctrl+b d | Detach — leave the session running, return to your plain terminal |
Ctrl+b c | Create a new window (a new tab) |
Ctrl+b n | Next window |
Ctrl+b p | Previous window |
Ctrl+b % | Split the current pane vertically (left and right) |
Ctrl+b " | Split the current pane horizontally (top and bottom) |
Ctrl+b x | Kill the current pane (confirms with y/n) |
Ctrl+b z | Zoom the current pane to full screen (press again to restore) |
Ctrl+b [ | Enter scroll mode — use arrow keys, Page Up/Down, q to exit |
A real AI workflow in tmux
Here's a layout you might use when developing and monitoring an agent:
bash
tmux new-session -s agent-dev
# Window 1 (code): your editor
# Ctrl+b % to split vertically: editor on the left, shell on the right
# Ctrl+b c → Window 2 (logs): tail -f server.log
# Ctrl+b " to split horizontally: uvicorn output top, nginx access log bottom
# Ctrl+b c → Window 3 (monitoring): htop or nvidia-smi -l 1
# Watch GPU memory, CPU load while the agent runs
# Ctrl+b 0 → back to Window 1Now you can detach (Ctrl+b d), close your terminal, go to a meeting, come back, and tmux attach -t agent-dev picks up right where you left off. Every pane, every scrollback buffer, every running process — still there.
If you can only remember three things about tmux
tmux new -s nameto start,Ctrl+b dto detach,tmux attach -t nameto come back. That alone saves you from losing work to disconnected SSH.Ctrl+b %for a vertical split,Ctrl+b "for a horizontal split. These replace opening three separate terminal windows.Ctrl+b [then arrow keys to scroll. This is the one everyone misses — without it, you're stuck staring at the bottom of a log file with no way to go back.
Build it: from slow shell to smooth workspace
Step 1: Measure your current startup time.
bash
time bash -i -c exit
# or for zsh:
time zsh -i -c exitWrite down the number. You'll compare against it at the end.
Step 2: Add the aliases you'll actually use.
Open ~/.bashrc and add the aliases from the section above that apply to your workflow. Don't add all of them — add the ones that replace commands you typed today. Then source ~/.bashrc.
Step 3: Identify and stub your slow loaders.
Run this to see what your shell is sourcing at startup:
bash
# For bash: add 'set -x' temporarily to see every command executed
bash -x -i -c exit 2>&1 | tail -50Look for anything sourcing large files — nvm, conda, pyenv, asdf, cloud CLI completions. Add stubs for the ones you don't need in every single terminal.
Step 4: Measure again.
bash
time bash -i -c exitIf you stubbed something heavy, you should see a real drop. Going from 1.5s to 0.1s is common when you lazy-load nvm and conda.
Step 5: Set up a basic tmux workflow.
bash
tmux new-session -s dev
# Split the window, run something in one pane
# Ctrl+b %, then in the new pane: htop
# Ctrl+b d to detach
tmux attach -t dev # confirm everything is still runningStep 6: Add a minimal .tmux.conf.
Create ~/.tmux.conf with quality-of-life settings:
text
# Enable mouse — click to select panes and windows, scroll with wheel
set -g mouse on
# More scrollback history (10,000 lines instead of the default 2,000)
set -g history-limit 10000
# Start window numbering at 1 instead of 0 (matches keyboard layout)
set -g base-index 1
setw -g pane-base-index 1Reload with tmux source-file ~/.tmux.conf.
What goes wrong
| Mistake | How you notice it | The fix |
|---|---|---|
| Adding an alias that shadows a real command | You aliased ls to ls --color=auto but now scripts that depend on raw ls output break, or you aliased rm to rm -i and batch scripts are prompting for every file | Don't alias over existing commands unless you're certain. For safety wrappers, use a different name: alias del='rm -i' instead of overriding rm |
| Lazy-load stub that never actually loads | You call nvm install 20 and get "command not found" — the stub ran once, unset itself, but the source path was wrong | Test each stub after adding it: open a fresh terminal, run the stubbed command, then run it a second time. The first call should trigger the load, the second should use the real command |
| tmux prefix key conflicts with another tool | You're in a tmux pane, press Ctrl+b to go back one character (readline binding), and tmux eats it instead of your editor | Either remap tmux's prefix (add set -g prefix C-a to .tmux.conf), or press Ctrl+b twice — the second one passes through to the application |
.bashrc has an error that prevents the shell from starting | You edited .bashrc, opened a new terminal, and it immediately closed, or you see error spew before the prompt | Always keep a second terminal open when editing .bashrc. If you lock yourself out, SSH in from another machine or boot into single-user mode. The safer workflow: bash --rcfile <(cat ~/.bashrc; echo 'your new line') to test changes in a subshell |
| tmux session accumulates stale panes | tmux ls shows five sessions, each with ten windows, and half of them are running processes you forgot about from last month | Make a habit of tmux kill-session -t name when a project is done. Run tmux ls once a week and clean up. tmux sessions survive reboots — they don't, but the habit of thinking they do leads to cruft |
Confirm it worked
- Run
time bash -i -c exit(or the zsh equivalent) and confirm it's under 0.2 seconds. If it's not, you've still got something heavy loading at startup - Open a fresh terminal and type your most-used alias —
gsforgit status, or whatever you set up. It should work immediately - Start a tmux session, create two panes, detach, close the terminal, open a new terminal, and reattach. Everything should be exactly as you left it
- In tmux, scroll up through your terminal history with
Ctrl+b [. If mouse mode is on in.tmux.conf, you should be able to scroll with the mouse wheel too - Run a long command in a tmux pane (like
sleep 120), detach, and reattach — confirm the command is still running
Next: Python Environments — virtual environments, uv, and keeping project dependencies from colliding.