My CLI World
When I was first starting out, I spent 60+% percent of my time ssh’ed into a remote server. So I refrained from heavily customizing my own shell because those customizations were difficult, if not impossible, to have consistently across all of my servers and situations.
Due to this habit it wasn’t until the last couple of years I realized… I don’t really ssh anymore!
Ok, to be clear, yes I still ssh into the occasional server but now 99% of my time in a shell is local on MY laptop.
So I can customize the hell out of it!
I’m going to walk you through a couple of workflows, some zsh bits, and some CLI tools I use daily. These do a few things for me:
- cuts down on my typing which is good for RSI
- makes things consistent between client projects
- reduces the friction speedbump of getting started
- encapsulates differences between projects so I don’t have to think about them
- helps me stay in flow
Together these save me hours of frustration and time every week.
My shell prompt
I have to admit, I’ve never been one to heavily customize my prompt and I still don’t do much, but I have installed and really like Starship. It’s fast as it’s written in Rust, but nicely customizable to show you useful information such as the Python version in use on a per-directory basis.

This is my prompt when working on this blog. It’s showing the directory we’re in which is frankwiles.com
it’s letting
me know we’re on the git branch main
and that we have untracked files. The next version number v0.0.1
isn’t useful
here because it’s the “version” of my AstroJS blog package itself which will forever remain there.
The next couple of versions however are somewhat useful. It’s not SUPER clear but that icon is a dumpling or bun so Starship is letting me know that I’m using bun version 1.2.7. and I have a little Typer based CLI tool I use to create new draft posts and other blog maintenance tasks and this virtual env happens to be on Python v3.11.1.
It’s a nice to have, but honestly the weakest recommenation I’ll be making in this post. If you’re happy with your prompt, skip this one.
Starting work on something…
The most frequently used, and thus most useful, workflow hack I have set up uses a combination of direnv and zoxide.
No matter if the next task for me is dev or ops related, my first step is to move into that project folder.
If my project is in ~/src/django-test-plus
I could obviously type cd ~/src/django-test-plus/
. zoxide helps me
track my common folders and fuzzy matches on them to quickly get me where I’m going. I just tested and I’m able to instead type z django-test-plus
or z django-test
and it takes me there.
Once I’m in the right place, direnv
does a few things for me:
- It sets specific environment variables I want, and most importantly unsets them when I leave this folder tree
- It allows me to adjust my path so I can have a
~/<project_dir>/bin
directory with specific tools and most impotantly specific VERSIONS of tools for this project - I can write my own zsh to handle specific things I want (see
layout_uv
later in this post)
This replaces a similar setup I had before which used virtualenv-wrapper
to do basically the same thing but wasted some disk space
with an empty venv sometimes and was a bit more cumbersome to set up so I didn’t do it 100% of the time.
For a dev project, I might be setting env vars to determine which S3 bucket some data is in. I might need to tie this project to a specific version of Tailwind CLI or and old version of some other tool.
With ops, I set up a specific ops folder per customer or customer environment. I can type z <customer>-ops
and a half dozen or more env vars are set to give me access to their AWS, GCP, or other cloud provider and with the specific CLI tool versions
I need for them.
This pattern ALSO gives me a spot to drop a Justfile
so I can curate a list of just recipes for common tasks for THIS client.
To show you an example, here is what happens when I move to my Politifact ops project:

This sets a env var to tell the gcloud
command which GCP project I’m using and sets a client specific KUBECONFIG
for their k8s
clusters. I’ve also got a special bin dir here to use a specific version of FluxCD’s CLi tool flux
.
Finding stuff…
We all have a ton of files. I just checked and in my ~/src
folder, where I keep most projects, and there are a little over 1 million individual files.
I use these three tools heavily when looking for stuff. The first, fd is a better version of find
.
Instead of having to type something like find . -name cli.py
you can type fd cli.py
. It’s faster and automatically ignores files in
your .gitignore
.
I also need to find stuff IN my files of code. I use a mixture of ripgrep and ack, but mostly I use ack.
ack foo
finds me all instances of the string foo
in all the files in this folder and below. I can constrain this to just
Python files with ack --python foo
.
I also have a shell command ackopen
which will open all of the matched files as individual
buffers in NeoVim below FYI.
I bet I type ack <something>
50+ times a day.
Shell History
I’ve recently switched to using Atuin to manage my shell history. It’s customizable and super fast. This allows me
to just type the beginning of a command and it searches through all my history for commands that begin with that which
cuts down on frequent things that aren’t quite frequent enough to make it into a Justfile
.
Here is a screenshot of hitting “up arrow” (well really esc k
since I rock vim mode in my shell):

