Justfile Skill
just is a command runner (not a build system) that saves and runs project-specific commands in a
file called Justfile. It uses make-inspired syntax but is simpler and more portable, with none of
make's idiosyncrasies (.PHONY, tab sensitivity, implicit rules, timestamp tracking).
This skill enforces a consistent house style so every Justfile looks the same across projects.
The rules below are the authoritative convention; Tools/lint.ts validates the deterministic ones.
Workflow Routing
| Trigger |
Workflow |
| "create a justfile", "set up just", "add a recipe/module" |
Workflows/CreateJustfile.md |
| "migrate Makefile to just", "convert make to just" |
Workflows/MigrateFromMake.md |
| "check/lint this justfile", "is this justfile correct" |
Workflows/CheckJustfile.md |
When to Use Just vs Make
| Scenario |
Tool |
| Project task automation (build, test, deploy, lint) |
just |
| Cross-platform command runner |
just |
Actual file-based build dependencies (compile .c → .o) |
make |
| Legacy projects already deep in make |
make (or migrate) |
Common Mistakes — do NOT do these
Patterns the model often generates incorrectly. Check output against this list.
| WRONG |
RIGHT |
justfile (lowercase) |
Justfile (capital J) |
mod docker (bare) |
mod docker '.justfiles/docker.just' |
Module at docker.just or just/docker.just |
Module at .justfiles/docker.just |
default: |
_default: (underscore required) |
@just --list |
@just --list --unsorted (module) or --unsorted --list-submodules (root with modules) |
env("NAME", "val") |
env_var_or_default("NAME", "val") |
| Module file without the three-line header |
Every file gets the full header |
Module file without its own _default recipe |
Every file gets its own _default |
Module named after a tool (psql.just) |
Module named after a concern (db.just) |
Tests in docker.just because they run in a container |
Tests in test.just — classify by purpose, not implementation |
| Root recipe duplicates module logic |
Root shortcut delegates: build: docker-build |
Ad-hoc names (run-tests, do-lint) |
Standard names: test, lint, build, dev, fmt, check |
Relative paths in module recipes (bash tests/run.sh) |
Use source_directory() for absolute paths |
Mandatory Rules — apply to EVERY file you create or edit
- The root file MUST be named
Justfile (capital J).
- EVERY file (root and every
.just module) MUST start with this three-line header:#!/usr/bin/env just --justfile
set shell := ["bash", "-euo", "pipefail", "-c"]
set dotenv-load := true
Use set dotenv-load := false where appropriate, but the line must always be present.
- EVERY file MUST have
_default as its first recipe:# List all available recipes
_default:
@just --list --unsorted
The root Justfile with modules uses @just --list --unsorted --list-submodules. Module
files use @just --list --unsorted (no --list-submodules).
- Section order in every file: variables → mod imports → recipes.
- Module files MUST live at
.justfiles/<name>.just — never just/, never beside the root.
- Import modules with explicit paths:
mod name '.justfiles/name.just' — never bare mod name.
- Use
env_var_or_default("NAME", "value") for variable defaults — never env().
- Every recipe gets a
# doc comment on the line directly above it.
- Parameterized recipes document each param:
# param - description (default: value).
- Private/helper recipes start with
_.
- Dependencies go on the definition line:
build: _lint test.
- Destructive recipes prompt for confirmation; the doc comment says "DESTRUCTIVE, prompts for confirmation".
- Extract modules by domain concern, named after the concern (
db.just) not the tool (psql.just). The root Justfile is a thin orchestrator: _default, shortcut recipes, and project-wide recipes like check/clean.
- The root provides shortcut recipes for common workflows that delegate to modules, giving developers a flat namespace for everyday tasks.
- Use the standard recipe vocabulary below as the public API. Never invent
run-tests, do-lint, compile, format.
- In modules, never use bare relative paths — module recipes run with the module's directory as CWD. Define
root := source_directory() / ".." and reference files as {{root}}/tests/run.sh.
Standard Recipe Vocabulary
A developer should be able to run just test, just dev, or just check in any project without guessing. Use these exact names; include only the ones that apply.
| Recipe |
Purpose |
Include when |
dev |
Start dev environment (server, watch, REPL) |
Project has a dev loop |
test |
Run the test suite |
Always |
build |
Build or compile |
Project has a build step |
lint |
Run linters |
Linters configured |
fmt |
Format code |
Formatters configured |
check |
Run ALL quality gates (check: lint test) |
Always |
clean |
Remove build artifacts, caches, generated files |
Project produces output |
check is the meta-recipe — depend on the applicable gates and add format checks (cargo fmt --check, ruff format --check) as appropriate.
Namespacing by Concern
Group by domain, not tool. Classify by purpose: a test that runs in Docker is a testing recipe (test.just), not a Docker recipe. A migration that uses kubectl is a database recipe (db.just).
| Concern |
Module |
Typical recipes |
| Development |
dev.just |
build, test, lint, fmt, bench |
| Testing |
test.just |
run, list, watch, coverage |
| Containers |
docker.just |
build, push, run, compose-up |
| CI/CD |
ci.just |
lint, deploy, release |
| Database |
db.just |
migrate, seed, reset, dump, restore |
| Infrastructure |
infra.just |
plan, apply, destroy |
| Kubernetes |
k8s.just |
apply, diff, rollback, logs |
| Documentation |
docs.just |
build, serve, publish |
Single-concern projects (e.g. a Go/Rust project with only build/test/lint/fmt) use one dev.just; the root still stays thin. Modules are self-contained: own variables, own _default, no cross-module recipe dependencies.
Templates & References
- Templates (root + module, copy-and-adapt):
Templates.md
- just language reference (variables, args, deps, conditionals, attributes, functions, install):
References/Syntax.md
- Recipe fragments by project type (Terraform, Go, Python, Docker, Azure, Ansible):
References/Patterns.md
- Makefile → just migration guide:
References/MakeMigration.md
Linting
After creating or editing ANY Justfile or .just module, run the structural lint and fix every failure:
bun Tools/lint.ts <project-dir>
It checks file naming, the three-line header, _default as first recipe, the --unsorted/--list-submodules flags, env_var_or_default() usage, doc comments on all recipes, explicit module import paths, and section order. Any FAIL is a bug — fix and re-run until clean. The lint cannot judge concern-based naming, self-containment, or standard-vocabulary use — verify those by inspection (see Workflows/CheckJustfile.md).
Gotchas
- Each recipe line runs in a separate shell by default —
cd foo on one line and ls on the next runs ls in the original directory. Use a shebang recipe for multi-line scripts.
set dotenv-load loads .env from the justfile directory, not the invocation directory — running just from a subdirectory loads the parent's .env. Use set dotenv-path to override.
- Backtick variables evaluate at parse time, every invocation —
git_hash := \git rev-parse HEAD`runs git on everyjust` call, even for unrelated recipes. Slow on large repos; move inside the recipe if not needed globally.
- Recipe arguments don't shell-quote automatically —
just deploy "my server" passes two args. Use set positional-arguments with "$@", or wrap as {{quote(target)}}.
set shell := ["bash", "-c"] breaks set -euo pipefail semantics because each line is its own -c invocation — pipefail only applies within that line. Use shebang recipes for proper fail-fast scripts.
just --list hides _-prefixed and [private] recipes but they're still callable — obscurity, not access control.
- Cross-platform
[macos]/[linux] attributes silently skip the recipe on other OSes — running just install on Windows when only [linux]/[macos] variants exist exits 0 with no error, which looks like success.
- The lint validates structure, not behavior — a recipe can pass every check and still run the wrong command. Run
just --dry-run <recipe> to verify expansion.
1---2name: justfile3description: Create, edit, refactor, lint, and maintain Justfiles and `.just` module files using the `just` command runner. ALWAYS use this skill when the user mentions justfile, Justfile, just recipes, just modules, `.just` files, or asks to set up task automation with just. Also trigger when migrating a Makefile to just, adding recipes or modules to an existing Justfile, or organizing and documenting project commands. Covers house conventions, templates, namespacing by domain, dotenv, cross-platform support, and a structural lint. NOT FOR file-based build dependency graphs that need timestamp tracking (use make).4---5
6# Justfile Skill
7
8`just` is a command runner (not a build system) that saves and runs project-specific commands in a
9file called `Justfile`. It uses make-inspired syntax but is simpler and more portable, with none of
10make's idiosyncrasies (`.PHONY`, tab sensitivity, implicit rules, timestamp tracking).
11
12This skill enforces a consistent **house style** so every Justfile looks the same across projects.
13The rules below are the authoritative convention; `Tools/lint.ts` validates the deterministic ones.
14
15## Workflow Routing
16
17| Trigger | Workflow |
18|---------|----------|
19| "create a justfile", "set up just", "add a recipe/module" | `Workflows/CreateJustfile.md` |
20| "migrate Makefile to just", "convert make to just" | `Workflows/MigrateFromMake.md` |
21| "check/lint this justfile", "is this justfile correct" | `Workflows/CheckJustfile.md` |
22
23## When to Use Just vs Make
24
25| Scenario | Tool |
26|----------|------|
27| Project task automation (build, test, deploy, lint) | **just** |
28| Cross-platform command runner | **just** |
29| Actual file-based build dependencies (compile `.c` → `.o`) | **make** |
30| Legacy projects already deep in make | **make** (or migrate) |
31
32## Common Mistakes — do NOT do these
33
34Patterns the model often generates incorrectly. Check output against this list.
35
36| WRONG | RIGHT |
37|-------|-------|
38| `justfile` (lowercase) | `Justfile` (capital J) |
39| `mod docker` (bare) | `mod docker '.justfiles/docker.just'` |
40| Module at `docker.just` or `just/docker.just` | Module at `.justfiles/docker.just` |
41| `default:` | `_default:` (underscore required) |
42| `@just --list` | `@just --list --unsorted` (module) or `--unsorted --list-submodules` (root with modules) |
43| `env("NAME", "val")` | `env_var_or_default("NAME", "val")` |
44| Module file without the three-line header | Every file gets the full header |
45| Module file without its own `_default` recipe | Every file gets its own `_default` |
46| Module named after a tool (`psql.just`) | Module named after a concern (`db.just`) |
47| Tests in `docker.just` because they run in a container | Tests in `test.just` — classify by purpose, not implementation |
48| Root recipe duplicates module logic | Root shortcut delegates: `build: docker-build` |
49| Ad-hoc names (`run-tests`, `do-lint`) | Standard names: `test`, `lint`, `build`, `dev`, `fmt`, `check` |
50| Relative paths in module recipes (`bash tests/run.sh`) | Use `source_directory()` for absolute paths |
51
52## Mandatory Rules — apply to EVERY file you create or edit
53
541. The root file MUST be named `Justfile` (capital J).
552. EVERY file (root and every `.just` module) MUST start with this three-line header:
56 ```just
57 #!/usr/bin/env just --justfile
58 set shell := ["bash", "-euo", "pipefail", "-c"]
59 set dotenv-load := true
60 ```
61 Use `set dotenv-load := false` where appropriate, but the line must always be present.
623. EVERY file MUST have `_default` as its first recipe:
63 ```just
64 # List all available recipes
65 _default:
66 @just --list --unsorted
67 ```
68 The **root** Justfile with modules uses `@just --list --unsorted --list-submodules`. Module
69 files use `@just --list --unsorted` (no `--list-submodules`).
704. Section order in every file: **variables → mod imports → recipes**.
715. Module files MUST live at `.justfiles/<name>.just` — never `just/`, never beside the root.
726. Import modules with explicit paths: `mod name '.justfiles/name.just'` — never bare `mod name`.
737. Use `env_var_or_default("NAME", "value")` for variable defaults — never `env()`.
748. Every recipe gets a `#` doc comment on the line directly above it.
759. Parameterized recipes document each param: `# param - description (default: value)`.
7610. Private/helper recipes start with `_`.
7711. Dependencies go on the definition line: `build: _lint test`.
7812. Destructive recipes prompt for confirmation; the doc comment says "DESTRUCTIVE, prompts for confirmation".
7913. Extract modules by **domain concern**, named after the concern (`db.just`) not the tool (`psql.just`). The root Justfile is a thin orchestrator: `_default`, shortcut recipes, and project-wide recipes like `check`/`clean`.
8014. The root provides shortcut recipes for common workflows that delegate to modules, giving developers a flat namespace for everyday tasks.
8115. Use the standard recipe vocabulary below as the public API. Never invent `run-tests`, `do-lint`, `compile`, `format`.
8216. In modules, never use bare relative paths — module recipes run with the module's directory as CWD. Define `root := source_directory() / ".."` and reference files as `{{root}}/tests/run.sh`.
83
84## Standard Recipe Vocabulary
85
86A developer should be able to run `just test`, `just dev`, or `just check` in any project without guessing. Use these exact names; include only the ones that apply.
87
88| Recipe | Purpose | Include when |
89|--------|---------|--------------|
90| `dev` | Start dev environment (server, watch, REPL) | Project has a dev loop |
91| `test` | Run the test suite | Always |
92| `build` | Build or compile | Project has a build step |
93| `lint` | Run linters | Linters configured |
94| `fmt` | Format code | Formatters configured |
95| `check` | Run ALL quality gates (`check: lint test`) | Always |
96| `clean` | Remove build artifacts, caches, generated files | Project produces output |
97
98`check` is the meta-recipe — depend on the applicable gates and add format checks (`cargo fmt --check`, `ruff format --check`) as appropriate.
99
100## Namespacing by Concern
101
102Group by **domain**, not tool. Classify by purpose: a test that runs in Docker is a *testing* recipe (`test.just`), not a Docker recipe. A migration that uses kubectl is a *database* recipe (`db.just`).
103
104| Concern | Module | Typical recipes |
105|---------|--------|-----------------|
106| Development | `dev.just` | build, test, lint, fmt, bench |
107| Testing | `test.just` | run, list, watch, coverage |
108| Containers | `docker.just` | build, push, run, compose-up |
109| CI/CD | `ci.just` | lint, deploy, release |
110| Database | `db.just` | migrate, seed, reset, dump, restore |
111| Infrastructure | `infra.just` | plan, apply, destroy |
112| Kubernetes | `k8s.just` | apply, diff, rollback, logs |
113| Documentation | `docs.just` | build, serve, publish |
114
115Single-concern projects (e.g. a Go/Rust project with only build/test/lint/fmt) use one `dev.just`; the root still stays thin. Modules are self-contained: own variables, own `_default`, no cross-module recipe dependencies.
116
117## Templates & References
118
119- **Templates** (root + module, copy-and-adapt): `Templates.md`
120- **just language reference** (variables, args, deps, conditionals, attributes, functions, install): `References/Syntax.md`
121- **Recipe fragments by project type** (Terraform, Go, Python, Docker, Azure, Ansible): `References/Patterns.md`
122- **Makefile → just migration guide**: `References/MakeMigration.md`
123
124## Linting
125
126After creating or editing ANY Justfile or `.just` module, run the structural lint and fix every failure:
127
128```bash
129bun Tools/lint.ts <project-dir>
130```
131
132It checks file naming, the three-line header, `_default` as first recipe, the `--unsorted`/`--list-submodules` flags, `env_var_or_default()` usage, doc comments on all recipes, explicit module import paths, and section order. Any `FAIL` is a bug — fix and re-run until clean. The lint cannot judge concern-based naming, self-containment, or standard-vocabulary use — verify those by inspection (see `Workflows/CheckJustfile.md`).
133
134## Gotchas
135
136- **Each recipe line runs in a separate shell by default** — `cd foo` on one line and `ls` on the next runs `ls` in the original directory. Use a shebang recipe for multi-line scripts.
137- **`set dotenv-load` loads `.env` from the justfile directory, not the invocation directory** — running `just` from a subdirectory loads the parent's `.env`. Use `set dotenv-path` to override.
138- **Backtick variables evaluate at parse time, every invocation** — `git_hash := \`git rev-parse HEAD\`` runs git on every `just` call, even for unrelated recipes. Slow on large repos; move inside the recipe if not needed globally.
139- **Recipe arguments don't shell-quote automatically** — `just deploy "my server"` passes two args. Use `set positional-arguments` with `"$@"`, or wrap as `{{quote(target)}}`.
140- **`set shell := ["bash", "-c"]` breaks `set -euo pipefail` semantics** because each line is its own `-c` invocation — `pipefail` only applies within that line. Use shebang recipes for proper fail-fast scripts.
141- **`just --list` hides `_`-prefixed and `[private]` recipes** but they're still callable — obscurity, not access control.
142- **Cross-platform `[macos]`/`[linux]` attributes silently skip the recipe on other OSes** — running `just install` on Windows when only `[linux]`/`[macos]` variants exist exits 0 with no error, which looks like success.
143- **The lint validates structure, not behavior** — a recipe can pass every check and still run the wrong command. Run `just --dry-run <recipe>` to verify expansion.