TL;DR:
- Just is a command runner written in Rust: install it with
cargo install justorbrew install just, create ajustfile, and run tasks withjust <taskname> - Unlike Make, Just doesn’t have implicit rules or tab-sensitivity issues, supports parameters and environment variables cleanly, and gives clear errors when something goes wrong
- If you’re using a Makefile only to store common commands (not for actual build dependency tracking), Just is a straightforward upgrade with no new concepts to learn
Most projects accumulate a collection of commands that everyone on the team needs to run but nobody wants to memorise. Start the database. Run migrations. Start the dev server with these specific environment variables. Run the linter. Deploy to staging. These commands end up in a Makefile, a scripts/ folder, a README no one reads, or scattered across five different people’s .zshrc files.
Just solves this problem cleanly.
What Just Does
Just is a command runner — not a build system. It doesn’t try to track file dependencies or decide whether something needs rebuilding. It just runs the commands you tell it to run. This sounds like a limitation but is actually liberating: the tool is simpler, the mental model is clear, and it does exactly what you expect.
A basic justfile:
# Start everything needed for development
dev:
docker-compose up -d
pnpm install
pnpm dev
# Run the full test suite
test:
pnpm lint
pnpm test
# Run just the unit tests
unit:
pnpm test --run
# Deploy to the staging environment
deploy-staging:
pnpm build
wrangler deploy --env staging
Run tasks with just dev, just test, just deploy-staging. Run just with no arguments to see all available tasks and their docstrings.
Why Not Just Use Make?
Make is installed everywhere and works, so why switch? A few reasons:
Tab sensitivity: Make requires tabs, not spaces, for command indentation. This trips up practically every person who writes a Makefile infrequently. Just uses consistent, readable indentation.
Automatic .PHONY: In Make, if a file called test exists in your directory, make test won’t run your test recipe — it’ll think the target is already built. You have to declare tasks as .PHONY explicitly. Just doesn’t have this problem.
Recipes run in subshells by default: Make runs each line of a recipe in a separate shell. If you cd into a directory on one line, you’re back at the original directory on the next line. This surprises everyone at least once. Just runs each recipe in a single shell, so state persists across lines.
No implicit rules: Make has a large set of built-in rules for compiling C files, building object files, and so on. These sometimes interfere in unexpected ways. Just has no implicit rules — it only does what you wrote.
Better errors: When Just can’t find a recipe, can’t run a command, or encounters a missing variable, the error message tells you what happened and where.
Parameters and Variables
Just supports recipe parameters and variables, which Make handles awkwardly:
# Variables with defaults
db_url := env_var_or_default("DATABASE_URL", "postgres://localhost/myapp")
# Recipe with a parameter
migrate target="up":
diesel migration {{target}} --database-url {{db_url}}
# Run with default: just migrate
# Run with argument: just migrate down
Environment variables from a .env file in the project root are loaded automatically if you add set dotenv-load to your justfile. No extra tooling needed.
set dotenv-load
start:
echo "Starting with API_KEY=$API_KEY"
./server
Cross-Platform Support
Just works on macOS, Linux, and Windows. If your team includes Windows developers, this matters. Makefiles on Windows require either WSL or a separate Make installation, and shell commands often need adjustment. Just handles Windows natively and uses the appropriate shell automatically.
For recipes that need specific shell behaviour, you can set the interpreter per-recipe:
# Force bash for a specific recipe
@bash
deploy:
#!/usr/bin/env bash
set -euo pipefail
echo "Deploying..."
./scripts/deploy.sh
Listing and Documenting Recipes
One of the nicest Just features is automatic documentation. Add a comment above a recipe and it becomes its description:
# Build the production bundle
build:
pnpm build
# Run database migrations (use: just migrate [up|down])
migrate target="up":
./scripts/migrate.sh {{target}}
# Wipe the local database and reseed
reset-db:
./scripts/reset.sh
Running just --list shows all recipes with their descriptions:
Available recipes:
build # Build the production bundle
migrate # Run database migrations (use: just migrate [up|down])
reset-db # Wipe the local database and reseed
This is genuinely useful for onboarding: new team members run just --list and immediately see what commands exist.
Conditional Execution and Dependencies
Just supports basic recipe dependencies (recipes that run before the current one) and conditional execution:
# clean runs before build
build: clean
pnpm build
clean:
rm -rf dist/
# Only run if a variable is set
check-env:
#!/usr/bin/env bash
[[ -n "$API_KEY" ]] || { echo "API_KEY not set"; exit 1; }
Integrating With Your Existing Tooling
Just doesn’t replace your existing tools — it wraps them. Your just test recipe might call pytest or vitest. Your just deploy might call terraform and ansible. Just is the consistent entry point; the actual work still happens in the tools you already use.
Add the justfile to version control and make it the canonical reference for how to run common project commands. Your README can then say “see just --list for available commands” instead of a long manual section that goes out of date.
The comparison to alternatives like Taskfile (YAML-based, also popular) is mostly personal preference. Just has a slightly terser syntax and handles edge cases like recipe parameters more naturally. Both are substantial improvements over Makefiles for non-build-system use cases.
If your project already has a Makefile full of phony targets, the migration takes about twenty minutes: install Just, copy your recipes across (adjusting tab indentation to spaces), test them, and commit the justfile. Most teams don’t look back.