Back to Mise

Hooks

docs/hooks.md

2026.9.210.6 KB
Original Source

Hooks

mise can automatically execute scripts during a mise activate session. Except for the preinstall and postinstall hooks, these require the mise activate shell hook to be installed in your shell. Hooks are configured in mise.toml.

EventRuns whenRequires shell activation
cdThe working directory changesYes
enter / leaveThe shell enters or leaves a project's directory treeYes
preinstall / postinstallmise installs the selected toolsNo
watch_filesActivation detects a change to a matching fileYes

Use tasks for commands you want to invoke explicitly. Use mise watch for a running file watcher; watch_files hooks are checked by shell activation, rather than by a background watcher.

When the same hook type is defined in multiple loaded config files, mise runs every matching hook rather than overriding hooks from lower-precedence files. Hooks run from the highest-precedence config file to the lowest-precedence config file. Within a single config file, hooks defined as an array run in the order listed. For example, hooks in conf.d/a.toml, conf.d/b.toml, and conf.d/c.toml run as c, b, then a because later alphabetical fragments have higher precedence. Put order-dependent hooks in one array when they need to run in alphabetical order.

CD hook

This hook runs whenever the directory changes.

toml
[hooks]
cd = "echo 'I changed directories'"

Enter hook

This hook runs when the project is entered. Changing directories within the project does not trigger it again.

toml
[hooks]
enter = "echo 'I entered the project'"

Leave hook

This hook runs when the project is left. Changing directories within the project does not trigger it.

toml
[hooks]
leave = "echo 'I left the project'"

Preinstall/postinstall hook

These hooks run before and after tools are installed, respectively. Unlike other hooks, they do not require mise activate. They run with the project root as their working directory, even when mise install is invoked from a subdirectory. The invocation directory remains available in MISE_ORIGINAL_CWD.

toml
[hooks]
preinstall = "echo 'I am about to install tools'"
postinstall = "echo 'I just installed tools'"

String hooks are shorthand for run hooks. Use a hook table when you need to select the inline shell command:

toml
[hooks]
postinstall = { run = "echo 'installed'", shell = "bash -c" }

Like tasks, inline hook tables may define a Windows-specific command with run_windows. On Windows, mise uses run_windows when it is set; otherwise it uses run. On other platforms, a hook with only run_windows is skipped.

toml
[hooks]
postinstall = { run = "pwd", run_windows = "cd" }

For preinstall and postinstall, script = ... and scripts = ... are legacy aliases for run = .... If a shell is also set on a script/scripts hook, mise warns that the shell is ignored and still runs the script with the default inline shell. Use run = ... with shell = "bash -c" to choose the inline shell command. The script and scripts aliases for install hooks are deprecated.

A mise install that finds nothing to install (all configured tools are already present) still runs the postinstall hook — it is not skipped on a no-op install.

The postinstall hook receives a MISE_INSTALLED_TOOLS environment variable containing a JSON array of the tools that were just installed, or [] when nothing was installed (e.g. a no-op install). Hooks that should only act on real installs can guard on MISE_INSTALLED_TOOLS != "[]":

toml
[hooks]
postinstall = '''
echo "Installed: $MISE_INSTALLED_TOOLS"
# Example output: [{"name":"node","version":"20.10.0"},{"name":"python","version":"3.12.0"}]
'''

Tool-level postinstall

Use a tool's postinstall option for work specific to that installation. It runs after that tool installs; independent tool installations can still run in parallel. Use the project-level postinstall hook for work that needs the whole selected toolset:

toml
[tools]
node = { version = "24", postinstall = "node --version" }
python = { version = "3.12", postinstall = "python --version" }

Tool-level postinstall scripts receive the following environment variables:

  • MISE_TOOL_NAME: The short name of the tool (e.g., "node", "python")
  • MISE_TOOL_VERSION: The version that was installed (e.g., "20.10.0", "3.12.0")
  • MISE_TOOL_INSTALL_PATH: The path where the tool was installed
  • Variables from that tool's install_env option
  • MISE_CONFIG_FILE: The exact config file that declared the tool
  • MISE_CONFIG_ROOT: The root directory of that config
  • MISE_PROJECT_ROOT: The active project root (or the config root when no project is active)

Task hooks

Instead of inline scripts, hooks can reference mise tasks. The task is executed as a subprocess via mise run, so it reuses the full task system including dependencies, environment variables, and file-based task definitions.

toml
[tasks.install-deps]
run = "echo 'install project dependencies here'"

