Makefile Helper
Create Makefiles that are simple, discoverable, and maintainable.
Core Principles
- •Default to rich help - Use categorized help with emoji headers unless user requests minimal
- •Ask about structure upfront - For new Makefiles, ask: "Flat or modular? Rich help or minimal?"
- •Follow existing conventions - Match the project's style if Makefile already exists
- •Don't over-engineer - Solve the immediate need, not hypothetical futures
- •Use
uv run- Always run Python commands viauv runfor venv context - •Explain decisions - If choosing flat/minimal, explain why before generating
When to Use This Skill
- •Creating a new Makefile for a project
- •Adding specific targets to an existing Makefile
- •Improving/refactoring an existing Makefile
- •Setting up CI/CD make targets
Quick Start
For new projects, use the appropriate template:
| Project Type | Template | Complexity |
|---|---|---|
| Any project | templates/base.mk | Minimal |
| Python with uv | templates/python-uv.mk | Standard |
| Python FastAPI | templates/python-fastapi.mk | Full-featured |
| Node.js | templates/nodejs.mk | Standard |
| Go | templates/go.mk | Standard |
| Chrome Extension | templates/chrome-extension.mk | Modular |
Chrome Extension Structure
The chrome extension template uses a modular structure:
Makefile # Main file with help + includes makefiles/ colors.mk # ANSI colors & print helpers common.mk # Shell flags, VERBOSE mode, guards build.mk # Build zip, version bump, releases dev.mk # Test, lint, clean, install
Copy from templates/chrome-extension-modules/ to your project's makefiles/ directory.
Key features:
- •
build-release- Version bump menu (major/minor/patch) + zip for Chrome Web Store - •
build-beta- (Optional) GitHub releases withghCLI - •
dev-test/dev-test-e2e- Vitest + Playwright testing - •
VERBOSE=1 make <target>- Show commands for debugging
Interaction Pattern
- •Understand - What specific problem are we solving?
- •Check existing - Is there already a Makefile? Read it first!
- •Default to modular - For 5+ targets, use modular structure unless user requests flat
- •Match preferences - Use python-fastapi.mk template style as default for rich help
- •Explain structure - If you choose flat/minimal, explain the reasoning
- •Iterate - Add complexity or simplify based on feedback
Naming Conventions
Use kebab-case with consistent prefix-based grouping:
# Good - consistent prefixes (hyphens, not underscores) build-release, build-zip, build-clean # Build tasks dev-run, dev-test, dev-lint # Development tasks db-start, db-stop, db-migrate # Database tasks env-local, env-prod, env-show # Environment tasks # Internal targets - prefix with underscore to hide from help _build-zip-internal, _prompt-version # Not shown in make help # Bad - inconsistent run-dev, build, localEnv, test_net build_release, dev_test # Underscores - don't use
Name targets after the action, not the tool:
# Good - describes what it does remove-bg # Removes background from image format-code # Formats code lint-check # Runs linting # Bad - names the tool rembg # What does this do? prettier # Is this running prettier or configuring it? eslint # Unclear
Key Patterns
Always Use uv run for Python
# Good - uses uv run with ruff (modern tooling) dev-check: uv run ruff check src/ tests/ uv run ruff format --check src/ tests/ uv run mypy src/ dev-format: uv run ruff check --fix src/ tests/ uv run ruff format src/ tests/ # Bad - relies on manual venv activation dev-format: ruff format .
Use uv sync (not pip install)
env-install: uv sync # Uses pyproject.toml + lock file
Categorized Help (for 5+ targets)
help: @printf "$(BOLD)=== 🚀 API ===$(RESET)\n" @printf "$(CYAN)%-25s$(RESET) %s\n" "api-run" "Start server" @printf "%-25s $(GREEN)make api-run [--reload]$(RESET)\n" ""
Makefile ordering rule - help targets go LAST, just before catch-all:
- •Configuration (
?=variables) - •
HELP_PATTERNSdefinition - •Imports (
include ./makefiles/*.mk) - •Main targets (grouped by function)
- •
help:andhelp-unclassified:targets - •Catch-all
%:rule (absolute last)
Preflight Checks
_check-docker:
@docker info >/dev/null 2>&1 || { echo "Docker not running"; exit 1; }
db-start: _check-docker # Runs check first
docker compose up -d
External Tool Dependencies
When a target requires an external tool (not a system service):
- •Don't create public install targets (no
make install-foo) - •Use internal check as dependency (prefix with
_, no##comment) - •Show install command on failure - tell user what to run, don't do it for them
# Internal check - hidden from help (no ##)
_check-rembg:
@command -v rembg >/dev/null 2>&1 || { \
printf "$(RED)$(CROSS) rembg not installed$(RESET)\n"; \
printf "$(YELLOW)Run: uv tool install \"rembg[cli]\"$(RESET)\n"; \
exit 1; \
}
# Public target - uses check as dependency
.PHONY: remove-bg
remove-bg: _check-rembg ## Remove background from image
rembg i "$(IN)" "$(OUT)"
Key points:
- •Name target after the action (
remove-bg), not the tool (rembg) - •Check runs automatically - user just runs
make remove-bg - •If tool missing, user sees exactly what command to run
Env File Loading
Load .env and export to child processes:
# At top of Makefile, after .DEFAULT_GOAL -include .env .EXPORT_ALL_VARIABLES:
For per-target env override:
# Allow: E2E_ENV=.test.env make test-e2e
test-e2e:
@set -a && . "$${E2E_ENV:-.env}" && set +a && uv run pytest tests/e2e/
FIX Variable for Check/Format Targets
Use a FIX variable to toggle between check-only and auto-fix modes:
FIX ?= false dev-check: ## Run linting and type checks (FIX=false: check only) $(call print_section,Running checks) ifeq ($(FIX),true) uv run ruff check --fix src/ tests/ uv run ruff format src/ tests/ else uv run ruff check src/ tests/ uv run ruff format --check src/ tests/ endif uv run mypy src/ $(call print_success,All checks passed)
In help output, show usage:
@printf "$(CYAN)%-25s$(RESET) %s\n" "dev-check" "Run linting (FIX=false: check only)" @printf "%-25s $(GREEN)make dev-check FIX=true$(RESET) <- auto-fix issues\n" ""
When to Modularize
Default to modular for any new Makefile with 5+ targets.
Use flat file only when:
- •Simple scripts or single-purpose tools
- •User explicitly requests it
- •< 5 targets with no expected growth
Standard modular structure:
Makefile # Config, imports, help, catch-all makefiles/ colors.mk # ANSI colors & print helpers common.mk # Shell flags, VERBOSE, guards <domain>.mk # Actual targets (build.mk, dev.mk, etc.)
Legacy Compatibility
Default: NO legacy aliases. Only add when:
- •User explicitly requests backwards compatibility
- •Existing CI/scripts depend on old names (verify with
rg "make old-name")
When legacy IS needed, put them in a clearly marked section AFTER main targets but BEFORE help:
############################ ### Legacy Target Aliases ## ############################ .PHONY: old-name old-name: new_name ## (Legacy) Description
Key Rules
- •Always read existing Makefile before changes
- •Search codebase before renaming targets (
rg "make old-target") - •Test with
make helpandmake -n target - •Update docs after Makefile changes - When adding new targets:
- •Add to
make helpoutput (in the appropriate section) - •Update
CLAUDE.mdif the project has one (document new targets) - •Update any other relevant docs (README.md, Agents.md, etc.)
- •Add to
- •Never add targets without clear purpose
- •No line-specific references - Avoid patterns like "Makefile:44" in docs/comments; use target names instead
- •Single source of truth - Config vars defined once in root Makefile, not duplicated in modules
- •Help coverage audit - All targets with
##must appear in eithermake helpormake help-unclassified
Help System
ASCII box title for visibility:
help: @printf "\n" @printf "$(BOLD)$(CYAN)╔═══════════════════════════╗$(RESET)\n" @printf "$(BOLD)$(CYAN)║ Project Name ║$(RESET)\n" @printf "$(BOLD)$(CYAN)╚═══════════════════════════╝$(RESET)\n\n"
Categorized help with sections:
@printf "$(BOLD)=== 🏗️ Build ===$(RESET)\n" @grep -h -E '^build-[a-zA-Z_-]+:.*?## .*$$' ... | awk ... @printf "$(BOLD)=== 🔧 Development ===$(RESET)\n" @grep -h -E '^dev-[a-zA-Z_-]+:.*?## .*$$' ... | awk ...
Key help patterns:
- •
help- Main categorized help - •
help-unclassified- Show targets not in any category (useful for auditing) - •
help-all- Show everything including internal targets - •Hidden targets: prefix with
_(e.g.,_build-internal) - •Legacy targets: label with
## (Legacy)and filter from main help
Always include a Help section in make help output:
@printf "$(BOLD)=== ❓ Help ===$(RESET)\n" @printf "$(CYAN)%-25s$(RESET) %s\n" "help" "Show this help" @printf "$(CYAN)%-25s$(RESET) %s\n" "help-unclassified" "Show targets not in categorized help" @printf "\n"
help-unclassified pattern (note the sed to strip filename prefix):
help-unclassified: ## Show targets not in categorized help
@printf "$(BOLD)Targets not in main help:$(RESET)\n"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
sed 's/^[^:]*://' | \
grep -v -E '^(env-|dev-|clean|help)' | \
awk 'BEGIN {FS = ":.*?## "}; {printf "$(CYAN)%-25s$(RESET) %s\n", $$1, $$2}' || \
printf " (none)\n"
Description format - one line with example:
# Good - concise description + example on next line @printf "$(CYAN)%-14s$(RESET) %s\n" "scrape" "Fetch posts into SQLite, detect problems" @printf " $(GREEN)make scrape SUBREDDITS=python,django LIMIT=10$(RESET)\n" @printf "$(CYAN)%-14s$(RESET) %s\n" "dev-check" "Run ruff linter and formatter" @printf " $(GREEN)make dev-check FIX=true$(RESET)\n" # Bad - too verbose, multi-line explanation @printf " $(CYAN)$(BOLD)setup$(RESET)\n" @printf " Install Python dependencies using uv. Run this once after cloning.\n" @printf " Creates .venv/ and installs packages from pyproject.toml.\n" @printf " $(GREEN)make setup$(RESET)\n"
Help description rules:
- •One line max - Description must fit on single line unless user explicitly asks for more
- •Include what it affects - e.g., "creates .venv", "exports to CSV", "deletes database"
- •Example on next line - Show realistic usage with parameters in
$(GREEN) - •Skip examples for simple targets - If no parameters, no example needed
Catch-all redirects to help:
%: @printf "$(RED)Unknown target '$@'$(RESET)\n" @$(MAKE) help
Common Pitfalls
| Issue | Fix |
|---|---|
$var in shell loops | Use $$var to escape for make |
Catch-all %: shows error | Redirect to @$(MAKE) help instead |
| Config vars scattered | Put all ?= overridable defaults at TOP of root Makefile |
HELP_PATTERNS mismatch | Must match grep patterns in help target exactly |
| Duplicate defs in modules | Define once in root, reference in modules |
| Trailing whitespace in vars | Causes path splitting bugs - trim all variable definitions |
.PHONY on file targets | Only use .PHONY for non-file targets |
| Too many public targets | Don't expose install-X or check-X - use internal _check-X dependencies |
$(DIM) for usage text | Appears grey/unreadable - use $(GREEN) instead |
| Target named after tool | Name after the action: remove-bg not rembg |
help-unclassified shows filename | Use sed 's/^[^:]*://' to strip Makefile: prefix |
No .env export | Add -include .env and .EXPORT_ALL_VARIABLES: at top |
Cleanup Makefile Workflow
When user says "cleanup my makefiles":
IMPORTANT: Build a plan first and explain it to the user before implementing anything.
Phase 1: Audit (no changes yet)
make help # See categorized targets make help-unclassified # Find orphaned targets cat Makefile # Read structure ls makefiles/*.mk 2>/dev/null # Check if modular rg "make " --type md # Find external dependencies grep -E '\s+$' Makefile makefiles/*.mk # Trailing whitespace
Phase 2: Build & Present Plan
Create a checklist of proposed changes:
- • Structure - Convert flat → modular (if 5+ targets) or vice versa
- • Legacy removal - List specific targets to delete (with dependency check)
- • Duplicates - List targets to consolidate
- • Renames - List
old_name→new-namechanges - • Description rewrites - List vague descriptions to improve
- • Missing targets - Suggest targets that should exist (e.g.,
help-unclassified) - • Ordering fixes - Config → imports → targets → help → catch-all
Ask user to approve the plan before proceeding.
Phase 3: Implement (after approval)
- •Restructure (if needed) - Create
makefiles/directory, split into modules - •Remove legacy - Delete approved targets
- •Consolidate duplicates - Merge into single targets
- •Rename targets - Apply hyphen convention, add
_prefix for internal - •Rewrite descriptions - Make each
##explain the purpose - •Fix formatting
- •Usage examples in yellow:
$(YELLOW)make foo$(RESET) - •Remove trailing whitespace
- •
.PHONYonly on non-file targets
- •Usage examples in yellow:
- •Add missing pieces -
help-unclassified, catch-all%:, etc.
Phase 4: Verify
make help # Clean output? make help-unclassified # Should be empty or minimal make -n <target> # Dry-run key targets
What NOT to do without asking:
- •Rename targets that CI/scripts depend on
- •Remove targets that look unused
- •Change structure (flat ↔ modular) without approval
Files in This Skill
- •
reference.md- Detailed patterns, categorized help, error handling - •
templates/- Full copy-paste Makefiles for each stack - •
modules/- Reusable pieces for complex projects
Example: Adding a Target
User: "Add a target to run my tests"
.PHONY: test test: ## Run tests $(call print_section,Running tests) uv run pytest tests/ -v $(call print_success,Tests passed)
User: "Add database targets"
.PHONY: db-start db-stop db-migrate db-start: _check-docker ## Start database docker compose up -d postgres db-stop: ## Stop database docker compose down db-migrate: _check-postgres ## Run migrations uv run alembic upgrade head