← All writing

Top 10 Features in Mise You're Not Using

6-minute read
On this page

1. Task sources and outputs #

Any task can declare input/output globs. If all outputs are newer than all sources, the task is skipped entirely:

[tasks.build]
run = "cargo build"
sources = ["Cargo.toml", "src/**/*.rs"]
outputs = ["target/debug/myapp"]

Don’t want to enumerate outputs? Set outputs = { auto = true } (which is actually the default when sources is defined) — mise tracks a hash internally and skips the task if sources haven’t changed.

2. The github, http, and s3 backends #

The github backend installs tools directly from GitHub releases. It infers the right asset for your platform automatically — just point it at a repo and it usually works:

[tools]
"github:BurntSushi/ripgrep" = "latest"

For more complex setups (non-standard naming, multiple binaries, etc.), the aqua registry is a better choice for open-source tools — it has hand-written manifests for hundreds of tools with full platform mapping. The github backend is best for quick one-offs and private/internal tools.

The http backend goes further — install any binary from any URL with templated platform variables:

[tools."http:my-internal-tool"]
version = "2.1.0"
url = 'https://artifacts.internal.co/tool-v{{version}}-{{os(macos="darwin")}}-{{arch(x64="amd64")}}.tar.gz'

The s3 backend [experimental] does the same for S3 or S3-compatible storage (MinIO, etc.):

[tools."s3:my-tool"]
version = "1.0.0"
url = "s3://my-bucket/tools/my-tool-v{{ version }}.tar.gz"
endpoint = "http://minio.internal:9000"

Great for internal artifact servers, private buckets, or anything with a download link.

3. Tool stubs #

Inspired by Meta’s dotslash, tool stubs are self-contained scripts that auto-download and execute tools on first run. Check them into your repo and anyone can run them — they just need mise:

#!/usr/bin/env -S mise tool-stub
version = "20.0.0"
tool = "node"
bin = "node"

Generate them with mise generate tool-stub. Add --lock to embed platform-specific URLs and checksums for fully reproducible installs. Add --bootstrap to create a wrapper that installs mise itself if missing. You can also use the http backend style for custom binaries — no registry entry needed.

4. Typed task arguments with usage #

Tasks can have real CLI arguments with validation, choices, defaults, and auto-generated help:

[tasks.deploy]
usage = '''
arg "<environment>" help="Target env" { choices "dev" "staging" "prod" }
flag "-v --verbose" help="Verbose output"
flag "--region <region>" default="us-east-1"
'''
run = 'deploy.sh ${usage_environment} --region ${usage_region}'

mise run deploy --help prints a proper help page. Tab completion works out of the box — including completing argument values from choices or dynamic complete handlers. Adding autocompletion for custom values is as simple as:

complete "environment" run="mise plugins ls"

5. Lockfiles — avoid GITHUB_TOKEN issues with mise lock #

Pin exact versions, checksums, and download URLs for every tool:

mise lock

That’s it. Run it and it generates mise.lock with checksums and URLs for all common platforms. This is a big deal for teams — without a lockfile, every mise install hits the GitHub API to resolve versions and download URLs. GitHub’s API rate limits are aggressive for unauthenticated requests, and getting every developer on a team to configure a GITHUB_TOKEN is an unnecessary hassle. With a lockfile, installs are fully offline-capable: no API calls, no rate limits, no tokens needed.

Use --locked in CI to fail if the lockfile is missing or outdated.

6. Supply chain security #

Most tools in the mise registry now install through the aqua backend, which natively verifies integrity — no external tools required:

  • Checksums (SHA256, SHA512, Blake3) — always on
  • Cosign signature verification
  • SLSA provenance verification
  • GitHub Attestations — workflow-level verification
  • Minisign — public key signatures

Only 39 out of 919 tools in the registry default to a plugin-based backend (23 asdf, 16 vfox) — around 4%. The rest install through aqua, github, or other backends with built-in verification. Those remaining plugin-backed tools have all been forked to the mise-plugins org under mise maintainer control. When you run mise use ripgrep, you’re getting a verified binary from the vendor’s GitHub release — not running arbitrary bash from a third-party plugin.

7. Environment profiles with MISE_ENV #

Set MISE_ENV=staging and mise layers mise.staging.toml on top of mise.toml:

