Life in the Shell
Over time, I've built up a collection of tools which make my life easier in the shell. I'm constantly exploring and trying new tools, most of which get ditched fairly rapidly, but there are some which have survived the perpetual cull.
I'd like to share a few of them with you. You won't find all of these useful in the same way that I do, but there might be some which are new to you and have some value.
Which shell?
Many of the tools I'll mention are shell-agnostic, meaning that they don't care which shell you're using. The shell functions later in this piece are intended for fish and in most cases will not work directly in bash or zsh.
I am a big proponent of fish. It's not nothing that the syntax differs from what you might be more familiar with, but I like the changes.
One example is the following -
bash / zsh
while true
do
echo "hello"
done
if true
then
echo "hello"
fi
export FOO=BAR
export PATH=$PATH:/path/to/bin
fish
while true
echo "hello" # note lack of 'do/done'
end
if true
echo "hello" # note lack of 'do/done'
end
set -Ux FOO BAR # 'universal', persists forever
# saved in ~/.config/fish/fish_variables
# editing this file by hand is allowed
# changes are reflected immediately
set -gx FOO BAR # 'global', persists only in this shell
# session
set -lx FOO BAR # 'local', persists only in this shell
# session, with only local scope
# remove -x from any of the above `set` commands to not export an env
# variable, only make the variable available to fish
fish_add_path /path/to/bin # edits $fish_user_paths, persists forever (scope U)
# equivalent to set -Uax fish_user_paths /path/to/bin
# fish variables can be arrays, -a appends to it
There is of course more, but this is just an example of how the syntax feels more mature.
Autosuggestions and syntax highlighting come out of the box on fish, whereas you need a third-party plugin on bash or zsh.
I'll admit that if you disregard the syntax changes, you can implement pretty much anything you get on fish with zsh, using plugins. I can understand using either. I don't see any compelling reason to ever use bash.
Prompt
I use tide and adore it. It's beautiful and asynchronous so it won't slow you down.

