Makefile Best Practices
With several repos kept alive at once, the usual bottleneck is not the language. It is “how does this one boot.” The default front door is GNU Make. Not to compile C — because make help outlives a museum of commands in the README.
The problem is the entrance, not the compiler
Textbook gcc / .o samples are not the job. The AI Simi repos almost never use Make that way.
What has to be solved:
- A single first command after a directory sits untouched for three months
- A new agent (Copilot, Claude Code, Leo) that does not guess where the start script lives
- The same actions on a Mac and on a VPS, without a second dialect
The convention is narrow: a Makefile at the repo root, default target help, every supported action a named target. Docs say “run make help.” They do not paste a page of raw shell.
That is convention over configuration as daily practice.
The smallest shape that works
This is the skeleton in the AI Simi repo, not a hello.c tutorial. The dependency graph is boring on purpose: a human remembers one word, the machine expands the rest.
SHELL := /bin/bash
.DEFAULT_GOAL := help
.PHONY: help
help: ## List commands
@awk 'BEGIN {FS = ":.*?## "} \
/^## / { printf "\n%s\n", substr($$0, 4) } \
/^[a-zA-Z0-9_-]+:.*?## / { printf " %-22s %s\n", $$1, $$2 }' \
$(MAKEFILE_LIST)
.PHONY: setup
setup: setup-env setup-workspace ## Full bootstrap
.PHONY: setup-env
setup-env: ## Create the local config directory
@bash scripts/setup-env.sh
.PHONY: agent-status
agent-status: ## Show every agent
@bash scripts/agent-status.sh
Four rules:
.DEFAULT_GOAL := help— a baremakemust not build the world..PHONY— these names are not files. Miss it, and a same-named file in the directory silently skips the recipe. Official note: Phony Targets.##plusawk— help text lives next to the implementation, so the README cannot drift.- Targets orchestrate. Messy work goes in
scripts/*.sh. A Makefile is not a graveyard for bash.
Only a thin slice of the GNU Make manual is in use: variables, prerequisites, phony targets, the default goal.
Check the door:
make
make help
make -n agent-statusmake -n prints the recipe and does not run it. Glance at that before a destructive target.
How a failure shows up
A missing .PHONY fails quietly. If a file named help exists in the directory, make help reports it is already up to date and never runs the recipe.
touch help
make helpOutput like make: 'help' is up to date. is the trap. Delete the file. Add .PHONY: help.
A real recipe failure stops Make at the first non-zero exit. Do not prefix the line with - to swallow the error unless that failure is deliberately ignorable. make -d is too loud. Start with make -n, then run the script on its own.
Why not npm scripts, just, or Task
Those tools work. Make wins for concrete reasons:
| Option | When it fits | Why it is usually skipped |
|---|---|---|
| npm scripts | A JS-only frontend | A Go + Flutter + agent studio should not need Node to type dev |
| just | A new repo on a team that already standardized | Another binary on every machine; agent images may not have it |
| Task | A team that already speaks YAML | Another DSL to remember |
| Makefile | Cross-language, cross-machine, shared with agents | The syntax is occasionally ugly; almost every Unix already has it |
The constraint is “the next machine should be zero-friction.” Make is already on macOS developer tools and typical Ubuntu images. That beats prettier syntax.
Agents need the door more than humans do
In 2026 more operations go to agents. They follow a convention well. They choose poorly among five READMEs.
The first line in an agent-facing repo is usually: run make help, use only listed targets. That is more stable than pasting twenty lines of shell into a prompt, and safer than letting the model invent docker compose flags.
The failure mode is a Makefile full of undocumented targets. Help text is an interface, not politeness.
Hard rules
- kebab-case targets:
agent-up-leo, notagentUpLeo - One job per target.
setupmay depend on smaller ones; it should not hide an 80-line recipe - Anything that needs secrets refers to variable names and
~/.simi/paths, never values in git - Destructive actions (wipe data, force push, post to production) are never the default target
- Prefer POSIX tools already on the machine over a Python one-liner wrapped for its own sake
The smell for the last rule: python3 -c novels inside the Makefile. That is the moment to split a script under scripts/.
When not to use Make
- The incremental compile graph is genuinely large (big C/C++ / kernels) — use the language’s own build system
- The team already standardized on Taskfile — another dialect is not a personality
- Native Windows without GNU Make — not the daily environment here, and the convention will not be warped for it
The boundary: Make is how a human or an agent enters. The language toolchain is how code becomes an artifact. Mix them, and the README and CI rot together.