mise.toml              # shared defaults
mise.dev.toml          # dev overrides
mise.staging.toml      # staging overrides
mise.local.toml        # personal overrides, never committed

The catch: MISE_ENV determines which config files get loaded, so it can’t be set inside mise.toml itself. That’s what .miserc.toml is for — a lightweight config loaded before everything else:

# .miserc.toml
env = ["development"]

This way your project defaults to the development profile without requiring every developer to set an environment variable.

8. Shell aliases #

Project-scoped aliases that activate/deactivate as you cd:

[shell_alias]
ll = "ls -la"
dev = "npm run dev"
dc = "docker compose -f docker-compose.dev.yml"
src = "cd {{config_root}}/src"

Works in bash, zsh, and fish. Leave the directory and they’re gone.

9. mise prepare [experimental] #

Detects lockfiles and installs project dependencies only when they’re stale. If the content of package-lock.json has changed since the last run, it triggers npm install. Built-in providers for npm, yarn, pnpm, bun, go, pip, poetry, uv, bundler, and composer. Custom providers for anything else:

[prepare.codegen]
sources = ["schema/*.graphql"]
outputs = ["src/generated/"]
run = "npm run codegen"
depends = ["npm"]

Set auto = true and it runs automatically before mise run and mise exec. Pass --dry-run to see why each provider is stale (e.g., “package.json changed”, “outputs missing”).

Watch this space — this is an area I plan to improve further to give users a cargo/uv-like experience across all languages.

10. Monorepo tasks [experimental] #

Run tasks across packages with wildcard patterns:

mise //services/api:build          # specific package
mise //...:test                    # all packages
mise '//services/...:build'        # all under services/

Configuration inherits from parent directories — define node = "20" and common env vars in the root mise.toml, and every package gets them automatically. Individual packages can override just what they need (e.g., a legacy app pinning node = "14") while inheriting everything else. Enable with experimental_monorepo_root = true in your root mise.toml.


Bonus: The mise ecosystem #

fnox #

Secret management for development and production. Encrypt secrets in git with age or cloud KMS, or fetch them from AWS Secrets Manager, 1Password, Vault, and more. Shell integration auto-loads secrets on cd:

# fnox.toml
[providers]
age = { type = "age", recipients = ["age1ql3z..."] }

[secrets]
DATABASE_URL = { provider = "age", value = "YWdlLWVuY3..." }  # encrypted, safe to commit
API_KEY = { default = "dev-key-12345" }                         # plain default for local dev

[profiles.production.providers]
aws = { type = "aws-sm", region = "us-east-1", prefix = "myapp/" }

hk #

A git hook manager built for speed. The key innovation is read/write file locking: check commands (e.g., eslint, prettier --check) take read locks and run fully in parallel, while fix commands (e.g., prettier --write) take write locks to prevent race conditions. It runs checks first by default, only falling back to fixes when needed. No other git hook manager does this — tools like husky, lefthook, and pre-commit either run steps serially or do naive parallelism that can corrupt files when multiple fixers touch the same file:

amends "package://github.com/jdx/hk/releases/download/v1.36.0/hk@1.36.0#/Config.pkl"
import "package://github.com/jdx/hk/releases/download/v1.36.0/hk@1.36.0#/Builtins.pkl"

hooks {
    ["fix"] {
        fix = true
        steps {
            ["prettier"] = (Builtins.prettier) {}
            ["eslint"] = (Builtins.eslint) {}
            ["tsc"] { check = "tsc --noEmit" }
        }
    }
    ["check"] {
        steps {
            ["prettier"] = (Builtins.prettier) {}
            ["eslint"] = (Builtins.eslint) {}
            ["tsc"] { check = "tsc --noEmit" }
        }
    }
}

pitchfork #

Daemon manager for dev services. Auto-starts processes when you cd into a project, auto-stops when you leave. Ready checks, restart on failure, dependency ordering, and cron jobs:

# pitchfork.toml
[daemons.postgres]
run = "docker run --rm -p 5432:5432 -e POSTGRES_PASSWORD=dev postgres:16"
auto = ["start", "stop"]
ready_delay = 5

[daemons.api]
run = "npm run dev:api"
auto = ["start", "stop"]
ready_output = "listening on"
depends = ["postgres"]