It will be installed in the list of plugins below, starting its config routine when you install the plugins with fisher update. See below.
Shell Plugins
The fisher plugin manager is a must-have. It does what it does quietly and without any hassle.
Here is my ~/.config/fish/fish_plugins —
# Plugin manager
jorgebucaran/fisher # Plugin manager. Allows it to self-update.
# Plugins
franciscolourenco/done # Notify when long processes finish
gazorby/fish-abbreviation-tips # Remind about available abbreviations
gazorby/fish-git-emojis # Aliases to use Gitmoji and Conventional Commits
ilancosman/tide@v6 # Absolutely amazing fish prompt
jorgebucaran/replay.fish # Run Bash commands replaying changes in Fish
jorgebucaran/spark.fish # Generate sparklines in Fish
joseluisq/gitnow # Git aliases and keybindings
laughedelic/pisces # Match pairs of characters automatically
meaningful-ooo/sponge # Removes commands that fail from your history
nickeb96/puffer-fish # Speed up directory navigation
oh-my-fish/plugin-foreign-env # Wrap bash/zsh syntax into fish
oh-my-fish/plugin-osx # macOS utility commands
wfxr/forgit # Git utilities - requires fzf!
When you've installed fisher and created this file, just run fisher update to get set up. If you've added tide then its configuration routine will also start at this point.
Shell Functions
These are a few functions I use for convenience. They might be useful, or they might not, but here they are anyway.
Make fzf pretty
function fzf --wraps="fzf"
set -Ux FZF_DEFAULT_OPTS "
--color=fg:#908caa,bg:#191724,hl:#ebbcba
--color=fg+:#e0def4,bg+:#26233a,hl+:#ebbcba
--color=border:#403d52,header:#31748f,gutter:#191724
--color=spinner:#f6c177,info:#9ccfd8
--color=pointer:#c4a7e7,marker:#eb6f92,prompt:#908caa
"
command fzf
end
Get a GitHub authentication token
function github-auth
set -gx GITHUB_TOKEN (gh auth token)
end
Clean up opencommit when it hangs
Much as I love opencommit, if the change is too big, it will hang instead of realising it's over the token limit and failing. I use this function to clean it up when that happens.
function kill-oco
echo "Searching for Node.js processes containing 'oco'..."
set pids (ps aux | grep -i "[n]odejs.*oco\|[n]ode.*oco" | awk '{print $2}')
if test (count $pids) -eq 0
echo "No matching 'oco' processes found."
return 0
end
echo Found (count $pids) "process(es) to kill:"
for pid in $pids
set process_info (ps -p $pid -o command= | string sub -l 50)
echo "PID $pid: $process_info..."
end
read -l -P "Kill these processes? [y/N] " confirm
if test "$confirm" = y -o "$confirm" = Y
for pid in $pids
echo "Killing process $pid..."
kill -9 $pid
# Check if process was successfully killed
if kill -0 $pid 2>/dev/null
echo "Failed to kill process $pid!"
else
echo "Process $pid successfully terminated."
end
end
echo "All matching 'oco' processes have been terminated."
else
echo "Operation canceled. No processes were killed."
end
end
Wipe all workflow runs for a GitHub repository
function wipe-workflows -d "Wipe all workflow runs for a GitHub repository"
set -lx REPONAME $argv[1]
echo "Wiping all workflow runs for $REPONAME..."
for i in (gh api --paginate "/repos/$REPONAME/actions/runs" | jq '.workflow_runs.[].id')
echo $i
gh api --silent -X DELETE /repos/$REPONAME/actions/runs/$i
end
end
Fetch and pull all git repositories in a directory
function yank-all
for i in *
echo "$i"
cd $i
echo fetch
git fetch --all --tags --prune --jobs=8 --recurse-submodules=yes
echo pull
git pull --stat --tags --prune --jobs=8 --recurse-submodules=yes
echo out
cd ..
end
end
Queue PRs with trunk
function queue-prs
for i in *
cd $i
gh pr list | awk '{print $1}' | while read line
trunk merge $line
end
cd ..
end
end
Shell Theme
I change my theme all the time.
Currently I'm a fan of Monokai, using the excellent Monokai Pro in my editor.
This sets fish up to match.
# monokai theme
set fish_color_command F92672 # the color for commands
set fish_color_comment 75715E # the color used for code comments
set fish_color_cwd 66D9EF # the color used for the current working directory in the default prompt
set fish_color_end F8F8F2 # the color for process separators like ';' and '&'
set fish_color_error F8F8F2 --background=F92672 # the color used to highlight potential errors
set fish_color_escape 66D9EF # the color used to highlight character escapes like '\n' and '\x70'
set fish_color_match F8F8F2 # the color used to highlight matching parenthesis
set fish_color_normal F8F8F2 # the default color
set fish_color_operator AE81FF # the color for parameter expansion operators like '*' and '~'
set fish_color_param A6E22E # the color for regular command parameters
set fish_color_quote E6DB74 # the color for quoted blocks of text
set fish_color_redirection AE81FF # the color for IO redirections
set fish_color_search_match --background=49483E # the color used to highlight history search matches
set fish_pager_color_completion 75715E # the color of the completion itself
set fish_pager_color_description 49483E # the color of the completion description
set fish_pager_color_prefix F8F8F2 # the color of the prefix string, i.e. the string that is to be completed
set fish_pager_color_progress F8F8F2 # the color of the progress bar at the bottom left corner
set fish_pager_color_secondary F8F8F2 # the background color of the every second completion
Standalone Utilities
From here on, we're dealing with stuff that doesn't really care which shell you use. If they have shell integration, generally it works for bash, zsh, and fish.
They're sorted alphabetically rather than in any meaningful order.
atuin
Oh, atuin, how I love thee. Its primary purpose is to sync your shell history between machines, and it does that very well. It also offers a simple synced key-value store and dotfile and script management, but I don't use those.
bat
It's cat, but pretty! Smart enough to detect whether you're trying to output to a terminal, and add syntax highlighting, line numbers, and file headers, or whether it's being used in a pipe in which case it operates exactly the same as cat.
chezmoi
The One True Dotfile Manager. Provides a bulletproof system for syncing dotfiles to a Git repository, and thus syncing them between machines. Supports various forms of encryption for sensitive dotfiles, including sops and age.
direnv
Simple environment management. Applies variable changes defined in .env files and removes them when you leave the directory in question. Simple, bulletproof, immensely useful. Plays nice with fish.
eza
Replacement for ls. You can pretty much alias ls to it and never look back. Faster, more flexible, better featured.
fzf
A very important building block for other tools. Provides a user interface to select options from a list - that's all. It can be - and is - used in interesting ways by other tools. Should be installed even if you never invoke it by hand.
lefthook
One of many Git hook managers, this one is just my choice. If I'm not using trunk in a repository (which has its own hook management) this does the job nicely.
mise
Absolutely key to my workflow. Version manager, environment manager, task runner. Using mise and chezmoi makes it really easy to sync my toolset between machines. It can handle language-specific packaging systems, as well as ubi to simply fetch GitHub release binaries.
opencommit
I don't know the last time I actually wrote a Git commit message. Uses an AI model to generate your commit message and pretty much always gets it right. Supports Gitmoji and Conventional Commits.
pik
Simple process killer. When you just want to nuke something and don't want to think about it too much, pik is here to help.
ripgrep
It's grep but specifically designed for codebases, and much faster because of it. Ignores things that you probably don't want to be searching.
shadowenv
When direnv is too simple, there's shadowenv. Uses a LISP config file and you can do some pretty interesting things with its logic.
tlm
Same idea as ai-shell but using a locally-running model. Uses ollama to manage models, and allows you to turn free text into shell commands.
xc
Task runner, notable for using README.md as its configuration file so that your documentation becomes your task runner config. I use mise for task management much more commonly, but generally I include the task specifications in xc's format so it can be used too.
zellij
We all love tmux, but zellij is tmux on steroids. More batteries included, easier to configure, generally more flexible.
zoxide
Directory jumper. Keeps track of all the directories you enter. When you want to go back into a directory, execute z SEARCHTEXT and you'll cd into the most recently used directory matching SEARCHTEXT.
More
There are so many more utilities I use, but I'm going to leave that for another post. If you want to skip the wait and explore yourself, here's my mise config file which you can use to install pretty much everything I use.
I've split it into two sections because Notion disables syntax highlighting over 10k characters. They should be combined into ~/.config/mise/config.toml.
Mise settings
[env]
EDITOR = 'code --wait'
MISE_ACTIVE = 'true'
[settings]
activate_aggressive = true
all_compile = true
auto_install = true
color = true
env_file = '.env'
experimental = true
gix = true
jobs = 4
libgit2 = true
not_found_auto_install = true
profile = 'main'
shorthands_file = '~/.config/mise/shorthands.toml'
status = { show_env = true, show_tools = true, missing_tools = 'always' }
task_run_auto_install = true
use_versions_host = true
verbose = false
[settings.aqua]
cosign = true
minisign = true
slsa = true
[settings.cargo]
binstall = true
[settings.node]
gpg_verify = true
[settings.npm]
bun = true
[settings.pipx]
uvx = true
[settings.python]
# uv_venv_auto = true
venv_auto_create = true
[settings.sops]
age_key_file = '~/.sops.key'
rops = true
[settings.swift]
gpg_verify = true
Mise tools
[tools]
'aqua:arl/gitmux' = { version = 'latest' }
'aqua:atuinsh/atuin' = { version = 'latest' }
'aqua:jacek-kurlit/pik' = { version = 'latest' }
'aqua:WebAssembly/binaryen' = { version = 'latest' }
'cargo:bat' = { version = 'latest' }
'cargo:binsider' = { version = 'latest' }
'cargo:cargo-cache' = { version = 'latest' }
'cargo:cargo-edit' = { version = 'latest' }
'cargo:cargo-generate' = { version = 'latest' }
'cargo:cargo-outdated' = { version = 'latest' }
'cargo:cargo-release' = { version = 'latest' }
'cargo:cargo-tree' = { version = 'latest' }
'cargo:cargo-update' = { version = 'latest' }
'cargo:cargo-watch' = { version = 'latest' }
'cargo:ducker' = { version = 'latest' }
'cargo:fast-conventional' = { version = 'latest' }
'cargo:fd-find' = { version = 'latest' }
'cargo:git-absorb' = { version = 'latest' }
'cargo:git-brws' = { version = 'latest' }
'cargo:git-delta' = { version = 'latest' }
'cargo:gitnr' = { version = 'latest' }
'cargo:gitoxide' = { version = 'latest' }
'cargo:gitui' = { version = 'latest' }
'cargo:hgrep' = { version = 'latest' }
'cargo:hyperfine' = { version = 'latest' }
'cargo:just' = { version = 'latest' }
'cargo:localtunnel' = { version = 'latest' }
'cargo:mdbook' = { version = 'latest' }
'cargo:openapi-tui' = { version = 'latest' }
'cargo:rage' = { version = 'latest' }
'cargo:ripgrep' = { version = 'latest' }
'cargo:rops-cli' = { version = 'latest' }
'cargo:sd' = { version = 'latest' }
'cargo:tokei' = { version = 'latest' }
'cargo:wasm-bindgen-cli' = { version = 'latest' }
'cargo:xsv' = { version = 'latest' }
'dotnet:dotnet-dump' = { version = 'latest' }
'dotnet:Microsoft.CST.DevSkim.CLI' = { version = 'latest' }
'gem:bundler' = { version = 'latest' }
'gem:cocoapods' = { version = 'latest' }
'gem:foreman' = { version = 'latest' }
'gem:httparty' = { version = 'latest' }
'gem:notion-task' = { version = 'latest' }
'gem:rails' = { version = 'latest' }
'gem:rubocop' = { version = 'latest' }
'gem:rubocop-rspec' = { version = 'latest' }
'gem:rubyfmt' = { version = 'latest' }
'gem:solargraph' = { version = 'latest' }
'gem:standard' = { version = 'latest' }
'gem:syntax_tree' = { version = 'latest' }
'gem:yard' = { version = 'latest' }
'go:github.com/adleong/tapshark' = { version = 'latest' }
'go:github.com/caddyserver/xcaddy/cmd/xcaddy' = { version = 'latest' }
'go:github.com/evilmartians/lefthook' = { version = 'latest' }
'go:github.com/go-acme/lego/v4/cmd/lego' = { version = 'latest' }
'go:github.com/google/gops' = { version = 'latest' }
'go:github.com/goreleaser/goreleaser/v2' = { version = 'latest' }
'go:github.com/jesseduffield/lazygit' = { version = 'latest' }
'go:github.com/maaslalani/nap' = { version = 'latest' }
'go:github.com/nsf/gocode' = { version = 'latest' }
'go:github.com/schollz/croc/v10' = { version = 'latest' }
'go:github.com/sigstore/cosign/v2/cmd/cosign' = { version = 'latest' }
'go:github.com/theupdateframework/go-tuf/cmd/tuf-client' = { version = 'latest' }
'go:github.com/UpCloudLtd/upcloud-cli/v3/...' = { version = 'latest' }
'go:github.com/yusufcanb/tlm' = { version = 'latest' }
'go:sigs.k8s.io/kind' = { version = 'latest' }
'npm:@anthropic-ai/claude-code' = { version = 'latest' }
'npm:@builder.io/ai-shell' = { version = 'latest' }
'npm:@johnlindquist/file-forge' = { version = 'latest' }
'npm:@prettier/plugin-php' = { version = 'latest' }
'npm:@prettier/plugin-pug' = { version = 'latest' }
'npm:@prettier/plugin-ruby' = { version = 'latest' }
'npm:@prettier/plugin-xml' = { version = 'latest' }
'npm:degit' = { version = 'latest' }
'npm:firebase-tools' = { version = 'latest' }
'npm:fish-lsp' = { version = 'latest' }
'npm:genaiscript' = { version = 'latest' }
'npm:http-server' = { version = 'latest' }
'npm:husky' = { version = 'latest' }
'npm:localtunnel' = { version = 'latest' }
'npm:opencommit' = { version = 'latest' }
'npm:prettier' = { version = 'latest' }
'npm:prettier-plugin-astro' = { version = 'latest' }
'npm:prettier-plugin-erb' = { version = 'latest' }
'npm:prettier-plugin-go-template' = { version = 'latest' }
'npm:prettier-plugin-java' = { version = 'latest' }
'npm:prettier-plugin-jinja-template' = { version = 'latest' }
'npm:prettier-plugin-nginx' = { version = 'latest' }
'npm:prettier-plugin-properties' = { version = 'latest' }
'npm:prettier-plugin-rust' = { version = 'latest' }
'npm:prettier-plugin-sh' = { version = 'latest' }
'npm:prettier-plugin-svelte' = { version = 'latest' }
'npm:prettier-plugin-toml' = { version = 'latest' }
'npm:vsce' = { version = 'latest' }
'npm:wrangler' = { version = 'latest' }
'pipx:aerleon' = { version = 'latest' }
'pipx:asitop' = { version = 'latest' }
'pipx:autopep8' = { version = 'latest' }
'pipx:black' = { version = 'latest' }
'pipx:bpython' = { version = 'latest', uvx_args = '--with urwid' }
'pipx:bpytop' = { version = 'latest' }
'pipx:braindrop' = { version = 'latest' }
'pipx:flake8' = { version = 'latest' }
'pipx:git+https://github.com/itsnexn/raindropcli.git' = { version = 'latest', extras = 'requests' }
'pipx:httpie' = { version = 'latest' }
'pipx:ipython' = { version = 'latest' }
'pipx:isort' = { version = 'latest' }
'pipx:jc' = { version = 'latest' }
'pipx:jupyterlab' = { version = 'latest', uvx_args = '--with PySide6 --with jupyter-pieces --with jupyter_base16_theme --with jupyterlab-horizon-theme --with matplotlib --with scipy' }
'pipx:legit' = { version = 'latest' }
'pipx:markitdown' = { version = 'latest' }
'pipx:mypy' = { version = 'latest' }
'pipx:nyx' = { version = 'latest' }
'pipx:pelican' = { version = 'latest' }
'pipx:pipenv' = { version = 'latest' }
'pipx:pre-commit' = { version = 'latest' }
'pipx:pygi' = { version = 'latest' }
'pipx:pylint' = { version = 'latest' }
'pipx:pyoxidizer' = { version = 'latest' }
'pipx:pyright' = { version = 'latest' }
'pipx:remarshal' = { version = 'latest' }
'pipx:rmate' = { version = 'latest' }
'pipx:sherlock-project' = { version = 'latest' }
'pipx:showcert' = { version = 'latest' }
'pipx:shyaml' = { version = 'latest' }
'pipx:sourcery' = { version = 'latest' }
'pipx:sshclick' = { version = 'latest' }
'pipx:thefuck' = { version = 'latest' }
'pipx:tidal-dl-ng' = { version = 'latest', extras = 'gui' }
'pipx:toml-sort' = { version = 'latest' }
'pipx:virtualfish' = { version = 'latest' }
'pipx:xonsh' = { version = 'latest' }
'pipx:yamale' = { version = 'latest' }
'pipx:yamllint' = { version = 'latest' }
'pipx:yt-dlp' = { version = 'latest' }
'ubi:danielfoehrKn/kubeswitch' = { version = 'latest', exe = 'switcher' }
'ubi:gitkraken/gk-cli' = { version = 'latest', exe = 'gk' }
'ubi:marwanhawari/ppath' = { version = 'latest', exe = 'ppath' }
'ubi:Shopify/shadowenv' = { version = 'latest', exe = 'shadowenv' }
'ubi:termkit/gama' = { version = 'latest', exe = 'gama' }
'ubi:unfrl/dug' = { version = 'latest', exe = 'dug' }
'ubi:wader/fq' = { version = 'latest', exe = 'fq' }
act = { version = 'latest' }
age = { version = 'latest' }
aqua = { version = 'latest' }
bun = { version = 'latest' }
caddy = { version = 'latest' }
cargo-binstall = { version = 'latest' }
checkov = { version = 'latest' }
chezmoi = { version = 'latest' }
cocoapods = { version = 'latest' }
cosign = { version = 'latest' }
crystal = { version = 'latest' }
deno = { version = 'latest' }
direnv = { version = 'latest' }
dotnet = { version = 'latest' }
draft = { version = 'latest' }
envsubst = { version = 'latest' }
esc = { version = 'latest' }
eza = { version = 'latest' }
flutter = { version = 'latest' }
fzf = { version = 'latest' }
gcloud = { version = 'latest' }
gh = { version = 'latest' }
glooctl = { version = 'latest' }
go = { version = 'latest' }
helix = { version = 'latest' }
helm = { version = 'latest' }
hk = { version = 'latest' }
k9s = { version = 'latest' }
krew = { version = 'latest' }
kubectl = { version = 'latest' }
lazydocker = { version = 'latest' }
linkerd = { version = 'latest' }
lua = { version = 'latest' }
minikube = { version = 'latest' }
minisign = { version = 'latest' }
neovim = { version = 'latest' }
node = { version = 'latest' }
npm = { version = 'latest' }
perl = { version = 'latest' }
pipx = { version = 'latest' }
pkl = { version = 'latest' }
pnpm = { version = 'latest' }
poetry = { version = 'latest' }
pulumi = { version = 'latest' }
python = { version = 'latest' }
redis = { version = 'latest' }
ruby = { version = 'latest' }
rust = { version = 'latest', profile = 'default', targets = 'wasm32-wasip1,wasm32-wasip1-threads,wasm32-wasip2,wasm32-unknown-emscripten,wasm32-unknown-unknown', components = 'rustc,cargo,rustfmt,rust-std,rust-docs,rust-analyzer,clippy,rust-src,llvm-tools,rustc-dev' }
slsa-verifier = { version = 'latest' }
snyk = { version = 'latest' }
sops = { version = 'latest' }
terraform = { version = 'latest' }
tflint = { version = 'latest' }
tfsec = { version = 'latest' }
tmux = { version = 'latest' }
uv = { version = 'latest' }
vultr = { version = 'latest' }
watchexec = { version = 'latest' }
websocat = { version = 'latest' }
wtfutil = { version = 'latest' }
xc = { version = 'latest' }
xh = { version = 'latest' }
yarn = { version = 'latest' }
yq = { version = 'latest' }
zellij = { version = 'latest' }
zig = { version = 'latest' }
zls = { version = 'latest' }
zoxide = { version = 'latest' }