Introduction
Think about the absurdity of modern developer experience for a second. We spent years mastering complex algorithms, distributed systems, and clean architecture... only to spend half our Tuesday in Slack asking: "Hey, does anyone remember the secret flags to run local integration tests?"
Overnight, you’re no longer a Senior Software Engineer — you’ve been demoted to a digital archaeologist, digging through 2023 Slack threads, broken READMEs, and history | grep fragments. At this point, booting a simple microservice requires a more ritualistic alignment of .env files, hidden shell scripts, and secret flags than launching an actual rocket into orbit.
And now, enter the newest monster looming over our developer tooling: AI coding agents. Datacenters are currently burning the energy equivalent of a medium-sized city — not just to generate reggaeton tracks, cat photos, and sourdough recipes, but to spend tens of thousands of tokens "intelligently" trying to figure out how to build your app... only to hallucinate the wrong build command one out of four times because its training data was scraped from a broken 2017 forum thread.
I first stumbled upon just during a massive internal refactor at work. A brilliant teammate introduced a justfile into one of our core libraries, modernizing how we built, monitored, and interacted with our stack. It solved years of technical debt in one clean sweep.
Naturally, corporate politics intervened. A high-level manager mandated that we abandon the modernized stack for a trendy framework in another language — one with tooling that was five years out of date and would take us two years just to reach feature parity.
Then came two brutal rounds of layoffs. The first wave eliminated the manager who forced the framework swap. The second wave, unfairly, took out the talented developer who introduced just.
Both of them are gone now, along with the corporate initiatives they pushed. But amidst the chaos of restructuring and abandoned frameworks, that humble justfile survived. I inherited it, learned to squeeze every drop of value out of it, and brought it into every project I've touched since.
Here is why just outlived the corporate drama — and why I want to persuade you to add it to your daily toolkit.
The Problem: Friction in the Developer Experience
Whether you're working solo, in a startup, or inside an enterprise, task execution quickly degenerates into chaos. Let me give you some examples:
1. Operational & Human Friction
- The 6-Month Amnesia: Returning to a project after months away means playing guessing games with your shell history just to remember how to start the dev server.
- The "New Laptop" Panic: You get a fresh machine to ship an urgent hotfix, only to find the setup guide in
README.mdhasn't been updated in two years. Because, let's be honest: we are lazy, and even with LLM assistants, updating documentation is the first thing we forget. - The Bus Factor: You need a staging database dump to debug an error. The credentials are in
.env, but the only engineer who knows the exact script flags just went on vacation — or worse, was laid off to be replaced by "AI". - Onboarding Lag: A new teammate joins, wasting their first two days troubleshooting path variables and environment setups instead of learning the platform and shipping their first contribution.
- Markdown Fatigue: I'm still a fan of Markdown, but it’s a markup language, not a task execution engine. The Python community has reStructuredText with executable code blocks, and plenty of people try similar hacks in Markdown. But Markdown files can’t take command-line arguments or easily debug scripts. Hiding raw, executable bash snippets inside a 900-line
.mdfile is just a fancy way to bury documentation.
2. Tooling & Scripting Limitations
- Script Sprawl: Your repository root is littered with
./scripts/test.sh,./bin/seed.py, and./deploy.sh. They're hard to discover, inflexible, and lack a unified interface. - Make Abuse:
Makeshines at build graphs, but passing arguments to targets is clumsy, tab-syntax is finicky, and developers end up hoarding private cheat-sheets. - Legacy Makefiles: These files often accumulate decades of cruft, undefined variables, and obscure dependencies that turn a simple change into a game of Russian roulette.
- Corporate CLI Bloat: Your company’s internal CLI has 45+ subcommands for enterprise platform tools — complete overkill when your specific microservice only needs 3 repeatable local commands.
- AI Agent Over-Engineering: We try to solve doc debt by writing an
AGENTS.mdfile full of CLI instructions. For small tasks, it works. But as the project grows, you end up maintaining dozens of sub-files and custom skills — only to watch the damned LLM ignore them anyway and generate a rogue Python script to invoke commands because "it felt right in the moment.". I hate it when the LLM writes Python behind my back! How dare you!
3. Governance & Standardized Execution
- The Illusion of Container Governance: Docker solved environment consistency — you no longer maintain custom local setups or worry about OS incompatibilities. But Docker doesn't govern how you run tasks. If invoking your app requires stringing together complex
docker compose execcalls, custom.envoverrides, and bash scripts, you've just shifted the chaos from the host OS to the container. - The Historical Precedent: When Stuart Feldman created
Makeat Bell Labs back in 1976, he wasn't just building a dependency tracker — he was establishing execution governance. He wanted a single, auditable entry point so every engineer on the team compiled code the exact same way. - Unchecked Command Drift: Without a centralized task runner, every developer (and CI pipeline) runs tests, seeds databases, and executes deployments with slightly different flags, environment variables, and hidden assumptions.
- The Single Entry Point: Concentrating recipes into a single file isn't just about convenience; it's about governance. It establishes a contract: if it's not in the
justfile, it's not how we build or run this project.
A Possible Solution
What if we could decouple contextual documentation from command execution? What if we had a single, unified place to simply run: "just build", "just test", or "just deploy"?
That is exactly what Just does. And as you've probably guessed, that’s where it gets its name.
What Is Just?
We finally reach the place where I tell you what in the world Just is. Just is a task runner that:
- Declares recipes in a plain‑text
justfile. - Executes commands with a clean, tab‑free syntax.
- Stays out of the way of actual build systems (they already do an excellent job,
justwill let them do what they do best). - Works with Python, Node, Java, Rust, Go, Bash… any language and tool you can call from the shell.
How Just Relates to the Ecosystem
To place Just in context (lame pun intended), here is how it compares to language-specific tools and direct competitors:
| Tool | Ecosystem / Config | Primary Purpose | How Just Relates |
|---|---|---|---|
| Make | C / Universal (Makefile) | File-dependency build system | Direct Alternative: just strips out Make's tab syntax, implicit file rules, and arcane syntax, keeping only recipe orchestration. |
Task (go-task) | Go / Universal (Taskfile.yml) | Direct competitor (YAML task runner) | Competitor: Task uses YAML + Go templates. just uses plain shell syntax with zero YAML overhead. |
| npm / pnpm / yarn | Node.js / TS (package.json) | Package manager & script runner | Wrapper: just delegates JS steps to npm run, while wrapping non-JS tasks (Docker, database seeds, Terraform) in the same project. |
| Poetry / Poe / Invoke | Python (pyproject.toml / tasks.py) | Python dependency & task runner | Wrapper / Complement: Python task runners require an active venv/Python runtime. just runs globally as a single binary without Python dependencies. |
| Cargo / cargo-make | Rust (Cargo.toml / Makefile.toml) | Rust build tool & task extension | Complement: just (written in Rust itself) delegates compilation to cargo build, but orchestrates multi-container/CI setup steps. |
| Gradle / Maven | JVM / Java / Kotlin (build.gradle) | Heavy JVM build & lifecycle engine | Wrapper: just invokes ./gradlew targets without replacing Gradle's compilation dependency graph. |
| Composer Scripts | PHP (composer.json) | PHP package manager & script hooks | Wrapper: just calls composer commands while managing external dev services (Redis, MySQL containers). |
| Rake | Ruby (Rakefile) | Ruby task management tool | Alternative: Rake requires Ruby installed on the machine; just is a standalone binary. |
| Nx / Turborepo | Monorepos (nx.json / turbo.json) | Monorepo build caching & task graphs | Complement: Nx/Turbo handle monorepo dependency caching. just handles local, day-to-day developer recipes. |
Build Systems vs. Task Runners
The key distinction to keep in mind:
- Build Systems (
Make,Gradle,Cargo,Webpack) track file modifications, compile source code, and construct dependency trees. - Task Runners (
Just,Task,npm scripts) don't care about compiling code. They exist solely to give human developers (and AI agents) a short, memorable alias for complex commands.
Just vs Make vs npm scripts
Make shines at dependency graphs but suffers from:
- Arcane syntax (mandatory tabs, cryptic escaping).
- Implicit rules that are hard to debug.
- Weak error handling and poor Windows support.
Just improves on those pain points:
- Clear, modern syntax.
- Consistent behavior on Linux, macOS, and Windows. Conveniently, it lets you define OS-specific recipes.
- Robust error handling – a failing command aborts the recipe.
npm scripts are great for pure Node projects but become unwieldy when you need Docker, Python, or any non‑Node tooling.
Just is basically Make rewritten without the 1970s baggage.
Problems Just does solve
- Living documentation –
justlists all tasks;just --help <task>shows the recipe. - Cross‑language portability – a single
justfileorchestrates the whole stack. - Human‑friendly syntax – no hidden tabs, no implicit rules.
- Robust error handling – recipes stop on the first failure.
- Composition – recipes can call other recipes and accept parameters.
What Just doesn’t do
- Not a dependency manager.
- Not a build system (it won’t compile your code).
- Not a CI/CD platform (but works great inside GitHub Actions).
- Not a Docker replacement (it can invoke Docker).
- Not magically cross‑platform if you write OS‑specific commands.
Getting Started with Just
1. Installation
The best part about Just is that it's a single binary with zero runtime dependencies. You can install it on any platform using your package manager of choice (or via cargo install just if you're a fellow Rustacean):
# macOS (Homebrew) brew install just # Linux (Debian / Ubuntu / Fedora / Arch) sudo apt install just # Debian / Ubuntu sudo dnf install just # Fedora sudo pacman -S just # Arch Linux # Windows (Winget / Chocolatey) winget install -e --id Casey.Just choco install just
Verify your installation:
$ just --version just 1.34.0
2. Anatomy of a justfile
When you type just in a terminal, it looks for a file named justfile (or .justfile) in the current directory or parent folders.
Here is a practical justfile that demonstrates all the core syntax building blocks:
# Global variables (defined with :=)
PYTHON := "python3"
VERSION := "1.0.0"
# Default recipe (first recipe in the file)
hello:
echo "Hello from Just!"
# Recipe with a dependency (runs 'hello' first)
build: hello
echo "Building version {{VERSION}} using {{PYTHON}}..."
# Recipe with a parameter
greet name:
echo "Hola, {{name}}!"
# Recipe with a comment (used as documentation in --list)
# Run the test suite
test:
echo "Running integration tests..."
# Recipe composition (orchestrates multiple recipes)
all: build test
3. Interacting with Your Recipes
Save the snippet above as justfile in an empty directory. The first command you should ever run in a new project is just --list:
$ just --list Available recipes: all # Recipe composition (orchestrates multiple recipes) build # Recipe with a dependency (runs 'hello' first) greet name # Recipe with a parameter hello # Default recipe test # Run the test suite
Pro Tip: Notice how comments placed immediately above a recipe automatically become docstrings in the
--listoutput. This turns yourjustfileinto living, self-documenting CLI help!
Executing individual recipes is as simple as:
$ just greet Alice echo "Hola, Alice!" Hola, Alice! $ just all echo "Hello from Just!" Hello from Just! echo "Building version 1.0.0 using python3..." Building version 1.0.0 using python3... echo "Running integration tests..." Running integration tests...
You can also chain multiple recipes in a single invocation:
$ just test hello echo "Running integration tests..." Running integration tests... echo "Hello from Just!" Hello from Just!
4. Key Syntax Rules at a Glance
If you've used Make or Python, the syntax feels natural:
- Variables (
:=): Declared at the top, interpolated inside recipes using{{VARIABLE}}. - Recipes & Colons: Recipes end with a colon
:and indentation (spaces or tabs, just stay consistent). - Dependencies: Listed after the colon (
build: hello). Dependent recipes run automatically before the target recipe. - Parameters: Defined right after the recipe name (
greet name:). - Self-Documentation: Comments (
#) above a recipe are parsed into--listdescriptions.
Essential & Advanced Just Features
Now that you have your first justfile running, let's explore the core features you will use every day in production. Instead of dropping unexplained code snippets, let's look at why each feature exists and how to use it effectively.
1. The Default Recipe & Self-Listing CLI
By default, when you run just without arguments, it executes the very first recipe defined in your justfile.
Instead of letting a random task (like clean or build) run by accident, the standard best practice is to define a default recipe at the top that invokes @just --list:
# Default recipe: shows all available tasks
default:
@just --list
Now, simply typing just in your terminal gives your team an instant, interactive menu of available commands!
2. Quiet Execution with @ (Suppressing Command Echo)
By default, just echoes every shell command to stdout before executing it (just like Make). While great for debugging, it creates noisy output for simple status messages or help text.
Prefixing a command line with @ suppresses the echo:
# Loud recipe (prints the echo command AND the output)
noisy:
echo "This line will be printed twice"
# Quiet recipe (only prints the output)
clean:
@echo "Cleaning temporary build artifacts..."
@rm -rf ./dist ./.pytest_cache
You can also prefix the recipe name itself with @ (@clean:) to make the entire recipe quiet by default.
3. Parameters: Defaults, Required Args, and Variadic Arguments (*args)
just makes passing command-line arguments clean and flexible without writing complex getopts bash parsing:
# 1. Required parameter (fails if omitted: 'just greet')
greet name:
@echo "Hello, {{name}}!"
# 2. Default parameter value (fallback if omitted: 'just serve')
serve port="8000" env="development":
@echo "Starting {{env}} server on port {{port}}..."
# 3. Variadic arguments (captures all remaining CLI flags into a single variable)
test *flags:
pytest tests/ {{flags}}
With variadic parameters (*flags or +flags), you can pass arbitrary flags directly to underlying tools:
$ just test -k auth --verbose --cov # Executes: pytest tests/ -k auth --verbose --cov
4. How Just Executes Commands: Subshell Ephemerality
A common trap for developers coming from shell scripts is expecting state to persist between lines of a recipe:
# BROKEN EXAMPLE: cd and export do NOT persist to the next line!
broken-setup:
cd /tmp
pwd # Surprise! Still in your project root directory, NOT in /tmp!
Why does this happen?
Each line in a justfile recipe is executed in a separate subshell instance. When line 1 finishes (cd /tmp), that subshell terminates, and line 2 starts in a brand new shell at the original working directory.
Solutions for Multi-line Logic:
- Chain inline commands using
&&:setup: cd /tmp && pwd - Use Shebang Recipes for complex multi-step scripts.
5. Custom Shells & Shebang Recipes
If a recipe requires complex logic (loops, conditionals, or inline Python/Node scripts), just allows you to write Shebang recipes:
# Change the default shell globally (e.g. to bash with strict error flags)
set shell := ["bash", "-uc"]
# Shebang recipe: executes the block as a single script file
backup-db:
#!/usr/bin/env bash
set -euo pipefail
echo "Starting backup process..."
DATE=$(date +%Y%m%d)
pg_dump my_db > "backup_${DATE}.sql"
echo "Backup completed successfully!"
You aren't restricted to Bash! You can use Python, Node, or Ruby in a shebang recipe:
# Inline Python script inside your justfile
parse-data:
#!/usr/bin/env python3
import json
print("Processing JSON payload directly inside Just!")
6. Environment Variables and .env File Support
Managing environment variables in shell scripts is notorious for breaking across platforms. just handles environment variables natively:
# Load .env automatically if present in the project root
set dotenv-load
# Export a variable to child processes spawned by recipes
export DATABASE_URL := "postgres://user:pass@localhost:5432/db"
migrate:
@echo "Connecting to {{DATABASE_URL}}..."
alembic upgrade head
Setting set dotenv-load instructs just to parse .env before running any recipe, making local secret management completely seamless.
Squeezing Every Drop: My Favorite Just Features
This is where just transforms from a simple Makefile alternative into an indispensable powerhouse. Here are my favorite power-user features that make writing and maintaining justfiles a joy:
1. Developer Tooling: LSP, Editor Extensions, and Native MCP Support
just isn't an obscure syntax you have to write blind. It has first-class Editor Support (VS Code, Neovim, Emacs, Zed) and an official Language Server Protocol (LSP) for autocomplete and diagnostics.
Even better: just now supports the Model Context Protocol (MCP)! Remember our earlier rant about AI agents hallucinating bash commands? With the just MCP server, AI assistants (like Claude, Cursor, or custom agents) can directly query and execute your justfile recipes as structured tools instead of guessing terminal flags.
2. Attribute Directives & Parameter Constraints ([arg(...)])
Using Attributes, you can attach metadata to recipes and validate inputs before execution:
# Prompt the user for confirmation before running destructive actions
[confirm("Are you sure you want to drop the production database?")]
drop-db:
@echo "Dropping database..."
# Validate parameters using [arg(...)]
[arg("env", choices: ["dev", "staging", "prod"])]
deploy env:
@echo "Deploying to {{env}}..."
3. Recipe Groups ([group(...)])
When your justfile grows to 30+ recipes, just --list can become overwhelming. You can organize recipes into logical Groups:
[group('Database')]
db-migrate:
alembic upgrade head
[group('Database')]
db-seed:
python scripts/seed.py
[group('Testing')]
test-unit:
pytest tests/unit
[group('Testing')]
test-e2e:
playwright test
Running just --list now renders a beautifully structured, categorized CLI menu!
4. Private Recipes ([private] or _name)
Keep internal helper scripts out of your public CLI documentation using Private Recipes:
# Hidden from 'just --list' (using [private] or leading underscore '_')
[private]
_check-vpn:
@ping -c 1 internal.vpn > /dev/null
# Public recipe
deploy: _check-vpn
@echo "Deploying payload..."
5. Conditional Expressions
just supports clean Conditional Expressions inside recipe lines or variable declarations:
# Dynamic configuration based on environment or OS
IS_CI := if env_var_or_default("CI", "false") == "true" { "yes" } else { "no" }
BUILD_DIR := if os() == "windows" { "C:\\build" } else { "/tmp/build" }
info:
@echo "Running in CI: {{IS_CI}}"
6. Built-in Expressions & String Substitutions
just includes a rich standard library of Expressions & Substitutions for string manipulation, hashing, and filesystem checks:
GIT_HASH := `git rev-parse --short HEAD`
CONFIG_EXISTS := path_exists("config.json")
UPPER_ENV := uppercase(env_var_or_default("ENV", "dev"))
status:
@echo "Commit: {{GIT_HASH}} | Config present: {{CONFIG_EXISTS}}"
7. Parallel Recipe Execution
Need to run multiple independent tasks as fast as possible? just supports native Parallelism via the --jobs (-j) flag:
# Run lint, test, and typecheck in parallel across 4 worker threads $ just -j 4 lint test typecheck
8. Scaling Up: Modules and Imports
When a single justfile grows too large, just provides two distinct ways to split your configuration:
- Imports: Merge external files directly into the root namespace (great for company-wide shared tasks or CI helpers):
import 'shared/ci-helpers.just' - Modules: Create clean sub-namespaces for monorepos or multi-service projects:
Now you can runmod api 'services/api/justfile' mod web 'apps/web/justfile'just api::devorjust web::testwith perfect namespace isolation!
9. Pro-Tip: Ad-Hoc Remote Automation over SSH
Here is an operational trick I love: if your staging or production servers also use a justfile, you can define local wrapper recipes in your machine's justfile that proxy execution to the remote server via SSH:
SERVER := "deploy@production-node-01.internal"
# Stream live remote logs cleanly
remote-logs service="web":
ssh -t {{SERVER}} "cd /opt/app && just logs {{service}}"
# Trigger remote database migrations
remote-migrate:
ssh {{SERVER}} "cd /opt/app && just db-migrate"
This gives you a powerful, ad-hoc remote management workflow without needing to spin up heavy Ansible playbooks for simple operational tasks!
When (and When Not) to Use Just
When Just Shines
- Polyglot & Multi-service stacks: Unifies Python, Node, Go, Rust, and Docker under a single CLI contract.
- Taming Onboarding Debt: Replaces long
README.mdcommand catalogs with a self-documenting executable interface (just --list). - AI Agent Guardrails: Exposes deterministic recipes to LLM coding assistants (especially via MCP) so they never hallucinate dangerous bash flags.
When to Skip It
- Pure single-language apps: If your project is 100% Rust (
Cargo) or 100% Node (npm), stick to the ecosystem's native scripts. - Complex Dependency Graphs: If you need strict file modification tracking and target compilation trees, use a real build system like
Make,Bazel, orGradle.
Conclusion & TL;DR
Just is not trying to revolutionize build systems or reinvent package managers. It does one thing, and does it exceptionally well: it replaces tribal knowledge and command-line chaos with a clean, standardized entry point.
If your project's README.md has a "How to Run Locally" section with 15 copy-pasted terminal commands, burn the doc, write a justfile, and let your team (and your AI agents) run just.
References
- Just GitHub Repository
- Just Programmer's Manual
- Editor Support & Plugins
- Language Server Protocol (LSP)
- Model Context Protocol (MCP)
- Attributes & Parameter Constraints
- Groups Documentation
- Conditional Expressions
- Expressions & Substitutions
- Parallelism
- Private Recipes
- Imports Documentation
- Modules Documentation
