Makefiles and Taskfiles skill for Codex
One command to list, run and trust every task. Codex picks the right runner for the repo (Make for real file targets, just for readable aliases, Task for cross-platform teams), then writes recipes with pipefail shells, .PHONY targets, sources and generates checks and a built-in task list. Destructive tasks get prompts and environment guards, and CI is pointed at the same entry points developers use.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- makefiles-taskfiles Installs to ~/.agents/skills/makefiles-taskfiles/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install makefiles-taskfiles The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project makefiles-taskfiles Installs to .agents/skills/makefiles-taskfiles/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/makefiles-taskfiles.zipunzip -o makefiles-taskfiles.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) makefiles-taskfiles PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $makefiles-taskfiles, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes and repairs project command runners - GNU Make Makefiles, just justfiles and Taskfile.yml (go-task) - with self-documenting help, strict shells, real file targets, incremental sources/generates checks, safe argument passing and confirmation gates on destructive recipes. Use when a repo needs one entry point for build, test, lint, dev and release commands, when a Makefile has tab, .PHONY or $$ escaping bugs, when choosing between make, just and Task, or when porting npm scripts or shell scripts into a task runner.
Use it when
- A repo needs a single entry point for build, test, lint and dev commands
- A Makefile has tab, .PHONY, $$ escaping or pipefail bugs
- You are choosing between make, just and Task for a team
- Local commands and CI steps have drifted apart
Use something else when
- You need cached task graphs across many JS packages: use monorepo-tooling
- The real problem is slow CI pipelines rather than local commands: use ci-speedup
How to check the result
List tasks with make help, just --list or task --list, run each non-destructive task, deliberately break one command to confirm a non-zero exit, and run file-producing tasks twice to see the second skip.
The mistake it prevents
Leaving command targets out of .PHONY, so a stray file named test or build makes Make silently report the target as up to date.
Done well means: a newcomer (or Codex) runs one command to list every task, each task fails loudly on the first error, tasks that produce files skip work when inputs are unchanged, and nothing destructive runs without a confirmation.
Before writing anything
- Read
AGENTS.md,README,CONTRIBUTING.md, CI workflow files andpackage.jsonscripts. The runner must call the same commands CI calls, or the two drift. - Check what already exists:
Makefile,GNUmakefile,justfile/.justfile,Taskfile.yml/Taskfile.yaml. Extend the existing runner; do not add a second one. - Check tool availability:
make --version(macOS ships GNU Make 3.81 at/usr/bin/make, which lacks.ONESHELL,--output-syncand$(file ...)),just --version,task --version. Installing just or Task needs network and may need approval in the Codex sandbox; note it rather than assuming it is present.
Pick the runner
| Situation | Default | Why |
|---|---|---|
| Builds real files from other files (C, protobuf codegen, docs) | GNU Make | Timestamp-based rebuilds are its core strength |
| Only command aliases (test, lint, dev, deploy), team wants readable syntax | just | No tabs, no .PHONY, real arguments, just --list built in |
| Cross-platform team incl. Windows without WSL, YAML is accepted | Task (go-task v3) | Single static binary, checksum-based up-to-date checks, includes for monorepos |
| JS-only repo with a handful of scripts | package.json scripts | Do not add a runner for five commands |
| Already has a working Makefile | Keep Make | Migration costs more than syntax pain |
Make is installed almost everywhere; just and Task are one binary each but still an extra install. In repos used by agents, pick the one whose install is already documented.
GNU Make
Header every Makefile should have
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DEFAULT_GOAL := help
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables --no-builtin-rules
.PHONY: help
help: ## List available targets
@grep -E '^[a-zA-Z0-9_.-]+:.*?## ' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}'
.SHELLFLAGSwithpipefailmakescmd | tee logfail whencmdfails..DELETE_ON_ERRORremoves a half-written target so the next run does not treat it as up to date.- Every recipe line starts with a literal tab. Spaces give
missing separator.
Command targets vs file targets
.PHONY: test lint fmt dev
test: ## Run unit tests
go test ./...
lint: ## Static checks
golangci-lint run
# File target: rebuilt only when a .proto file is newer than the output
gen/api.pb.go: $(wildcard proto/*.proto)
protoc --go_out=gen proto/*.proto
build: bin/app ## Build the binary
bin/app: $(shell find . -name '*.go' -not -path './vendor/*') go.mod go.sum
go build -o $@ ./cmd/app
- Mark every non-file target
.PHONY, otherwise a file or folder namedtestmakesmake testreport “up to date”. - Use
$@(target),$<(first prerequisite),$^(all prerequisites). - Shell variables need
$$:for f in *.sql; do echo $$f; done. - Each recipe line runs in a new shell.
cd diron one line does not affect the next; writecd dir && cmdor use.ONESHELL:(GNU Make 3.82+ only).
Variables and arguments
ENV ?= dev # override: make deploy ENV=staging
VERSION := $(shell git describe --tags --always --dirty)
IMAGE := ghcr.io/acme/app:$(VERSION)
:=evaluates once;=re-evaluates on every use (slow with$(shell ...)).?=sets a default the caller can override from the command line or environment.- Make has no positional arguments. Use
VAR=value, never the$(MAKECMDGOALS)hack.
just
set dotenv-load
set shell := ["bash", "-euo", "pipefail", "-c"]
# List recipes (first recipe is the default)
default:
@just --list
# Run tests, optionally filtered: just test ./pkg/...
test *args:
go test {{args}}
# Build and tag an image
image tag=`git rev-parse --short HEAD`:
docker build -t ghcr.io/acme/app:{{tag}} .
[confirm("Drop and recreate the local database?")]
db-reset:
dropdb --if-exists app_dev && createdb app_dev && just migrate
[private]
migrate:
goose -dir migrations postgres "$DATABASE_URL" up
- Comments directly above a recipe become its
just --listdescription. - Recipes run each line in a fresh shell, like Make. For multi-line logic use a shebang
recipe (first line
#!/usr/bin/env bashplusset -euo pipefail). {{args}}is substituted unquoted. If arguments may contain spaces, addset positional-argumentsand use"$@"inside the recipe instead.[confirm]requires a reasonably recent just; checkjust --versionand fall back to an explicitread -pprompt in a shebang recipe on old versions.[group('db')]organises long lists;just --list --unsortedkeeps file order.
Task (go-task v3)
version: '3'
dotenv: ['.env']
vars:
VERSION:
sh: git describe --tags --always --dirty
tasks:
default:
cmds: [task --list]
silent: true
test:
desc: Run unit tests (extra args after --)
cmds:
- go test ./... {{.CLI_ARGS}}
build:
desc: Build the binary
sources: ['**/*.go', go.mod, go.sum]
generates: [bin/app]
cmds:
- go build -ldflags "-X main.version={{.VERSION}}" -o bin/app ./cmd/app
db:reset:
desc: Drop and recreate the local database
prompt: This deletes all local data. Continue?
preconditions:
- sh: '[ "$APP_ENV" != "production" ]'
msg: Refusing to reset a production database
cmds:
- dropdb --if-exists app_dev
- createdb app_dev
task --listshows only tasks with adesc;task --list-allshows everything.sources/generatesuse checksums by default (stored in.task/, add it to.gitignore);method: timestampswitches to mtime.{{.CLI_ARGS}}receives everything after--:task test -- -run TestLogin.depsrun in parallel; use sequentialcmds: [{task: a}, {task: b}]when order matters.includes:pulls in per-package Taskfiles in monorepos (task api:test).--yesauto-confirms prompts. Never put it in CI for destructive tasks.
Hard rules
- The runner calls the same scripts CI calls. Better still, CI calls the runner
(
make ci,just ci,task ci) so local and CI cannot diverge. - Destructive or outward-facing tasks (
deploy,db-reset,release,publish,cleanthat deletes outside build dirs) need a confirmation gate and an environment guard. When Codex is asked to run one, it asks the user first even if the task has--yes. - No secrets in the runner file. Read them from the environment or
.env(gitignored). - Keep recipes thin: more than ~10 lines of shell belongs in
scripts/<name>.shwithset -euo pipefail, called from the runner. - Do not shadow real tools: a Make target named
dockerorgoconfuses everyone. - One naming scheme: verbs (
test,lint,build), namespaced with:in Task or-in Make/just (db-migrate).
Gotchas
| Symptom | Cause | Fix |
|---|---|---|
missing separator. Stop. | Spaces instead of a tab | Re-indent recipe lines with tabs; check editorconfig |
make test says nothing to do | File/dir named test and no .PHONY | Add .PHONY: test |
$f is empty in a loop | Make expanded $f | Escape as $$f |
| Pipeline failure ignored | Default sh -c without pipefail | SHELL := bash plus .SHELLFLAGS |
| Works on Linux, breaks on macOS | GNU Make 3.81 or BSD sed -i | Avoid .ONESHELL; use sed -i.bak or a script |
| Task never re-runs build | generates missing or wrong glob | Run task build --force, fix globs, task --status build |
just recipe ignores .env | dotenv-load not set | Add set dotenv-load |
Verification
make -n deploy ENV=staging # dry run: print commands without executing
make help && make test
just --list && just --dry-run image
task --list && task --dry build && task build && task build # 2nd run: "up to date"
- Run each new task once (non-destructive ones) and confirm the exit code is non-zero when you break the underlying command on purpose.
- For file targets, run twice: the second run must do nothing.
- Update
AGENTS.mdwith the runner commands so Codex and humans use them.
Output contract
When adding or rewriting a runner, report:
**Runner:** justfile (just 1.x required; install documented in README)
**Tasks:** default (list), test, lint, fmt, dev, build, image, db-reset (confirm)
**CI:** .github/workflows/ci.yml now calls `just ci`
**Checked:** `just --list`, `just test`, `just lint` pass; `just --dry-run image` shown
**Not run:** db-reset, deploy (destructive; need your go-ahead) What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Makefiles and Taskfiles"
short_description: "One command to list, run and trust every task"
default_prompt: "$makefiles-taskfiles Add a Makefile with help, test, lint and build targets that match our CI" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.