makefile-conventions
Structures Makefiles as a universal command interface using modular includes and self-documenting help. Use when the user wants to add a make target, create a Makefile, organize build commands, set up a make-based workflow, or asks how to structure make targets. Also activate before modifying any Makefile or mk/ file in this project.
Use Make as the universal command interface. All project
operations are accessible via make, regardless of the
underlying implementation language.
Workflow
-
Investigate the project — Before writing targets, check:
- Does a Makefile already exist? Read it.
- What scripts, tools, or build steps does the project use?
- Is there an existing
mk/directory?
-
Decide placement — If the target belongs to an existing concern group, add it there. If it starts a new concern, create a new modular makefile. See makefile-structure for the full modular pattern.
-
Write the target — Follow these conventions:
- Declare every target
.PHONYunless it produces a file - Add a
## Descriptioncomment for the help system - Delegate complex logic to scripts
- Use
@prefix for clean output - Quote variable references:
"$(VAR)"
- Declare every target
-
Update help — Add the new target to the appropriate concern group in
make helpoutput. -
Verify — Run
make helpto confirm the target appears. Run the target to confirm it works.
Complete Example
Root Makefile pattern — includes domain modules and provides grouped help:
.DEFAULT_GOAL := help
include mk/test.mk
include mk/deploy.mk
.PHONY: help
help: ## Show this help
@echo "Project - Make Commands"
@echo "======================"
@echo ""
@echo "Testing:"
@echo " make test - Show test help"
@echo " make test sh - Run shellcheck"
@echo ""
@echo "Development:"
@echo " make check - Check prerequisites"
For the full modular delegation pattern (domain .mk
files, subdirectory Makefiles, subcommand delegation), see
makefile-structure.
Conventions
Structure
- One main
Makefileat the project root - Modular makefiles in
mk/for each concern domain - Domains with subcommands get
mk/<domain>/Makefile .DEFAULT_GOAL := helpso baremakeshows help- Never use
%: @:catch-all patterns. Use conditional explicit subcommand declarations instead (see Subcommand Delegation below).
Naming
- Targets: lowercase, hyphenated for multi-word
- Subcommands: space separation (
make test sh) - Variables: UPPER_CASE
Help system
makeormake helpshows all commands grouped by concernmake <domain>shows domain-specific help- When a domain has 2 or more subcommands, bare
make <domain>must show subcommand help. Do not make it execute a default action.
Delegation
Makefiles are the interface, not the implementation:
deploy:
@./scripts/deploy.sh "$(ENV)"
Subcommand delegation
When a domain uses $(MAKE) -C for space-separated
subcommands (make db migrate), Make sees migrate as a
separate goal and errors. Solve this with conditional
explicit targets — not a catch-all:
.PHONY: db
db:
@$(MAKE) -C mk/db $(filter-out $@,$(MAKECMDGOALS))
# Only activate when 'db' is on the command line
ifneq ($(filter db,$(MAKECMDGOALS)),)
.PHONY: migrate rollback
migrate rollback:
@:
endif
This ensures:
make db migrate— works (migrate is a known no-op)make db typo— errors (typo has no rule)make typo— errors (conditional is inactive)
Never use %: @: — it swallows all unknown targets
silently, making typos invisible.
Example Scenario
User: "Add a make target for running database migrations"
- Reads existing Makefile — finds
mk/structure - No existing
db.mk— createsmk/db.mkandmk/db/Makefilewithmigrate,rollback,status - Adds
include mk/db.mkto main Makefile - Adds Database section to
make helpoutput - Verifies:
make db migrateworks
Common Failures
- No help text — targets without descriptions are
undiscoverable. Always update
make help. - Logic in Makefiles — complex bash in a target is hard to debug. Delegate to scripts.
- Catch-all swallows errors —
%: @:silently succeeds for ANY unknown target, including typos within domains (make skills typo). Never use a blanket catch-all. Instead, declare valid subcommands explicitly inside conditional blocks.
├── references/
SKILL.md | | Raw
Makefile Conventions
Use Make as the universal command interface. All project
operations are accessible via make, regardless of the
underlying implementation language.
Workflow
-
Investigate the project — Before writing targets, check:
- Does a Makefile already exist? Read it.
- What scripts, tools, or build steps does the project use?
- Is there an existing
mk/directory?
-
Decide placement — If the target belongs to an existing concern group, add it there. If it starts a new concern, create a new modular makefile. See makefile-structure for the full modular pattern.
-
Write the target — Follow these conventions:
- Declare every target
.PHONYunless it produces a file - Add a
## Descriptioncomment for the help system - Delegate complex logic to scripts
- Use
@prefix for clean output - Quote variable references:
"$(VAR)"
- Declare every target
-
Update help — Add the new target to the appropriate concern group in
make helpoutput. -
Verify — Run
make helpto confirm the target appears. Run the target to confirm it works.
Complete Example
Root Makefile pattern — includes domain modules and provides grouped help:
.DEFAULT_GOAL := help
include mk/test.mk
include mk/deploy.mk
.PHONY: help
help: ## Show this help
@echo "Project - Make Commands"
@echo "======================"
@echo ""
@echo "Testing:"
@echo " make test - Show test help"
@echo " make test sh - Run shellcheck"
@echo ""
@echo "Development:"
@echo " make check - Check prerequisites"
For the full modular delegation pattern (domain .mk
files, subdirectory Makefiles, subcommand delegation), see
makefile-structure.
Conventions
Structure
- One main
Makefileat the project root - Modular makefiles in
mk/for each concern domain - Domains with subcommands get
mk/<domain>/Makefile .DEFAULT_GOAL := helpso baremakeshows help- Never use
%: @:catch-all patterns. Use conditional explicit subcommand declarations instead (see Subcommand Delegation below).
Naming
- Targets: lowercase, hyphenated for multi-word
- Subcommands: space separation (
make test sh) - Variables: UPPER_CASE
Help system
makeormake helpshows all commands grouped by concernmake <domain>shows domain-specific help- When a domain has 2 or more subcommands, bare
make <domain>must show subcommand help. Do not make it execute a default action.
Delegation
Makefiles are the interface, not the implementation:
deploy:
@./scripts/deploy.sh "$(ENV)"
Subcommand delegation
When a domain uses $(MAKE) -C for space-separated
subcommands (make db migrate), Make sees migrate as a
separate goal and errors. Solve this with conditional
explicit targets — not a catch-all:
.PHONY: db
db:
@$(MAKE) -C mk/db $(filter-out $@,$(MAKECMDGOALS))
# Only activate when 'db' is on the command line
ifneq ($(filter db,$(MAKECMDGOALS)),)
.PHONY: migrate rollback
migrate rollback:
@:
endif
This ensures:
make db migrate— works (migrate is a known no-op)make db typo— errors (typo has no rule)make typo— errors (conditional is inactive)
Never use %: @: — it swallows all unknown targets
silently, making typos invisible.
Example Scenario
User: "Add a make target for running database migrations"
- Reads existing Makefile — finds
mk/structure - No existing
db.mk— createsmk/db.mkandmk/db/Makefilewithmigrate,rollback,status - Adds
include mk/db.mkto main Makefile - Adds Database section to
make helpoutput - Verifies:
make db migrateworks
Common Failures
- No help text — targets without descriptions are
undiscoverable. Always update
make help. - Logic in Makefiles — complex bash in a target is hard to debug. Delegate to scripts.
- Catch-all swallows errors —
%: @:silently succeeds for ANY unknown target, including typos within domains (make skills typo). Never use a blanket catch-all. Instead, declare valid subcommands explicitly inside conditional blocks.
references/makefile-structure.md | | Raw
Makefile Structure
Directory layout
project/
├── Makefile # Entry point, includes mk/*.mk
└── mk/
├── test.mk # Delegates to mk/test/Makefile
├── deploy.mk # Delegates to mk/deploy/Makefile
├── test/
│ └── Makefile # Subcommands: sh, yaml, md, html
└── deploy/
└── Makefile # Subcommands: staging, production
Main Makefile
The root Makefile includes all domain modules and provides the top-level help:
.DEFAULT_GOAL := help
include mk/test.mk
include mk/deploy.mk
.PHONY: help
help: ## Show this help
@echo "Project Name - Make Commands"
@echo "============================"
@echo ""
@echo "Testing:"
@echo " make test - Show test help"
@echo " make test sh - Run shellcheck"
@echo " make test yaml - Run yamllint"
@echo ""
@echo "Deployment:"
@echo " make deploy - Show deploy help"
@echo " make deploy production - Deploy to production"
Domain module (mk/test.mk)
Each .mk file delegates to a subdirectory Makefile:
.PHONY: test
test:
@$(MAKE) -C mk/test $(filter-out $@,$(MAKECMDGOALS))
# Valid subcommands for 'make test <sub>'
ifneq ($(filter test,$(MAKECMDGOALS)),)
.PHONY: sh yaml md html
sh yaml md html:
@:
endif
The filter-out pattern passes subcommands through:
make test sh calls $(MAKE) -C mk/test sh.
The conditional block declares valid subcommands as explicit
no-op targets, active only when test is on the command
line. This prevents "No rule to make target" for valid
subcommands while still erroring on typos like
make test typo.
Domain Makefile (mk/test/Makefile)
Contains the actual targets and domain-specific help:
.DEFAULT_GOAL := help
.PHONY: help sh yaml
help:
@echo "Testing"
@echo "======="
@echo ""
@echo "Commands:"
@echo " make test sh - Run shellcheck"
@echo " make test yaml - Run yamllint"
sh:
@./scripts/test-shell.sh
yaml:
@yamllint .
Simple targets
Not every target needs the delegation pattern. Simple,
standalone targets go directly in the main Makefile or
a flat .mk file:
.PHONY: check
check: ## Check development prerequisites
@./scripts/check-prereqs.sh
.PHONY: clean
clean: ## Remove build artifacts
@rm -rf dist/ build/
Use the modular pattern when a domain has multiple subcommands. Use flat targets when there is only one command for the concern.
makefile-conventions structures Makefiles as a universal command interface with modular includes and self-documenting help.
Why Make is the interface and scripts are the implementation
Makefiles become unreadable when they contain logic. Delegating to scripts keeps make targets as a discoverable command menu while scripts handle complexity. Each can be tested independently.
Why the catch-all target pattern is banned
%: @: silently swallows typos. make tset succeeds
with no output instead of failing. Explicit targets
fail loud on typos, which is correct behavior.
Why bare domain commands show help
When make chat has multiple subcommands, running it
bare should show what's available — not silently do
nothing or pick a default. Discoverability over
convenience.
Why targets are split into modular mk/ files
A single Makefile grows unwieldy past 100 lines. Splitting by domain (chat.mk, dev.mk, skills.mk) lets teams own their section without merge conflicts. The indirection cost is low; the organization value is high.
[1.0.1] - 2026-06-11
Changed
- Replaced catch-all
%: @:pattern with conditional explicit subcommand declarations throughout
Added
- Warning against placing
%: @:catch-all in root Makefile (Common Failures, Structure conventions, and reference doc) - RATIONALE.md explaining design decisions
[1.0.0] — 2026-05-24
Added
- SKILL.md with workflow for creating and organizing Make targets
- Modular Makefile structure reference with delegation pattern
- Conventions for naming, help system, and script delegation