name: make description: >- Guide for writing idiomatic, maintainable Makefiles following modern best practices. Enforces safety headers (SHELL, .SHELLFLAGS, .DELETE_ON_ERROR), self-documenting help, .PHONY declarations, modular includes. Triggers on "makefile", "Makefile", "make target", "make rule", "make pattern", "make help", "make template", ".mk file", "make include", "make variable", "make phony", "make dependency", "build system", "make clean", "make install", "make test", "make build", "make all", "make default", "task runner", "make automation", "GNU make", "make recipe", "make prerequisite". PROACTIVE: MUST invoke when writing ANY Makefile or *.mk file. allowed-tools: Read, Write, Edit, Bash, Glob, Grep
ABOUTME: Make skill for idiomatic, maintainable Makefiles with modular patterns
ABOUTME: Emphasizes safety, self-documentation, modularity, and orchestration focus
Make Skill
Quick Reference
| Rule | Enforcement |
|---|---|
| Safety First | Always set SHELL, .SHELLFLAGS, .DELETE_ON_ERROR |
| Self-Documenting | Every target has ## comment; use ##@ for groups |
| .PHONY | Declare ALL non-file targets as phony |
| Explicit | No magic; clear variable names and dependencies |
| Modular | Split into *.mk files; use include |
| Orchestration | Make orchestrates; scripts do complex logic |
| Naming | Hyphen-case; verb-noun prefixes (e.g., stack-up) |
| Help Default | .DEFAULT_GOAL := help |
🛑 FILE OPERATION CHECKPOINT (BLOCKING)
Before EVERY Write or Edit tool call on a Makefile or *.mk file:
╔══════════════════════════════════════════════════════════════════╗
║ 🛑 STOP - MAKE SKILL CHECK ║
║ ║
║ You are about to modify a Makefile. ║
║ ║
║ QUESTION: Is /make skill currently active? ║
║ ║
║ If YES → Proceed with the edit ║
║ If NO → STOP! Invoke /make FIRST, then edit ║
║ ║
║ This check applies to: ║
║ ✗ Write tool with file_path containing "Makefile" ║
║ ✗ Edit tool with file_path containing "Makefile" ║
║ ✗ Write/Edit with file_path ending in .mk ║
║ ✗ ANY Makefile, regardless of conversation topic ║
║ ║
║ Examples that REQUIRE this skill: ║
║ - "add a build target" (edits Makefile) ║
║ - "update the docker targets" (edits make/docker.mk) ║
║ - "fix the help target" (edits any Makefile) ║
╚══════════════════════════════════════════════════════════════════╝
Why this matters: Makefiles without safety headers can fail silently or
produce corrupt builds. The skill ensures .DELETE_ON_ERROR and proper .PHONY.
🔄 RESUMED SESSION CHECKPOINT
When a session is resumed from context compaction, verify Makefile development state:
┌─────────────────────────────────────────────────────────────┐
│ SESSION RESUMED - MAKE SKILL VERIFICATION │
│ │
│ Before continuing Makefile implementation: │
│ │
│ 1. Was I in the middle of writing Makefiles? │
│ → Check summary for "Makefile", "make target", ".mk" │
│ │
│ 2. Did I follow all Make skill guidelines? │
│ → Safety headers (SHELL, .SHELLFLAGS, .DELETE_ON_ERROR) │
│ → .PHONY declarations for non-file targets │
│ → Self-documenting help target │
│ → ABOUTME headers on new files │
│ │
│ 3. Check Makefile quality before continuing: │
│ → Run: make -n <target> (dry run) │
│ → Verify help target works: make help │
│ │
│ If implementation was in progress: │
│ → Review the partial Makefile for completeness │
│ → Ensure safety headers are present │
│ → Verify no recipes exceed 5 lines (move to scripts) │
│ → Re-invoke /make if skill context was lost │
└─────────────────────────────────────────────────────────────┘
When to Use Make
Use Make for orchestration and build tasks:
- Build automation (compile, link, bundle)
- Development workflow commands (start, stop, test, lint)
- Multi-service orchestration (Docker, Terraform)
- CI/CD pipeline steps
- Task runners with dependencies
Do NOT use Make for:
- Complex business logic (use Python, Go, etc.)
- Scripts requiring conditionals/loops (use Bash scripts)
- Anything requiring error recovery
- Configuration management (use dedicated tools)
Size guideline: If a Makefile exceeds ~300 lines, split into modular *.mk files.
Recipe rule: If a recipe exceeds 5 lines or contains if/else, move it to a script and invoke the /bash skill.
Core Principles
1. Safety Headers
Every Makefile starts with strict settings (like Bash's set -euo pipefail):
SHELL := bash
.SHELLFLAGS := -eu -o pipefail -c
.DELETE_ON_ERROR:
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
| Setting | Purpose |
|---|---|
SHELL := bash | Use Bash instead of /bin/sh |
.SHELLFLAGS | -e exit on error, -u error on undefined, -o pipefail |
.DELETE_ON_ERROR | Remove target if recipe fails (prevents corrupt state) |
--warn-undefined-variables | Catch typos in variable names |
--no-builtin-rules | Disable implicit rules for clarity |
2. Self-Documenting Help System
Every Makefile must have a help target as default:
.DEFAULT_GOAL := help
##@ General
.PHONY: help
help: ## Display this help
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} \
/^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-20s\033[0m %s\n", $$1, $$2 } \
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
Documentation syntax:
##@ Section Header # Creates bold section in help output
target: ## Description # Documents the target
3. Phony Targets
Declare ALL non-file-producing targets:
.PHONY: help build test clean deploy
.PHONY: docker-up docker-down
.PHONY: logs-% # Pattern rules too
Why: Prevents conflicts with files named build, test, etc.
4. Explicit Over Implicit
- Use
$(VARIABLE)not$VARIABLE - Quote paths:
"$(PATH_VAR)" - Name variables clearly:
DOCKER_COMPOSE_FILESnotDCF - Avoid automatic variables in complex contexts
5. Modular Design
Split large Makefiles into focused modules:
# Root Makefile
include make/common.mk
include make/docker.mk
include make/test.mk
# Optional includes (won't fail if missing)
-include make/local.mk
See resources/common.mk for a reusable library pattern.
6. Orchestration Focus
Make orchestrates; it doesn't implement:
# GOOD: Make orchestrates
test: ## Run tests
@./scripts/run-tests.sh
# BAD: Complex logic in Make
test:
@if [ -f .env ]; then \
source .env && \
for dir in $(TEST_DIRS); do \
cd $$dir && npm test || exit 1; \
done \
fi
7. Consistent Naming
Prefixes for semantic grouping:
| Prefix | Purpose | Example |
|---|---|---|
stack- | Full application stack | stack-up, stack-down |
infra- | Infrastructure only | infra-network, infra-wait |
docker- | Docker operations | docker-build, docker-ps |
test- | Test execution | test-unit, test-e2e |
db- or migrate- | Database operations | db-migrate, db-seed |
Verbs:
| Verb | Meaning |
|---|---|
up / start | Create and run |
down / stop | Stop and remove |
restart | Stop then start |
build | Build only |
rebuild | Build and start |
logs | Stream logs |
status | Show current state |
Standard Template
Use resources/template.mk as a starter:
cp ~/.claude/skills/make/resources/template.mk ./Makefile
The template includes:
- Safety headers
- Color definitions
- Logging macros
- Help system
- Example targets
Variable Patterns
Path Resolution (Portable)
# Get directory containing this Makefile
MAKEFILE_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# Project root (if Makefile is in project root)
PROJECT_ROOT := $(MAKEFILE_DIR)
# Relative paths from Makefile location
SRC_DIR := $(PROJECT_ROOT)src
BUILD_DIR := $(PROJECT_ROOT)build
Default Values
# Use ?= for overridable defaults
COMPOSE_PROJECT_NAME ?= myproject
PORT ?= 8080
# Use := for computed values
TIMESTAMP := $(shell date +%Y%m%d-%H%M%S)
GIT_SHA := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
Environment Exports
# Export for child processes
export DOCKER_BUILDKIT := 1
export COMPOSE_DOCKER_CLI_BUILD := 1
# Pass through from environment
export API_KEY
export DATABASE_URL
Conditional Variables
# Makefile conditionals (evaluated at parse time)
ifeq ($(CI),true)
VERBOSE := 1
endif
ifdef DEBUG
BUILD_FLAGS += -v
endif
ifndef REQUIRED_VAR
$(error REQUIRED_VAR is not set)
endif
Macros (define/endef)
Logging Macros
# Color codes
CYAN := \033[0;36m
GREEN := \033[0;32m
YELLOW := \033[1;33m
RED := \033[0;31m
BOLD := \033[1m
NC := \033[0m
define log_info
@printf "$(CYAN)[INFO]$(NC) %s\n" "$(1)"
endef
define log_success
@printf "$(GREEN)[OK]$(NC) %s\n" "$(1)"
endef
define log_warn
@printf "$(YELLOW)[WARN]$(NC) %s\n" "$(1)"
endef
define log_error
@printf "$(RED)[ERROR]$(NC) %s\n" "$(1)"
endef
define log_step
@printf "$(BOLD)>>> %s$(NC)\n" "$(1)"
endef
# Usage
build:
$(call log_step,Building project)
@go build ./...
$(call log_success,Build completed)
Utility Macros
# Check if command exists
define check_cmd
@command -v $(1) >/dev/null 2>&1 || { \
printf "$(RED)[ERROR]$(NC) $(1) is required but not installed\n"; \
exit 1; \
}
endef
# Ensure directory exists
define ensure_dir
@mkdir -p $(1)
endef
# Confirm dangerous action
define confirm
@read -p "Are you sure? [y/N] " ans && [ "$${ans:-N}" = "y" ]
endef
# Usage
deploy: ## Deploy to production
$(call check_cmd,kubectl)
$(call confirm)
$(call log_step,Deploying to production)
@kubectl apply -f manifests/
Pattern Rules
Dynamic Targets with %
# logs-<service> - tail logs for any service
.PHONY: logs-%
logs-%: ## Tail logs for a specific service
docker compose logs -f $*
# docker-exec-<service> - exec into any container
.PHONY: exec-%
exec-%: ## Execute shell in a container
docker compose exec $* sh
# build-<component> - build specific component
.PHONY: build-%
build-%:
$(call log_step,Building $*)
@cd $* && make build
Automatic variables:
| Variable | Meaning |
|---|---|
$@ | Target name |
$< | First prerequisite |
$^ | All prerequisites |
$* | Stem matched by % |
No-op Targets for Arguments
# Allow: make logs backend
# These are filter arguments, not real targets
.PHONY: backend frontend api worker
backend frontend api worker:
@:
.PHONY: logs
logs: ## Tail logs (usage: make logs <service>)
@component="$(filter-out $@,$(MAKECMDGOALS))"; \
if [ -n "$$component" ]; then \
docker compose logs -f $$component; \
else \
docker compose logs -f; \
fi
Modular Library Pattern
Structure
project/
├── Makefile # Root: includes modules, defines high-level targets
└── make/
├── common.mk # Shared: variables, colors, logging macros
├── docker.mk # Docker Compose operations
├── test.mk # Test orchestration
└── app-python.mk # Language-specific targets
Root Makefile
# Include order matters: common first, then specific
include make/common.mk
include make/docker.mk
include make/test.mk
# Optional local overrides
-include make/local.mk
##@ Stack Management
.PHONY: up
up: docker-up ## Start all services
$(call log_success,Stack is running)
.PHONY: down
down: docker-down ## Stop all services
$(call log_success,Stack stopped)
Consumer Pattern (Multi-Repo)
For projects that consume a shared Makefile library:
# Define path to shared infra
INFRA_DIR ?= $(HOME)/projects/shared-infra
# Clone if missing
$(if $(wildcard $(INFRA_DIR)),,$(shell git clone git@github.com:org/shared-infra.git $(INFRA_DIR)))
# Include shared modules
-include $(INFRA_DIR)/make/common.mk
-include $(INFRA_DIR)/make/app.mk
-include $(INFRA_DIR)/make/app-python.mk
See resources/common.mk for a complete reusable library.
Dependency Management
Prerequisite Chains
# Direct dependencies
deploy: build test ## Deploy (requires build and test)
@./scripts/deploy.sh
# Chain dependencies
clean-all: clean-build clean-test clean-docker
# Order-only prerequisites (directory must exist, but changes don't trigger rebuild)
$(BUILD_DIR)/output: source.c | $(BUILD_DIR)
gcc -o $@ $<
$(BUILD_DIR):
mkdir -p $@
Conditional Dependencies
.PHONY: start
start: ## Start services (optionally with migrations)
ifeq ($(MIGRATE),1)
$(call log_info,Running migrations first)
@$(MAKE) db-migrate
endif
@docker compose up -d
Sentinel Files (Complex Dependencies)
For dependencies that don't produce predictable files (like npm install):
# Sentinel file tracks when install was last run
.stamps:
@mkdir -p .stamps
.stamps/npm-install: package.json package-lock.json | .stamps
npm ci
@touch $@
.stamps/pip-install: requirements.txt | .stamps
pip install -r requirements.txt
@touch $@
# Depend on sentinel, not directory
build: .stamps/npm-install
npm run build
clean:
rm -rf .stamps node_modules
Integration Patterns
Docker Compose
# Compose file configuration
COMPOSE_FILES := -f docker-compose.yml
ifdef CI
COMPOSE_FILES += -f docker-compose.ci.yml
endif
COMPOSE := docker compose $(COMPOSE_FILES)
.PHONY: docker-up
docker-up: ## Start Docker services
$(COMPOSE) up -d
.PHONY: docker-down
docker-down: ## Stop Docker services
$(COMPOSE) down
Terraform
TF_DIR := terraform
.PHONY: tf-init
tf-init: ## Initialize Terraform
cd $(TF_DIR) && terraform init
.PHONY: tf-plan
tf-plan: ## Plan Terraform changes
cd $(TF_DIR) && terraform plan -out=tfplan
.PHONY: tf-apply
tf-apply: ## Apply Terraform changes
$(call confirm)
cd $(TF_DIR) && terraform apply tfplan
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Spaces instead of tabs | Recipes won't run | Use tabs for recipe lines |
| Missing .PHONY | Target skipped if file exists | Declare all non-file targets |
Bare rm without -f | Fails if file missing | Use rm -f or rm -rf |
| Ignoring errors silently | @command || true hides failures | Handle errors explicitly |
| Complex shell logic | Unmaintainable, error-prone | Move to script; invoke /bash |
| Recursive make without $(MAKE) | Breaks parallelism, options | Use $(MAKE) -C subdir |
| Hardcoded paths | Not portable | Use variables with defaults |
| No help target | Users don't know available commands | Always provide help |
Excessive @ silencing | Can't debug issues | Only silence noise, not errors |
Validation
Run the validation script to check Makefile quality:
~/.claude/skills/make/scripts/validate_makefile.sh [Makefile]
The script checks:
- Safety headers present
- Help target exists
.PHONYdeclarations- Forbidden patterns (complex shell logic, missing error handling)
- Uses
checkmakeif available
Checklist
Before committing a Makefile:
- Safety headers present (
SHELL,.SHELLFLAGS,.DELETE_ON_ERROR) -
.DEFAULT_GOAL := helpset -
helptarget with awk-based documentation parser - All non-file targets declared
.PHONY - Variables use
$(VAR)syntax (not$VAR) - Paths are quoted where needed
- No recipes exceed 5 lines (moved to scripts)
- No complex shell logic (if/else, loops) in recipes
- Logging macros used for user feedback
- Dependencies declared correctly
- Runs
validate_makefile.shwithout errors - Tested with
make -n <target>(dry run)