Development

Mise

Use mise for local tools, environment variables, secrets, and project tasks.

By Guillermo Alvarez - Published - Updated

What mise solves

GoLazy apps use mise as the local development coordinator. It gives the app one place to declare helper dependencies, development environment variables, secret loading, and project tasks without adding framework runtime code for those concerns.

That means a new checkout can run:

mise trust
mise install
mise tasks ls

lazy new already runs the trust and install steps for generated apps.

Dependencies

Declare local command-line dependencies in mise.toml:

[tools]
node = "24"
"aqua:FiloSottile/age" = "1.3.1"
"aqua:getsops/sops" = "3.13.1"
"aqua:jdx/usage" = "3.5.3"

Do not add Go as a generated app dependency. Go already reads the module's go directive and can select toolchains per module. Also, go install writes binaries into a user-level bin directory; it is not a project-scoped system installer that mise needs to wrap for normal GoLazy development.

Add stateful tools only when the app needs them. For example, an app that runs PostgreSQL and MinIO locally can declare them next to Node:

[tools]
node = "24"
postgres = "17"
minio = "latest" # choose a concrete release before committing

Use mise registry postgres, mise registry minio, and mise ls-remote <tool> to choose a backend and pin a version. The tool declaration only installs binaries. Starting, checking, creating, migrating, dumping, and loading local services belongs in task files. Read Services for those lifecycle tasks.

Configuration files

Committed project defaults live in mise.toml. Personal machine overrides belong in ignored local config such as mise.local.toml when the app chooses to load it.

Use these commands to see what mise is reading and exposing:

mise config ls
mise current
mise env
mise tasks ls --all

mise config ls lists config files in use. mise current shows active tool versions. mise env prints the environment for commands run through mise. mise tasks ls --all lists task files from the current project hierarchy.

Environment variables

Committed development values can live directly in mise.toml:

[env]
APP_ENV = "development"
DATABASE_NAME = "my_app_development"
_.file = ".secrets/development.env"

Use committed values for non-secret defaults that every checkout should share. Use mise.local.toml or an ignored env file for machine-specific ports, paths, or experimental toggles. Production should provide normal environment variables through the deployment platform.

Secrets with SOPS

Generated apps install age and sops so applications can move from checked-in development examples to encrypted local secret files without adding a GoLazy runtime dependency.

The sample app task convention is:

mise run secrets:new-key -- alice
mise run secrets:users
mise run secrets:add-key -- bob age1...
mise run secrets:remove-user -- bob

secrets:new-key creates a private age identity under .secrets/keys/, registers the public recipient in .secrets/recipients.txt, and refreshes .sops.yaml. Commit public recipient files and .sops.yaml; do not commit private keys or decrypted production secrets.

SOPS keeps encrypted values in a normal text file, so adding or changing a key is reviewable as a small diff instead of a binary blob. After recipient changes, update existing encrypted files:

sops updatekeys -y .secrets/development.sops.env

Run commands from encrypted env values with sops exec-env when your app uses an encrypted development env file:

SOPS_AGE_KEY_FILE=.secrets/keys/alice.txt \
  sops exec-env .secrets/development.sops.env 'lazy'

Tasks

Project scripts live under .mise/tasks/. Nested directories become colon-separated task names:

.mise/tasks/postgres/start      -> mise run postgres:start
.mise/tasks/postgres/check      -> mise run postgres:check
.mise/tasks/secrets/add-key     -> mise run secrets:add-key

Use standalone task files instead of stuffing scripts into mise.toml. Interactive scripts that open an editor should include:

#MISE raw=true

Small Go scripts can be task files too. Keep the Go script header as the first line so the file is directly executable:

///usr/bin/env go run "$0" "$@"; exit $?
package main

func main() {
    println("hello from a mise task")
}

Save that as .mise/tasks/hello.go, mark it executable, and run:

mise run hello