← All articles
TERMINAL just: A Modern Command Runner That Makes Makefiles O... 2026-02-14 · 5 min read · just · command-runner · make

just: A Modern Command Runner That Makes Makefiles Obsolete

Terminal 2026-02-14 · 5 min read just command-runner make task-automation terminal developer-productivity

just: A Modern Command Runner That Makes Makefiles Obsolete

Every project accumulates a set of commands that developers run repeatedly: build, test, lint, deploy, start the dev server, run database migrations. Make has been the traditional tool for wrapping these into named targets, but Make was designed to compile C programs in 1976. It uses tabs as syntax, has confusing automatic variables like $@ and $<, and silently swallows errors in ways that waste hours of debugging.

just command runner

just is a command runner that keeps the good parts of Make (named recipes in a file, dependency declarations, running from the project root) and throws away the bad parts (tab sensitivity, implicit rules, file-based targets). It is not a build system. It is a command runner, and it does that one job well.

Installation

# macOS
brew install just

# Fedora
sudo dnf install just

# Ubuntu/Debian
sudo apt install just

# Arch
pacman -S just

# Cargo
cargo install just

# Via mise
mise use just@latest

# Via npm (for JavaScript projects)
npx --yes @anthropic/just

Basic Usage

Create a file called justfile (no extension) in your project root:

# Run the development server
dev:
    npm run dev

# Run all tests
test:
    npm test

# Lint and format
lint:
    npx biome check --write .

# Build for production
build:
    npm run build

# Deploy to production
deploy: build
    rsync -avz dist/ server:/var/www/app/

Run a recipe:

just dev          # runs the 'dev' recipe
just test         # runs 'test'
just deploy       # runs 'build' first (dependency), then 'deploy'
just              # runs the first recipe (dev) by default
just --list       # shows all available recipes with descriptions

The comments above each recipe become descriptions in just --list. This turns your justfile into self-documenting project automation.

Why just Over Make

No Tab Sensitivity

Make requires tabs for indentation. Spaces silently break things. just uses consistent 4-space indentation (or tabs -- your choice). This alone prevents a class of frustrating invisible bugs.

No File-Based Targets

Make tracks file modification times to decide whether to rebuild. This is powerful for C compilation but meaningless for running tests or starting servers. In Make, you end up with .PHONY declarations everywhere. just has no concept of file targets -- every recipe is always a command to run.

Better Error Messages

When something goes wrong in Make, the error message often references internal Make concepts. just gives clear errors that point to the line in your justfile:

error: Unknown recipe `depoly`
Did you mean `deploy`?

Variables and String Interpolation

version := "1.2.0"
registry := "ghcr.io/myorg"

# Build and tag a Docker image
docker-build:
    docker build -t {{registry}}/myapp:{{version}} .

# Push the image
docker-push: docker-build
    docker push {{registry}}/myapp:{{version}}

Variables use {{}} interpolation, which is visually distinct from shell variables ($VAR). In Make, the interaction between Make variables and shell variables is a constant source of confusion.

Practical Patterns

Recipes with Arguments

# Run a specific test file
test file:
    pytest {{file}} -v

# Deploy to a specific environment
deploy env="staging":
    ./scripts/deploy.sh {{env}}

# Create a new migration
migration name:
    alembic revision --autogenerate -m "{{name}}"

Usage:

just test tests/test_api.py
just deploy production
just deploy              # uses default: staging
just migration "add users table"

Environment Variables

# Load from .env file
set dotenv-load

# Set for all recipes
export DATABASE_URL := "postgres://localhost/myapp"

# Set per-recipe
test:
    DATABASE_URL="postgres://localhost/myapp_test" pytest

The set dotenv-load directive reads your .env file automatically, making just aware of the same environment your application uses.

Conditional Recipes

# OS-specific commands
install:
    #!/usr/bin/env bash
    if [[ "$(uname)" == "Darwin" ]]; then
        brew install dependencies
    elif [[ -f /etc/fedora-release ]]; then
        sudo dnf install dependencies
    else
        sudo apt install dependencies
    fi

