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: justfile
3description: |
4 Create, edit, refactor, lint, and maintain Justfiles and `.just` module files using the `just`
5 command runner. ALWAYS use this skill when the user mentions justfile, Justfile, just recipes,
6 just modules, `.just` files, or asks to set up task automation with just. Also trigger when
7 migrating a Makefile to just, adding recipes or modules to an existing Justfile, or organizing
8 and documenting project commands. Covers house conventions, templates, namespacing by domain,
9 dotenv, cross-platform support, and a structural lint. NOT FOR file-based build dependency
10 graphs that need timestamp tracking (use make).
11allowed-tools: Read, Write, Edit, Bash, Grep, Glob
12---
13
14# Justfile Skill
15
16`just` is a command runner (not a build system) that saves and runs project-specific commands in a
17file called `Justfile`. It uses make-inspired syntax but is simpler and more portable, with none of
18make's idiosyncrasies (`.PHONY`, tab sensitivity, implicit rules, timestamp tracking).
19
20This skill enforces a consistent **house style** so every Justfile looks the same across projects.
21The rules below are the authoritative convention; `Tools/lint.ts` validates the deterministic ones.
22
23## Workflow Routing
24
25| Trigger | Workflow |
26|---------|----------|
27| "create a justfile", "set up just", "add a recipe/module" | `Workflows/CreateJustfile.md` |
28| "migrate Makefile to just", "convert make to just" | `Workflows/MigrateFromMake.md` |
29| "check/lint this justfile", "is this justfile correct" | `Workflows/CheckJustfile.md` |
30
31## When to Use Just vs Make
32
33| Scenario | Tool |
34|----------|------|
35| Project task automation (build, test, deploy, lint) | **just** |
36| Cross-platform command runner | **just** |
37| Actual file-based build dependencies (compile `.c` → `.o`) | **make** |
38| Legacy projects already deep in make | **make** (or migrate) |
39
40## Common Mistakes — do NOT do these
41
42Patterns the model often generates incorrectly. Check output against this list.
43
44| WRONG | RIGHT |
45|-------|-------|
46| `justfile` (lowercase) | `Justfile` (capital J) |
47| `mod docker` (bare) | `mod docker '.justfiles/docker.just'` |
48| Module at `docker.just` or `just/docker.just` | Module at `.justfiles/docker.just` |
49| `default:` | `_default:` (underscore required) |
50| `@just --list` | `@just --list --unsorted` (module) or `--unsorted --list-submodules` (root with modules) |
51| `env("NAME", "val")` | `env_var_or_default("NAME", "val")` |
52| Module file without the three-line header | Every file gets the full header |
53| Module file without its own `_default` recipe | Every file gets its own `_default` |
54| Module named after a tool (`psql.just`) | Module named after a concern (`db.just`) |
55| Tests in `docker.just` because they run in a container | Tests in `test.just` — classify by purpose, not implementation |
56| Root recipe duplicates module logic | Root shortcut delegates: `build: docker-build` |
57| Ad-hoc names (`run-tests`, `do-lint`) | Standard names: `test`, `lint`, `build`, `dev`, `fmt`, `check` |
58| Relative paths in module recipes (`bash tests/run.sh`) | Use `source_directory()` for absolute paths |
59
60## Mandatory Rules — apply to EVERY file you create or edit
61
621. The root file MUST be named `Justfile` (capital J).
632. EVERY file (root and every `.just` module) MUST start with this three-line header:
64 ```just
65 #!/usr/bin/env just --justfile
66 set shell := ["bash", "-euo", "pipefail", "-c"]
67 set dotenv-load := true
68 ```
69 Use `set dotenv-load := false` where appropriate, but the line must always be present.
703. EVERY file MUST have `_default` as its first recipe:
71 ```just
72 # List all available recipes
73 _default:
74 @just --list --unsorted
75 ```
76 The **root** Justfile with modules uses `@just --list --unsorted --list-submodules`. Module
77 files use `@just --list --unsorted` (no `--list-submodules`).
784. Section order in every file: **variables → mod imports → recipes**.
795. Module files MUST live at `.justfiles/<name>.just` — never `just/`, never beside the root.
806. Import modules with explicit paths: `mod name '.justfiles/name.just'` — never bare `mod name`.
817. Use `env_var_or_default("NAME", "value")` for variable defaults — never `env()`.
828. Every recipe gets a `#` doc comment on the line directly above it.
839. Parameterized recipes document each param: `# param - description (default: value)`.
8410. Private/helper recipes start with `_`.
8511. Dependencies go on the definition line: `build: _lint test`.
8612. Destructive recipes prompt for confirmation; the doc comment says "DESTRUCTIVE, prompts for confirmation".
8713. 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`.
8814. The root provides shortcut recipes for common workflows that delegate to modules, giving developers a flat namespace for everyday tasks.
8915. Use the standard recipe vocabulary below as the public API. Never invent `run-tests`, `do-lint`, `compile`, `format`.
9016. 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`.
91
92## Standard Recipe Vocabulary
93
94A 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.
95
96| Recipe | Purpose | Include when |
97|--------|---------|--------------|
98| `dev` | Start dev environment (server, watch, REPL) | Project has a dev loop |
99| `test` | Run the test suite | Always |
100| `build` | Build or compile | Project has a build step |
101| `lint` | Run linters | Linters configured |
102| `fmt` | Format code | Formatters configured |
103| `check` | Run ALL quality gates (`check: lint test`) | Always |
104| `clean` | Remove build artifacts, caches, generated files | Project produces output |
105
106`check` is the meta-recipe — depend on the applicable gates and add format checks (`cargo fmt --check`, `ruff format --check`) as appropriate.
107
108## Namespacing by Concern
109
110Group 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`).
111
112| Concern | Module | Typical recipes |
113|---------|--------|-----------------|
114| Development | `dev.just` | build, test, lint, fmt, bench |
115| Testing | `test.just` | run, list, watch, coverage |
116| Containers | `docker.just` | build, push, run, compose-up |
117| CI/CD | `ci.just` | lint, deploy, release |
118| Database | `db.just` | migrate, seed, reset, dump, restore |
119| Infrastructure | `infra.just` | plan, apply, destroy |
120| Kubernetes | `k8s.just` | apply, diff, rollback, logs |
121| Documentation | `docs.just` | build, serve, publish |
122
123Single-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.
124
125## Templates & References
126
127- **Templates** (root + module, copy-and-adapt): `Templates.md`
128- **just language reference** (variables, args, deps, conditionals, attributes, functions, install): `References/Syntax.md`
129- **Recipe fragments by project type** (Terraform, Go, Python, Docker, Azure, Ansible): `References/Patterns.md`
130- **Makefile → just migration guide**: `References/MakeMigration.md`
131
132## Linting
133
134After creating or editing ANY Justfile or `.just` module, run the structural lint and fix every failure:
135
136```bash
137bun Tools/lint.ts <project-dir>
138```
139
140It 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`).
141
142## Gotchas
143
144- **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.
145- **`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.
146- **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.
147- **Recipe arguments don't shell-quote automatically** — `just deploy "my server"` passes two args. Use `set positional-arguments` with `"$@"`, or wrap as `{{quote(target)}}`.
148- **`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.
149- **`just --list` hides `_`-prefixed and `[private]` recipes** but they're still callable — obscurity, not access control.
150- **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.
151- **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.