My Fish Shell Setup
Notes on how I have fish shell set up on macOS. Fish is the shell I land in every time I open Alacritty, so I spend a lot of time here.
Why Fish
Bash and zsh are fine, but fish gives me good defaults out of the box. Syntax highlighting as I type, autosuggestions from history, and a sane scripting language. It is not POSIX, which means I cannot paste random shell snippets and expect them to work, but for daily interactive use I prefer it.
Fish lives at /opt/homebrew/bin/fish (installed via Homebrew) and is set
as the shell in my Alacritty config.
Auto attach to tmux
The first thing my config does after loading Homebrew is jump straight into tmux:
if status is-interactive
and not set -q TMUX
exec tmux new -As0
end
tmux new -As0 either attaches to session 0 if it exists, or creates
it. Combined with exec, the fish process is replaced by tmux, so I do
not have a stray shell hanging around outside the session.
The check on TMUX is important. Without it, every pane fish opens
inside tmux would try to start tmux again.
Greeting and basics
set fish_greeting
Empty greeting. I do not need fish to tell me hello every time.
set -gx GPG_TTY (tty)
set -U EDITOR nvim
GPG_TTY is required for GPG signing to prompt for the passphrase. I
sign commits, so this matters. EDITOR is universal so git, fzf and
anything else that calls into $EDITOR picks neovim.
Version managers
I have a few language version managers installed. They are gated on
status --is-interactive so they do not slow down non interactive
sessions:
# rbenv
if status --is-interactive
set -x PATH $PATH "$(brew --prefix)/shared/rbenv"
rbenv init - | source
end
# pyenv
if status --is-interactive
set -Ux PYENV_ROOT $HOME/.pyenv
set -U fish_user_paths $PYENV_ROOT/bin $fish_user_paths
pyenv init - fish | source
pyenv virtualenv-init - | source
end
I also have tfenv on PATH (for Terraform) and sdkman for the JVM languages, set up through a conf.d snippet that ships with the sdkman fish plugin.
Locale
I set every LC_* variable to en_US.UTF-8. Worth being explicit
because some tools (ruby, postgres, certain ssh setups) will silently
fall back to C locale otherwise, which breaks UTF-8 handling:
set -gx LANG "en_US.UTF-8"
set -gx LC_ALL "en_US.UTF-8"
# ...and the rest
PATH and SDK paths
Android SDK, JDK 17 include path for native builds, libomp for compiling some Python wheels, and a few extras:
set -gx ANDROID_HOME /Users/ferbass/Library/Android/sdk
set -gx PATH $ANDROID_HOME/tools $ANDROID_HOME/platform-tools $PATH
set -gx CPPFLAGS "-I/opt/homebrew/opt/openjdk@17/include"
set -gx LDFLAGS "-L/opt/homebrew/opt/libomp/lib"
Plus ~/.local/bin and ~/.tfenv/bin near the front of PATH.
Disable Homebrew analytics
set -gx HOMEBREW_NO_ANALYTICS 1
Small thing, but I do not need Homebrew sending usage data.
Aliases
I keep aliases short and few:
alias assume="set -x GRANTE_SAML true; source /opt/homebrew/bin/assume.fish"
alias tns="tmux new -d -s"
assumeis for the granted AWS credential tool. The env var puts it in SAML mode.tns foocreates a detached tmux session namedfoo. Handy when I want to spin up a background workspace without leaving the current one.
Functions
These live in fish/functions/. Each function gets its own file, which
fish auto loads on first call.
function cat
bat $argv
end
cat is shadowed by bat for the
syntax highlighting and line numbers.
function ls --wraps='eza --icons --group-directories-first'
eza $argv --icons --group-directories-first
end
ls is shadowed by eza for
icons and directories first.
function vim
if type -q nvim
command nvim $argv
else
echo "nvim is not installed"
end
end
vim runs nvim if it is installed. Keeps muscle memory working.
A couple of small utilities:
function myip
curl ifconfig.me
end
function lockscreen --description '[macOS] Lock your screen without Log out the user'
pmset displaysleepnow
end
I also have a few git helpers like default_branch <url> (uses
ls-remote --symref to pull the default branch name) and
ensure_remote <name> <url> (adds or updates a remote).
Tmux + AI status icon
When I run claude or gemini inside tmux, I want a robot icon to
appear in the tmux window name so I can see at a glance which window
has an AI session running. Fish has fish_preexec and fish_postexec
events that fire before and after every command:
function __tmux_ai_preexec --on-event fish_preexec
if set -q TMUX
set cmd (string split ' ' $argv[1])[1]
switch $cmd
case claude claude-personal
tmux set-option -p @ai_running "🤖"
case gemini
tmux set-option -p @ai_running "🤖"
end
end
end
function __tmux_ai_postexec --on-event fish_postexec
if set -q TMUX
tmux set-option -pu @ai_running
end
end
The tmux side picks this up through a window name format that reads
@ai_running. The result is a small robot icon next to the window
title while the command is running.
Keybindings
In conf.d/keybindings.fish:
bind \e. history-token-search-backward
This gives me Alt+. to paste the last token from the previous command,
which is the readline behaviour I am used to from bash and zsh. Fish
does not ship it by default, so I add it back.
Prompt
I use a small custom prompt in functions/fish_prompt.fish. It shows
the current directory (shortened), the git branch, and the exit code
in red if the last command failed. Two lines, so the command always
starts at column 1.
Local secrets
Last line of the config sources a gitignored file for anything machine specific:
if test -f ~/.config/fish/config.local.fish
source ~/.config/fish/config.local.fish
end
That is where any API tokens or credentials live, so they never end up in the dotfiles repo.
Plugins
Managed by fisher. The
fish_plugins file lists what is installed:
jorgebucaran/fisher
reitzig/sdkman-for-fish@v2.0.0
I keep the plugin list short on purpose. Most of what other people use plugins for, fish already does natively or I have a small function for.
That is it
If you want to copy any of this, it all lives in my
dotfiles repo under fish/.
This post was put together with help from an AI assistant.