Multi-Line Recipes with Shebang

By default, just runs each line of a recipe as a separate shell command. For multi-line scripts, use a shebang:

# Generate a release
release version:
    #!/usr/bin/env bash
    set -euo pipefail
    echo "Releasing version {{version}}"
    git tag -a "v{{version}}" -m "Release {{version}}"
    git push origin "v{{version}}"
    gh release create "v{{version}}" --generate-notes

The shebang approach means the entire recipe runs as one script, so variables, conditionals, and loops work naturally.

Recipe Dependencies

# Dependencies run before the recipe
deploy: test lint build
    ./deploy.sh

# Dependencies can also pass arguments
ci: (test "unit") (test "integration") lint build

test suite="all":
    pytest -m {{suite}}

Private Recipes

Prefix a recipe with _ to hide it from just --list:

# Public recipe
build: _clean _compile _bundle
    echo "Build complete"

# Private helpers
_clean:
    rm -rf dist/

_compile:
    tsc

_bundle:
    esbuild src/index.ts --bundle --outdir=dist

Confirming Dangerous Commands

# Require confirmation before running
[confirm("Are you sure you want to reset the database?")]
db-reset:
    dropdb myapp && createdb myapp && alembic upgrade head

Project-Specific justfiles

Python Project

set dotenv-load

# Run the development server
dev:
    uv run uvicorn app.main:app --reload

# Run tests with coverage
test:
    uv run pytest --cov=app --cov-report=term-missing

# Lint and format
lint:
    uv run ruff check --fix .
    uv run ruff format .

# Type check
typecheck:
    uv run mypy app/

# Run all checks (CI equivalent)
check: lint typecheck test

# Database migrations
migrate:
    uv run alembic upgrade head

migration name:
    uv run alembic revision --autogenerate -m "{{name}}"

TypeScript/Node Project

# Start dev server
dev:
    bun run dev

# Run tests
test:
    bun test

# Lint and format
lint:
    bunx biome check --write .

# Build
build:
    bun run build

# Database
db-push:
    bunx drizzle-kit push

db-studio:
    bunx drizzle-kit studio

# Full CI check
ci: lint test build

Docker Project

registry := "ghcr.io/myorg"
image := "myapp"

# Build the image
build tag="latest":
    docker build -t {{registry}}/{{image}}:{{tag}} .

# Run locally
run: build
    docker compose up

# Push to registry
push tag="latest": (build tag)
    docker push {{registry}}/{{image}}:{{tag}}

# Shell into the container
shell:
    docker compose exec app /bin/bash

Settings

just has global settings that go at the top of your justfile:

# Use bash with strict mode
set shell := ["bash", "-euo", "pipefail", "-c"]

# Load .env files
set dotenv-load

# Allow recipes to fail without stopping
set ignore-errors  # (use sparingly)

# Run from the justfile directory, not the CWD
set working-directory := "."

# Use PowerShell on Windows
set windows-shell := ["powershell.exe", "-NoLogo", "-Command"]

The set shell directive with bash strict mode is strongly recommended. It catches undefined variables, pipe failures, and other silent errors that would otherwise go unnoticed.

just vs. Make vs. Task vs. npm Scripts

just vs. Make: just is simpler, has better error messages, and does not carry Make's file-target legacy. Use Make only if you genuinely need file-based dependency tracking (compiling C/C++, complex build graphs).

just vs. Task (go-task): Task uses YAML configuration and has built-in file-watching and checksumming. just uses a simpler plaintext format and focuses purely on running commands. Task is more featureful; just is more predictable.

just vs. npm scripts: npm scripts work for JavaScript projects but become unwieldy beyond simple commands. You cannot define dependencies between scripts, pass arguments cleanly, or use variables. just works with any language and any toolchain.

The Bottom Line

just is the right tool for the gap between "I have some commands I run often" and "I need a full build system." It replaces the common pattern of a Makefile full of .PHONY targets with a cleaner syntax that new team members can read without studying Make's manual. Put a justfile in your project root, move your README's "Development Setup" commands into named recipes, and your project becomes self-documenting.