name: makefile description: Create or improve Makefiles with minimal complexity. Templates available: base, python-uv, python-fastapi, nodejs, go, chrome-extension. allowed-tools: Read, Write, Edit, Grep, Glob, Bash
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 Storebuild-beta- (Optional) GitHub releases withghCLIdev-test/dev-test-e2e- Vitest + Playwright testingVERBOSE=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
- 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 helphelp-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"
Usage examples on second line in green:
# Target description on first line
@printf "$(CYAN)%-40s$(RESET) %s\n" "remove-bg" "Remove background from image"
# Usage example on second line - use GREEN (not DIM, which appears grey)
@printf "%-40s $(GREEN)make remove-bg IN=logo.png [OUT=logo_nobg.png]$(RESET)\n" ""
Usage line format:
- Show on a separate second line (not inline with description)
- Use
$(GREEN)for visibility (avoid$(DIM)- appears grey/unreadable) - Include full command with realistic example values
- Show optional params in brackets with sensible defaults
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 handlingtemplates/- Full copy-paste Makefiles for each stackmodules/- 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