What is Just?
Just is a handy way to save and run project-specific commands. Think of it as make but without the headaches—no .PHONY recipes needed, actual cross-platform support, and error messages that don’t make you want to throw your keyboard.
I’ve been using just for a while now on this site, and it’s become indispensable for managing Hugo builds, git workflows, and various automation tasks. The learning curve is minimal if you’ve ever touched make, but the payoff is substantial.
Why Not Make?
Make was designed for building software from source files. Just is designed for running commands. That subtle distinction matters:
- Just works the same on Linux, macOS, and Windows without extra dependencies
- Error messages tell you what’s actually wrong with your justfile
- Unknown recipes and circular dependencies get caught before anything runs
- You don’t need to mark everything as
.PHONY
Basic Syntax
Commands live in a justfile with make-inspired syntax. The basic pattern:
# comment describing what this does
recipe-name:
command to run
another command
Run it with just recipe-name from anywhere in your project.
Recipe Fundamentals
Comments Matter
Always put a comment above each recipe. These show up in just --list output, which is how people (including future you) discover what’s available:
# build the Hugo site
hugo:
TZ=America/Los_Angeles hugo
Parameters
Recipes accept command-line arguments:
# create a new blog post
post branchname:
hugo new content "content/posts/{{branchname}}.md"
Call it: just post my-great-idea
Parameters can have defaults:
# build with optional force flag
hugo force='':
#!/usr/bin/env bash
if [[ -n "{{force}}" ]]; then
echo "Forcing rebuild..."
fi
Dependencies
Recipes can depend on other recipes:
# create PR after running checks
pr: _has_commits && pr_checks
gh pr create --title "Update" --body "..."
The && syntax means both dependencies must succeed before pr runs.
Multi-line Recipes
For anything non-trivial, use a shebang:
# complex bash logic
process:
#!/usr/bin/env bash
set -euo pipefail # strict mode
for file in *.txt; do
echo "Processing $file"
done
This is way more readable than trying to chain commands with && or semicolons.
Recipe Attributes
Attributes modify how recipes behave. Stack them above the recipe:
[group('Process')]
[no-cd]
_on_a_branch:
#!/bin/bash
if [[ $(git rev-parse --abbrev-ref HEAD) == "main" ]]; then
exit 100
fi
Essential Attributes
[group('NAME')] — Organize recipes into logical groups for just --list:
[group('Hugo')]
server:
hugo server
[private] — Hide recipes from listings. Useful for helpers:
[private]
_internal_helper:
echo "Not shown in just --list"
Or just prefix with underscore—same effect:
# this is also private
_another_helper:
echo "Also hidden"
[no-cd] — Don’t change to justfile’s directory before running. Essential for recipes that operate on the current working directory:
[no-cd]
_has_commits:
git cherry -v main HEAD
[confirm] — Require confirmation before running:
[confirm]
deploy:
git push production main
Override with just --yes deploy.
Platform-Specific Attributes
Run recipes only on certain operating systems:
[macos]
install-deps:
brew install hugo
[linux]
install-deps:
apt-get install hugo
Available: [linux], [macos], [unix], [windows]
Other Useful Attributes
[parallel] — Run dependencies in parallel instead of sequentially.
[doc('description')] — Set documentation separately from comments.
[working-directory('path')] — Change working directory for this recipe.
[positional-arguments] — Enable positional argument handling.
Variables
Set variables at the top of your justfile:
release_branch := "main"
sync:
git checkout "{{ release_branch }}"
git pull
Use {{variable}} to reference them in recipes.
Organizing Justfiles
Start Simple
One justfile per project. Don’t over-engineer from day one.
Scale with Imports
When your justfile gets unwieldy, split it into logical modules:
import? '.just/hugo.just'
import? '.just/gh-process.just'
import? '.just/utility.just'
The ? makes imports optional—useful if you’re sharing justfiles across projects where some modules might not exist.
Module Structure
I keep specialized functionality in .just/ directory:
justfile # main orchestration
.just/
hugo.just # Hugo-specific commands
gh-process.just # Git/GitHub workflow
pr-hook.just # PR automation
utility.just # Helper functions
Each module focuses on one domain. The root justfile stays clean.
The Default Recipe
Make the first recipe in your justfile show help:
# list recipes (default works without naming it)
list:
just --list
Now just with no arguments shows what’s available.
Patterns I’ve Found Useful
Timestamped Branches
# start a new branch
branch branchname: _main_branch
#!/usr/bin/env bash
NOW=$(just utcdate)
git checkout -b "$USER/$NOW-{{ branchname }}"
# print UTC date in ISO format
[no-cd]
@utcdate:
TZ=UTC date +"%Y-%m-%d"
Every branch gets an ISO date prefix automatically. Useful for tracking when work started.
Sanity Checks
Private recipes that error out if preconditions aren’t met:
# error if not on a git branch
[no-cd]
_on_a_branch:
#!/bin/bash
if [[ $(git rev-parse --abbrev-ref HEAD) == "main" ]]; then
echo "You are on the main branch, not a feature branch"
exit 100
fi
Use as dependencies: pr: _on_a_branch
PR Workflow
# create PR
pr: _has_commits && pr_checks
#!/usr/bin/env bash
set -euo pipefail
# run optional pre-pr hook
if [[ -e ".just/pr-hook.just" ]]; then
just _pr-hook
fi
git push -u origin HEAD
# generate PR body from commits
FIRST_COMMIT=$(git log --format=%s -1)
gh pr create --title "$FIRST_COMMIT" --body "..."
This pattern handles the entire PR creation flow with sanity checks.
Environment-Specific Setup
# run hugo server
server:
#!/usr/bin/env bash
TZ=America/Los_Angeles hugo server
just hugo # cleanup after server!
Set timezone consistently for Hugo’s date handling.
Best Practices
Formatting
- Use 4 spaces for indentation
- Run
just --unstable --fmtto auto-format your justfile - Add
.editorconfigentry for justfiles
Shell Selection
Default is sh which means POSIX syntax. For bash-specific features, use shebang recipes:
process:
#!/usr/bin/env bash
# now you can use bash arrays, etc.
Error Handling
In bash recipes, use strict mode:
#!/usr/bin/env bash
set -euo pipefail
This catches errors early instead of letting commands fail silently.
Quote Arguments
Always quote recipe parameters:
post branchname:
hugo new "content/posts/{{ branchname }}.md"
Prevents issues with spaces or special characters.
Built-in Functions Over Shell Commands
Just has built-in functions that work consistently across platforms. Prefer these over shelling out when possible. Check the official manual for the complete list.
Don’t Hardcode Values
Use parameters or variables instead:
# bad
deploy:
ssh server1 "systemctl restart app"
# good
deploy server:
ssh {{ server }} "systemctl restart app"
Comment Everything
Future you (and your teammates) will thank you:
# merge PR and return to main branch
merge: _on_a_branch && sync
gh pr merge -s -d
Color Output
Just provides built-in ANSI color constants (v1.37.0+) that work out of the box—no need to define them yourself:
# example recipe using built-in constants
build:
echo "{{GREEN}}Building...{{NORMAL}}"
echo "{{BLUE}}Processing files...{{NORMAL}}"
echo "{{RED}}Error occurred!{{NORMAL}}"
Available color constants:
Foreground colors: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE
Background colors: BG_BLACK, BG_RED, BG_GREEN, BG_YELLOW, BG_BLUE, BG_MAGENTA, BG_CYAN, BG_WHITE
Text attributes: NORMAL (reset), BOLD, ITALIC, UNDERLINE, INVERT, HIDE, STRIKETHROUGH
Utility: CLEAR (clears screen)
These are just ANSI escape sequences under the hood, so they work wherever your terminal supports color. Use NORMAL to reset formatting back to defaults.
Built-in Constants
Beyond colors, just provides other useful constants (v1.27.0+):
String constants:
HEXorHEXLOWER—"0123456789abcdef"HEXUPPER—"0123456789ABCDEF"
Path constants (v1.41.0+):
PATH_SEP—"/"on Unix,"\"on WindowsPATH_VAR_SEP—":"on Unix,";"on Windows
Useful for writing cross-platform recipes:
# works on both Unix and Windows
build-path:
echo "{{justfile_directory()}}{{PATH_SEP}}build"
Testing and Debugging
Dry Run
See what would execute without running it:
just --dry-run recipe-name
List Recipes
Show all available recipes:
just --list
Group them for better organization with [group('name')] attributes.
Evaluate Variables
Check variable values:
just --evaluate
Verbose Output
See every command before it runs:
just --verbose recipe-name
Version Management
If you’re using just across multiple projects or in a team, consider pinning the version. I’ve found that just’s backwards compatibility is solid, but explicit versions prevent surprises.
Options:
- mise (formerly rtx)
- asdf
- Specify in your project’s README
Common Gotchas
Shellcheck Warnings
Shellcheck doesn’t understand just’s template syntax. Disable warnings for template variables:
process force='':
#!/usr/bin/env bash
# force variable is replaced by just
# shellcheck disable=SC2157
if [[ -n "{{force}}" ]]; then
echo "Forcing..."
fi
Tab vs Spaces
Just works with tabs or spaces, as long as you use them consistently in the same file.
Working Directory
By default, just changes to the justfile’s directory before running recipes. Use [no-cd] if you need to stay in the invocation directory.
Real Example from This Site
Here’s how I use just for Hugo site management:
import? '.just/hugo.just'
import? '.just/gh-process.just'
# list recipes (default)
list:
just --list
# start a new post
[group('Process')]
post branchname: _main_branch
#!/usr/bin/env bash
NOW=$(just utcdate)
git co -b "chicks/post/$NOW-{{ branchname }}"
hugo new content "content/posts/$NOW-{{ branchname }}.md"
The imported modules handle Hugo builds, git workflows, and PR creation. The root justfile stays focused on high-level orchestration.
More Information
The official manual is comprehensive and well-written:
- Just Programmer’s Manual — Complete documentation
- GitHub repository — Source code and issue tracker
- Attributes reference — All recipe attributes
Just has transformed how I manage project automation. It’s one of those tools that gets out of your way and just works. Start simple, add complexity as needed, and you’ll wonder how you managed without it.
Sources: