Taskfile best practices
Review criteria and anti-patterns for go-task Taskfiles — structure, variable scoping, task design, command execution, dependency ordering, output, and security. Use when auditing or hardening a Taskfile.yaml in any language ecosystem. Also loaded by the taskfile-review agent as its knowledge base.From its SKILL.md
npx -y skills add CowDogMoo/squad-agents --skill taskfile-best-practicesAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
13.6 KB, as published. Nobody here has run it
Taskfile Best Practices
A comprehensive guide to writing effective Taskfile.yaml files following community best practices and the Taskfile philosophy. This document serves as the knowledge base for the taskfile-review agent.
Table of Contents
- Taskfile Philosophy
- File Structure
- Variable Management
- Task Design
- Command Execution
- Dependencies and Ordering
- Output and Logging
- Security Considerations
- Error Handling
- Includes and Composition
- Common Anti-Patterns
- Severity Classification
Taskfile Philosophy
Core Principles
| Principle | Description |
|---|---|
| Simplicity | Tasks should be easy to understand at a glance |
| Reproducibility | Same inputs = same outputs across environments |
| Composability | Small, focused tasks that combine well |
| Documentation | Every task should be self-documenting |
| Idempotency | Running a task twice should not break anything |
Key Benefits Over Make
- YAML syntax is more readable than Makefile syntax
- Native cross-platform support (no shell dependency issues)
- Built-in variable templating with Go templates
- Better handling of task dependencies and ordering
- Schema validation support
File Structure
Standard Layout
---
# yaml-language-server: $schema=https://taskfile.dev/schema.json
version: "3"
vars:
# Global variables at the top
BINARY_NAME: myapp
CMD_PATH: ./cmd/myapp
includes:
# External taskfiles (optional)
go:
taskfile: ./build/Taskfile.go.yaml
tasks:
default:
desc: Default task (runs when no task specified)
cmds:
- task: build
build:
desc: Build the application
cmds:
- go build -o {{.BINARY_NAME}} {{.CMD_PATH}}
Required Elements
| Element | Required | Purpose |
|---|---|---|
version: "3" | Yes | Taskfile schema version |
| Schema comment | Recommended | Enables IDE validation |
vars: section | If using variables | Define all variables at top |
desc: on tasks | Yes | Documents what task does |
File Naming
- Main file:
Taskfile.yaml(preferred) orTaskfile.yml - Included files:
Taskfile.<purpose>.yaml(e.g.,Taskfile.go.yaml) - Placed in repository root or clearly documented location
Variable Management
Variable Declaration
Good - Variables at top:
vars:
BINARY_NAME: myapp
VERSION:
sh: git describe --tags --always --dirty
BUILD_FLAGS: -ldflags "-X main.version={{.VERSION}}"
Bad - Hardcoded values in commands:
tasks:
build:
cmds:
- go build -o myapp ./cmd/myapp # Hardcoded!
Variable Types
| Type | Syntax | Use Case |
|---|---|---|
| Static | VAR: value | Constants |
| Dynamic | VAR: { sh: command } | Runtime values |
| Environment | VAR: '{{.ENV_VAR | default "fallback"}}' | User overrides |
Environment Variable Handling
Good - Default with override:
vars:
PROVIDER: '{{.PROVIDER | default "local"}}'
API_KEY: '{{.API_KEY | default ""}}'
_API_KEY_FLAG: '{{if .API_KEY}}--api-key {{.API_KEY}}{{end}}'
Anti-pattern - Hardcoded secrets:
vars:
API_KEY: sk-secret-key-hardcoded # NEVER do this
Conditional Flags
Use underscore-prefixed internal variables for conditional command construction:
vars:
BASE_URL: '{{.BASE_URL | default ""}}'
_BASE_URL_FLAG: '{{if .BASE_URL}}--base-url {{.BASE_URL}}{{end}}'
tasks:
run:
cmds:
- myapp {{._BASE_URL_FLAG}} --other-flag
Task Design
Task Anatomy
tasks:
build:
desc: "Build the application binary"
summary: |
Compiles the Go application with version information embedded.
The binary is output to ./bin/{{.BINARY_NAME}}.
vars:
OUTPUT_DIR: ./bin
deps:
- generate
preconditions:
- sh: test -f go.mod
msg: "go.mod not found - run from repository root"
cmds:
- mkdir -p {{.OUTPUT_DIR}}
- go build -o {{.OUTPUT_DIR}}/{{.BINARY_NAME}} {{.CMD_PATH}}
sources:
- "**/*.go"
generates:
- "{{.OUTPUT_DIR}}/{{.BINARY_NAME}}"
Required Task Properties
| Property | When Required | Purpose |
|---|---|---|
desc: | Always | One-line description for task --list |
summary: | Complex tasks | Detailed explanation |
preconditions: | When assumptions exist | Fail fast with clear message |
cmds: | Always | The actual commands |
Task Naming Conventions
| Pattern | Use Case | Example |
|---|---|---|
verb | Simple actions | build, test, clean |
noun:verb | Namespaced actions | docker:build, go:test |
run:name | Running agents/tools | run:go-review, run:lint |
check:name | Validation tasks | check:fmt, check:lint |
Good naming:
tasks:
build: # Simple
test: # Simple
docker:build: # Namespaced
run:go-review: # Agent runner
check:fmt: # Validation
Bad naming:
tasks:
BuildApp: # PascalCase - not idiomatic
DO_THE_BUILD: # SCREAMING_CASE - not idiomatic
b: # Too abbreviated
Command Execution
Multi-line Commands
Good - Pipe for readability:
tasks:
build:
cmds:
- |
go build \
-ldflags "{{.BUILD_FLAGS}}" \
-o {{.BINARY_NAME}} \
{{.CMD_PATH}}
Good - Separate commands for clarity:
tasks:
setup:
cmds:
- mkdir -p ./bin
- go mod download
- go generate ./...
Command Chaining
| Operator | Meaning | Use Case |
|---|---|---|
&& | Continue if success | Dependent commands |
; | Always continue | Independent commands |
| | Pipe output | Data transformation |
Good:
cmds:
- go build ./... && go test ./... # Test only if build succeeds
Silent Mode
Use silent: true for tasks with intentional output:
tasks:
run:
desc: Run the application
silent: true # Only show application output
cmds:
- ./bin/myapp
Ignore Errors
Use sparingly, only when errors are truly acceptable:
cmds:
- cmd: rm -rf ./tmp
ignore_error: true # OK if directory doesn't exist
Dependencies and Ordering
Task Dependencies
tasks:
build:
deps:
- generate
- lint
cmds:
- go build ./...
generate:
cmds:
- go generate ./...
lint:
cmds:
- golangci-lint run
Dependency Properties
| Property | Behavior |
|---|---|
deps: | Run in parallel before task |
task: X in cmds | Run sequentially as part of task |
preconditions: | Check before running anything |
Sequential vs Parallel
Parallel (default for deps):
deps:
- lint # These run in parallel
- generate
Sequential:
cmds:
- task: generate # This runs first
- task: build # This waits for generate
Output and Logging
Output Control
| Setting | Effect |
|---|---|
silent: true | Suppress task name prefix |
set: [pipefail] | Fail on pipe errors |
shopt: [globstar] | Enable ** glob |
Echo Control
tasks:
verbose:
cmds:
- echo "Starting build..." # Informational
- go build ./...
- echo "Build complete."
quiet:
silent: true
cmds:
- go build ./... # No extra output
Progress Indicators
tasks:
build:
cmds:
- echo "Building {{.BINARY_NAME}}..."
- go build -o {{.BINARY_NAME}} {{.CMD_PATH}}
- echo "Build complete: {{.BINARY_NAME}}"
Security Considerations
Secret Handling
Good - Environment variable with no default:
vars:
API_KEY: '{{.API_KEY | default ""}}'
tasks:
deploy:
preconditions:
- sh: test -n "{{.API_KEY}}"
msg: "API_KEY environment variable is required"
cmds:
- deploy --api-key {{.API_KEY}}
Good - External secret tool:
vars:
API_KEY:
sh: op item get "MyService" --fields password 2>/dev/null || echo ""
Bad - Hardcoded secrets:
vars:
API_KEY: sk-abc123secretkey # CRITICAL: Never commit secrets
Input Validation
tasks:
run:
vars:
PROMPT: '{{.PROMPT | default ""}}'
preconditions:
- sh: test -n "{{.PROMPT}}"
msg: "PROMPT is required. Usage: task run PROMPT='...'"
cmds:
- echo "Running with prompt: {{.PROMPT}}"
Path Safety
Good - Use variables for paths:
vars:
OUTPUT_DIR: ./bin
tasks:
clean:
cmds:
- rm -rf {{.OUTPUT_DIR}} # Safe - controlled path
Dangerous - User-controlled paths:
tasks:
clean:
cmds:
- rm -rf {{.USER_PATH}} # DANGEROUS if USER_PATH is "../.."
Error Handling
Preconditions
tasks:
build:
preconditions:
- sh: command -v go >/dev/null
msg: "Go is not installed"
- sh: test -f go.mod
msg: "go.mod not found - run from repository root"
- sh: test -d ./cmd
msg: "cmd/ directory not found"
Status Checks
Use status: for idempotent tasks:
tasks:
generate:
cmds:
- go generate ./...
sources:
- "**/*.go"
generates:
- "**/mock_*.go"
status:
- test -f ./internal/mock_service.go
Fail Fast
Default behavior is fail-fast. Override only when necessary:
tasks:
cleanup:
cmds:
- cmd: rm -rf ./tmp
ignore_error: true
- cmd: docker rm -f test-container
ignore_error: true
Includes and Composition
External Taskfiles
includes:
go:
taskfile: ./build/Taskfile.go.yaml
vars:
BINARY_NAME: myapp
docker:
taskfile: https://example.com/Taskfile.docker.yaml
Remote Includes
includes:
go:
taskfile: "https://raw.githubusercontent.com/org/taskfiles/main/go/Taskfile.yaml"
vars:
BINARY_NAME: myapp
CMD_PATH: ./cmd/myapp
Variable Passing
includes:
go:
taskfile: ./Taskfile.go.yaml
vars:
# Pass parent vars to included taskfile
BINARY_NAME: '{{.BINARY_NAME}}'
VERSION: '{{.VERSION}}'
Common Anti-Patterns
CRITICAL
| Anti-Pattern | Issue | Fix |
|---|---|---|
| Hardcoded secrets | Security breach risk | Use environment variables |
| No version field | Schema undefined | Add version: "3" |
| Unquoted template vars | YAML parsing errors | Quote: '{{.VAR}}' |
HIGH
| Anti-Pattern | Issue | Fix |
|---|---|---|
Missing desc: | Unusable with task --list | Add description |
| Hardcoded paths | Not portable | Use variables |
| Missing preconditions | Confusing failures | Add input validation |
| Complex inline scripts | Hard to maintain | Extract to shell script |
MEDIUM
| Anti-Pattern | Issue | Fix |
|---|---|---|
| Inconsistent naming | Hard to discover tasks | Use conventions |
| Duplicate commands | Maintenance burden | Extract to shared task |
No silent: on runners | Noisy output | Add silent: true |
Missing summary: | Complex tasks undocumented | Add detailed summary |
LOW
| Anti-Pattern | Issue | Fix |
|---|---|---|
| No schema comment | No IDE validation | Add schema comment |
| Unused variables | Clutter | Remove them |
| Overly long commands | Hard to read | Use multi-line or script |
Severity Classification
CRITICAL
Issues that affect security or cause immediate failures:
- Hardcoded secrets or credentials
- Missing
version:field - Syntax errors in YAML or templates
- Unvalidated user input in dangerous commands
HIGH
Issues that affect reliability or maintainability:
- Missing
desc:on tasks - Hardcoded values that should be variables
- Missing preconditions for required inputs
- Commands that fail silently
MEDIUM
Best practice violations:
- Inconsistent task naming
- Missing
summary:on complex tasks - Duplicate command patterns
- No
silent:on runner tasks
LOW
Minor improvements:
- Missing schema comment
- Overly long single-line commands
- Unused variables
- Style inconsistencies
INFO
Suggestions for optimization:
- Using
sources:andgenerates:for caching - Parallel task execution opportunities
- Remote include consolidation
Quick Reference Checklist
Before Committing
-
version: "3"present - Schema comment at top
- All tasks have
desc: - No hardcoded secrets
- Variables for all paths/values
- Preconditions for required inputs
- Consistent task naming
- Complex commands documented
Common Commands
# List all tasks
task --list
# Run with variable override
task build VERSION=1.0.0
# Dry run
task --dry
# Force run (ignore status)
task --force build
References
Last updated: 2026-02-04