If you stare at this closely you’ll see some of the commands I’ve had to type as I’ve written this post for example, like moving screenshots from my Desktop folder to this posts’ folder.
Custom Aliases
My most frequently used alias is r
, it moves me back to the root of THIS git repo. It’s simple but super useful:
# Go to root of this git repo
alias r='cd $(git root)'
UPDATE: So it turns out git root
doesn’t come with git, I’ve actually been getting it from git-extras
in homebrew,
and didn’t know it! It is just this:
git rev-parse --show-toplevel
Which you can make global with git config --global alias.root 'rev-parse --show-toplevel'
. Thanks to Adam Johnson and
Ned Batchelder for both pointing this out to me after I published this! Ok back to more aliases…
I also often want to look for files from the repo of this repo, so I have rd
(a pun of fd
) that does it for me:
# Search for files from the git root
function rd() {
fd "$1" $(git root)
}
As I mentioned above, I’m often looking for content in various source files so have ackopen
which uses ack to search
through files and then opens all of the matching files in NeoVim.
# Open files with ack
ackopen() {
nvim $(ack -l "$@")
}
One thing I like about ack
is that it listens to your .gitignore
, but sometimes that gets in the way because
what I’m actually looking for I also don’t want in my repo. Ack supports doing this, but I can’t be
bothered to memorize that so I have fullack
which remembers for me.
# Search all files regardless of .gitignore etc
fullack() {
ack --noenv $@
}
I also never rememember the right Finder steps to tell OSX that I’m ok with it executing this file I downloaded so I have:
appleallow() {
xattr -d com.apple.quarantine $@
}
Two other useful aliases, pulllog
and pulldiff
which help me see what I just pulled in with git which I wrote about
a couple of months ago.
I’m exclusively using uv
these days, so I added this to my ~/.config/direnv/direnvrc
to automatically activate it
if it exists or create it if it doesn’t yet exist:
layout_uv() {
if [[ -d "venv" ]]; then
VIRTUAL_ENV="$(pwd)/venv"
fi
if [[ -z $VIRTUAL_ENV || ! -d $VIRTUAL_ENV ]]; then
log_status "No virtual environment exists. Executing \`uv venv\` to create one."
uv venv venv
VIRTUAL_ENV="$(pwd)/venv"
fi
PATH_add "$VIRTUAL_ENV/bin"
export UV_ACTIVE=1 # or VENV_ACTIVE=1
export VIRTUAL_ENV
}
Now I just add layout_uv
to the top of my .envrc
files when I want this behavior, which is almost always FYI.
Your Mission
The main take away from this blog post should be the desire to heavily customize your environments.
Even if some of my little tools don’t resonate for your particular working style, they hopefully have sparked a idea that would simplify something for you. So go write it!
Go write that just
recipe you’ve been meaning to do.
Wrap that one tool’s cumbersome command line options with a little alias.
Craft a little shell function to handle that one situation you find yourself in once a week or month.
What common every day annoyance can you remove?
With AI you really have no excuse anymore.
Other Related Posts
- Why you should use be using just - shows how the just cli tool is incredbily useful
- Write yourself throw away tools - why writing little throw away tools is great especially if you have AI do it for you
- pulldiff and pulllog - a longer post on these two aliases mentioned above
- Customize direnv - Chris Rose has a good post on something I didn’t learn about for a long time. How easy it is to customize direnv to your specific needs.
Other Resources
- Tokei a better CLI tool for counting code lines by language
- dust a much better replacement for the
du
command

Frank Wiles
Founder of REVSYS and former President of the Django Software Foundation . Expert in building, scaling and maintaining complex web applications. Want to reach out? Contact me here or use the social links below.