Everyone has a Makefile they’re not entirely proud of. The whitespace sensitivity (tabs only, or it silently breaks). The platform-specific shell commands that work fine on Linux but blow up on macOS. The .PHONY declarations you always forget. The way make was designed for building C programs in 1976 and has been heroically repurposed as a general task runner ever since.

Task — configured via Taskfile.yml — is a cleaner alternative. It’s a Go binary with no runtime dependencies, uses readable YAML, runs on Windows, macOS, and Linux without modification, and handles dependency tracking and parallel execution without the Makefile arcana.

Installation

# macOS (Homebrew)
brew install go-task

# Linux (shell script)
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin

# npm (works anywhere Node is installed)
npm install -g @go-task/cli

# Windows (Scoop)
scoop install task

Or just drop the binary from the GitHub releases page into your PATH.

A Basic Taskfile

# Taskfile.yml
version: "3"

vars:
  APP_NAME: myapp
  BUILD_DIR: ./dist

tasks:
  default:
    desc: Show available tasks
    cmds:
      - task --list

  install:
    desc: Install dependencies
    cmds:
      - npm ci

  build:
    desc: Build the application
    deps: [install]
    cmds:
      - mkdir -p {{.BUILD_DIR}}
      - npm run build
    sources:
      - src/**/*.ts
    generates:
      - "{{.BUILD_DIR}}/**"

  test:
    desc: Run tests
    deps: [install]
    cmds:
      - npm test

  lint:
    desc: Run linter and formatter
    cmds:
      - npx eslint src/
      - npx prettier --check src/

  dev:
    desc: Start development server
    cmds:
      - npm run dev

  clean:
    desc: Remove build artifacts
    cmds:
      - rm -rf {{.BUILD_DIR}} node_modules

Run task --list to see all tasks with their descriptions. Run task build to execute the build task (which will run install first if its sources have changed).

The Features That Actually Matter

Cross-platform shell commands. Task’s cmds entries run through a cross-platform shell interpreter (sh on Unix, a compatible implementation on Windows), so you can write mkdir -p or rm -rf without worrying about Windows compatibility. If you need platform-specific commands, you can use cmd.windows, cmd.linux, and cmd.darwin conditionals.

Source/generates fingerprinting. The sources and generates fields let Task skip tasks when nothing has changed — similar to Make’s dependency tracking, but using file fingerprints rather than timestamps, which is more reliable:

tasks:
  compile:
    sources:
      - src/**/*.go
    generates:
      - bin/app
    cmds:
      - go build -o bin/app ./...

Run task compile twice in a row without changing any .go files and the second run prints “Task up to date” and exits immediately.

Parallel task execution. Run multiple tasks simultaneously:

tasks:
  check:
    desc: Run all checks in parallel
    deps:
      - task: lint
      - task: test
      - task: type-check

The three tasks start simultaneously and check completes when all of them finish (or any one fails).

Environment and .env file support. Task loads .env files automatically:

version: "3"

dotenv: [".env", ".env.local"]

tasks:
  deploy:
    cmds:
      - echo "Deploying to {{.ENVIRONMENT}}"
      - ./deploy.sh

Task dependencies with output passing. Tasks can be called with variables and their outputs can be captured — useful for dynamic builds:

tasks:
  get-version:
    silent: true
    cmds:
      - git describe --tags --always

  build-tagged:
    vars:
      VERSION:
        sh: task get-version
    cmds:
      - go build -ldflags "-X main.version={{.VERSION}}" -o bin/app ./...

Replacing Common Makefile Patterns

Most Makefile projects have a handful of patterns that translate directly to Taskfile:

The “help” target (listing available commands) is task --list — no custom implementation needed if you write desc: fields.

The .PHONY declaration doesn’t exist in Taskfile — all tasks are phony (non-file targets) unless you use sources/generates for fingerprinting.

Includes and shared logic. Task supports including other Taskfiles via the includes key, which is cleaner than Makefile’s include directive:

version: "3"

includes:
  docker:
    taskfile: ./tasks/docker.yml
    dir: .
  deploy:
    taskfile: ./tasks/deploy.yml
    dir: .

Then task docker:build and task deploy:prod work as namespaced tasks.

Should You Migrate Existing Makefiles?

For greenfield projects, Taskfile is an easy choice — start with it and you’ll never miss Makefiles. For existing Makefiles, the migration calculus depends on the Makefile’s complexity.

Simple Makefiles with a handful of shell commands translate in an hour. Complex Makefiles that rely on Make-specific features (pattern rules, automatic variables like $@ and $<, recursive make) are more work and may not be worth migrating unless the Makefile is causing active pain.

The most common pain point Taskfile solves: CI pipelines that run on Windows agents where Makefile syntax breaks, and onboarding new developers on Windows who can’t use Make without WSL. If either of those is a problem you’re currently working around, the migration pays for itself quickly.

The binary is available at taskfile.dev, the GitHub repository has 12,000+ stars, and it’s increasingly showing up as the default task runner in new open-source Go and TypeScript projects.