I Replaced My Bash Aliases With a Justfile and It Stuck Dev Tools

I Replaced My Bash Aliases With a Justfile and It Stuck

by Joule P. Kraft · May 15, 2026

I had a problem most developers eventually have: my shell history was 60% remembering what flag to pass to which CLI in which repo. I had aliases for some of it. I had shell functions for some of it. I had a folder of scripts/ in every project. Half the scripts were undocumented. Half the aliases were in a dotfile I hadn’t looked at since 2021. Onboarding anyone to any of my projects involved a half-hour of “actually you have to do this first.”

I tried Makefiles for years. They never stuck. The syntax fights you. Tab vs. spaces. Implicit rules. Phony targets. Variables that don’t expand the way any sane person expects. Make is the right tool for compiling C. It is the wrong tool for “run my tests and then deploy.”

About a year ago I switched to just. I now have a justfile in every project I work on. The aliases are gone. The scripts folder is mostly gone. The bash functions are gone. Everything that used to be a one-off is now just <verb>. This is a writeup of how that happened, what it looks like, and why I think it’ll keep sticking.

What just Actually Is

just is a command runner. It is intentionally not a build system. The author calls it a “modern, handy way to save and run project-specific commands.” That description undersells it.

You write a justfile in your project root. It looks like this:

default:
    @just --list

test:
    bundle exec rspec

lint:
    bundle exec rubocop
    bundle exec erb_lint --lint-all

setup:
    bundle install
    yarn install
    bin/rails db:setup

deploy env="staging":
    git push {{env}} main

clean:
    rm -rf tmp/cache
    rm -rf node_modules/.cache

You then type just test, just lint, just deploy production. That’s it. That’s the entire model.

If you’ve used make you’ll notice this looks like a Makefile. It is intentionally similar. It is also intentionally different in all the ways make is painful: no tab requirement (spaces are fine), no implicit rules, no phony targets, no weird variable expansion. Recipes are just shell scripts with parameters.

Why It Stuck Where Makefiles Didn’t

There are five things just gets right that make gets wrong, and they’re the entire reason this stuck for me.

No tab fetish. make requires tabs for indentation. Editors fight you. Copy-paste from a webpage breaks. just accepts spaces or tabs. This sounds trivial. After a decade of make errors that turned out to be tab/space issues, it isn’t.

Recipe arguments are first-class. just deploy production passes production as a parameter to the deploy recipe. In make you do this through environment variables, target hackery, or a custom shell preamble. In just it’s a recipe argument with a default value. The syntax is deploy env="staging": and inside the recipe you reference {{env}}. Easy.

just --list shows you everything. Type just --list (or just just, if you set a default recipe to list) and you get a table of every recipe with its docstring. This is the killer feature. New developer joins the project? They run just --list and immediately see every operation. No grepping through scripts/. No reading the README. The justfile is the project documentation for “how do I do thing X.”

Recipes can be in any shell. By default recipes are shell scripts in sh. But you can declare a recipe’s shebang and write it in Python, Ruby, Node, whatever. A complicated recipe in make becomes an unreadable mess of escaped backslashes; in just it’s just a script with a shebang.

It’s standalone. just is a single Rust binary. No dependencies, no language runtime. Install via brew install just, cargo install just, or grab a release binary. Works the same on macOS and Linux. (Windows too, but I haven’t tested.)

My Actual Workflow

Here is the justfile from a recent project, lightly redacted:

# default: list available commands
default:
    @just --list

# install all dependencies
setup:
    bundle install
    yarn install
    bin/rails db:setup

# run the full test suite
test:
    bundle exec rspec
    yarn test

# run only the ruby tests, optionally filtered
test-ruby filter="":
    bundle exec rspec {{filter}}

# run all linters
lint:
    bundle exec rubocop
    bundle exec erb_lint --lint-all
    yarn lint

# autofix what can be autofixed
fix:
    bundle exec rubocop -a
    yarn lint --fix

# start a local dev server
dev:
    bin/dev

# tail the production logs
logs env="production":
    fly logs -a myapp-{{env}}

# deploy to fly
deploy env="staging":
    fly deploy -a myapp-{{env}}