[tasks.setup]
run = "echo 'setting up project'"
depends = ["install-deps"]

[hooks]
enter = { task = "setup" }

You can mix task references with inline scripts in arrays:

toml
[hooks]
enter = ["echo 'entering project'", { task = "setup" }]

Task hooks work with all hook types (enter, leave, cd, preinstall, postinstall).

Task references used as preinstall hooks do not automatically install missing project- or task-level tools. This keeps the hook ahead of the installation it is preparing. Commands needed by a preinstall task must already be available from the system or an existing installation. Other task-backed hook types retain normal task tool installation behavior.

Watch files hook

While using mise activate, mise can watch files for changes and execute a script or task when one changes.

toml
[[watch_files]]
patterns = ["src/**/*.rs"]
run = "cargo fmt"

By default, run uses the configured inline shell: unix_default_inline_shell_args or windows_default_inline_shell_args. Add shell = "bash -c" to choose a different inline shell command for a watch file hook:

toml
[[watch_files]]
patterns = ["*.js"]
run = "eslint --fix ."
shell = "bash -c"

shell only applies to run hooks. You can also reference a mise task instead of an inline script:

toml
[[watch_files]]
patterns = ["uv.lock"]
task = "sync-deps"

Each [[watch_files]] entry should have either run or task, but not both.

This hook has the following environment variables set:

  • MISE_WATCH_FILES_MODIFIED: A colon-separated list of the files that have been modified. Colons are escaped with a backslash.

Hook execution

Hooks are executed with the following environment variables set:

  • MISE_ORIGINAL_CWD: The directory that the user is in.
  • MISE_PROJECT_ROOT: The root directory of the project.
  • MISE_CONFIG_ROOT: The root directory of the config that defines the hook.
  • MISE_PREVIOUS_DIR: The directory that the user was in before the directory change (only if a directory change occurred).
  • MISE_INSTALLED_TOOLS: A JSON array of tools that were installed (only for postinstall hooks).

Global hooks use the active project's root for MISE_PROJECT_ROOT and the global config root for MISE_CONFIG_ROOT. For global-only operations such as mise use --global, both variables use the global config root and project hooks do not run.

Inline run hooks can be written as { run = "..." } for any hook type. The string shorthand (enter = "echo hi") is equivalent to { run = "echo hi" }.

run and run_windows must be strings. run = ["echo one", "echo two"] is not supported.

To run separate spawned inline commands, define multiple hooks. Each hook entry is a separate execution, so mise starts one subprocess per run entry:

toml
[hooks]
enter = [
  { run = "echo one" },
  { run = "echo two" },
]

To run multiple shell lines in one spawned command, use one multiline run string. This is one hook execution and one subprocess:

toml
[hooks.enter]
run = """
echo one
echo two
"""

run hooks execute in a subprocess using the default inline shell: unix_default_inline_shell_args or windows_default_inline_shell_args. Add shell = "bash -c" to a run hook table to choose a different inline shell command. Like task shell, the value should include both the program and the argument that evaluates the inline command such as bash -c, zsh -c, or pwsh -Command.

Shell hooks

enter, leave, and cd hooks can be executed in the current shell, for example if you'd like to add bash completions when entering a directory:

toml
[hooks.enter]
shell = "bash"
script = "source completions.sh"

Current-shell hooks may use script/scripts arrays:

toml
[hooks.enter]
shell = "bash"
script = [
  "source completions.sh",
  "export PROJECT_READY=1",
]

[hooks.leave]
shell = "bash"
scripts = [
  "unset PROJECT_READY",
]

script with shell is for current-shell hooks. Here, shell is a shell-name selector such as bash, zsh, or fish, not an inline shell command like bash -c. mise only prints the script when the active mise activate shell matches.

Use run when the hook should execute as an inline command in a subprocess. preinstall and postinstall do not have a current shell, so script/scripts are only kept there as legacy aliases for run; if shell is set with script/scripts on those hooks, it is ignored.

::: warning mise does not track or undo changes made by shell scripts. For example, an enter script that exports a variable needs a corresponding leave script to unset it. Prefer [env] when you want mise to manage the value's lifetime.

:::

Multiple hooks syntax

You can use arrays to define multiple hooks in the same file:

toml
[hooks]
enter = [
  "echo 'I entered the project'",
  { run = "echo 'I am in the project'" }
]

[[hooks.cd]]
run = "echo 'I changed directories'"
[[hooks.cd]]
run = "echo 'I also changed directories'"