# open a production rails console
console env="production":
    fly ssh console -a myapp-{{env}} -C "/app/bin/rails console"

# nuke caches
clean:
    rm -rf tmp/cache
    rm -rf node_modules/.cache
    rm -rf .bundle/cache

Every recipe has a comment above it. just --list displays those as docstrings:

Available recipes:
    default              # default: list available commands
    setup                # install all dependencies
    test                 # run the full test suite
    test-ruby filter=""  # run only the ruby tests, optionally filtered
    lint                 # run all linters
    fix                  # autofix what can be autofixed
    dev                  # start a local dev server
    logs env="production"# tail the production logs
    deploy env="staging" # deploy to fly
    console env="production" # open a production rails console
    clean                # nuke caches

That table is the entire project documentation for “operational commands.” It’s right there in the repo, version-controlled, and impossible to miss.

The Bash Aliases I Deleted

Here is the kind of stuff that used to live in my .bashrc:

alias rt="bundle exec rspec"
alias rl="bundle exec rubocop"
alias rd="bin/dev"
alias rdb="bin/rails db:migrate"
fn deploy_staging() {
  fly deploy -a myapp-staging
}
fn deploy_production() {
  fly deploy -a myapp-production
}

All of this is now in the justfile. The advantage is not that aliases are bad — they’re fine. The advantage is:

  1. Aliases are per-machine. When I sit down at a new computer, my aliases don’t exist. The justfile lives in the repo, so it’s available everywhere the repo is.
  2. Aliases are global. rt running rspec works fine when I’m in a Ruby project. When I’m in a Node project, rt does nothing useful. just test is contextual — it runs whatever testing makes sense for this project.
  3. Aliases are invisible. A new developer on my project cannot see my aliases. They can see my justfile. The aliases were a private API; the justfile is a public one.
  4. Aliases can’t take arguments cleanly. Bash functions can, but the syntax is ugly. just deploy production is more readable than a deploy() function with positional args.

The aliases that survived are the ones that genuinely belong to my shell: gco for git checkout, gcm for git commit -m, the like. Things that aren’t project-specific. The project-specific stuff is all in justfiles now.

The scripts/ Folder

The other thing the justfile killed: my scripts/ folder. I used to have:

scripts/
├── deploy.sh
├── backup-db.sh
├── seed-staging.sh
├── reset-local.sh
└── check-prod-health.sh

Each one was a shell script. Half were undocumented. Most had to be run with specific flags I had to remember.

These all became justfile recipes. The shell scripts are gone. If a recipe got long enough to deserve its own file, I’d put it in scripts/ and call it from the justfile — but in practice, almost everything fits inline.

The justfile is the single entry point. just --list shows everything. There is one place to look.

The Few Gotchas

Three things to know going in:

Recipe variables use {{var}} syntax. This is Jinja-like, not shell-like. Inside a recipe body, {{env}} becomes the value; $env does not. This trips people up exactly once.

Each line of a recipe is its own shell invocation by default. This means cd somewhere followed by another command does not run the second command in somewhere. Fix: either use && to chain, or add set shell := ["bash", "-cu"] and use a multi-line recipe with #!/usr/bin/env bash shebang to make the whole recipe one script.

The default recipe runs when you type just with no args. I always set this to @just --list so a bare just shows the help. Otherwise it runs the first recipe, which is rarely what you want.

When To Reach For Something Else

just is a command runner. It is not a build system. If you need real build-graph dependency tracking — “rebuild this only if these inputs changed” — just is not it. Use make, ninja, or your language’s actual build tool. just doesn’t track file mtimes.

For everything else — running tests, deploying, setting up environments, tailing logs, opening consoles, autofixing lint, running migrations — just is the right tool.

The Bottom Line

A year in, I have a justfile in every project. Every machine I work on has just installed via brew. Every new developer I onboard runs just --list first and is productive ten minutes later.

The pitch is small: it’s just a tidier way to organize the commands you already run. But the second-order effect is large: my projects now self-document their operational surface. The aliases are gone. The mystery scripts are gone. The README section called “Common Commands” is gone too, because just --list replaced it.

If you’ve been meaning to clean up your shell aliases and project scripts, this is the cleanup. Install just. Make one justfile. See if it sticks.

Mine did.