jdx/mise · error

hook command failed

Error message

hook command failed: {status}

What it means

When executing a hook command (src/hooks.rs), mise runs the command synchronously and checks its exit status. If the hook's process exits non-zero, execution bails with 'hook command failed: {status}' reporting the exit code/signal.

Solutions

  1. Read the hook's own stderr/stdout just above the error — the hook's output explains the root cause; fix the hook script.
  2. Run the hook's command manually in the project root to reproduce and debug it outside mise.
  3. If the hook intentionally blocks (preinstall gate), resolve the condition it checks (e.g. required config, allowed versions) before retrying.

Example fix

// before (.mise.toml hook)
[hooks.postinstall]
run = "npm run setup --silent"   # fails if 'setup' script missing
// after
[hooks.postinstall]
run = "npm run setup --if-present --silent"
Defensive patterns

Strategy: try-catch

Validate before calling

command -v "$hook_cmd" >/dev/null 2>&1 && echo ok || echo "hook command not available"

Try / catch

if ! mise install; then
  echo "install failed at hook stage; inspect hook output above"
  mise cfg ls 2>/dev/null || cat mise.toml
fi

Prevention

When it happens

Trigger: Any hook (e.g. preinstall/postinstall or configured hooks) whose shell command exits with a non-zero status during run_matched_hook's execution path.

Common situations: A postinstall script referencing a missing tool or bad path; a preinstall gate script intentionally failing to block an install; script bugs (syntax error, failed assertion) in hook commands; environment differences making a hook fail under mise but not in an interactive shell.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/578b1253a1233c7f. Report an issue: GitHub.

Appendix: source

Thrown at src/hooks.rs:781

            let mut c = std::process::Command::new(&shell[0]);
            for a in &cmd_args {
                c.raw_arg(a);
            }
            c.env_clear();
            c.envs(env.iter());
            if matches!(hook.hook, Hooks::Preinstall | Hooks::Postinstall) {
                c.current_dir(project_root);
            }
            // Send the hook's stdout to mise's stderr (matching the duct
            // `stdout_to_stderr()` the non-cmd path uses) by handing the child a
            // clone of our stderr handle. Redirecting the descriptor directly —
            // rather than piping through a reader thread — means a hook that
            // spawns a background child holding the write end can't block us
            // waiting for pipe EOF, and stdout/stderr ordering is preserved.
            c.stdout(std::io::stderr().as_handle().try_clone_to_owned()?);
            let status = c.status()?;
            if !status.success() {
                eyre::bail!("hook command failed: {status}");
            }
            return Ok(());
        }
    }

    let cwd = matches!(hook.hook, Hooks::Preinstall | Hooks::Postinstall).then_some(project_root);
    let mut command = cmd(&shell[0], args).full_env(&env);
    if let Some(cwd) = cwd {
        command = command.dir(cwd);
    }
    crate::inline_command::optimize_expression(command, run, &env, cwd, direct_enabled)
        .stdout_to_stderr()
        .run()?;
    Ok(())
}

async fn execute_task(
    config: &Arc<Config>,

View on GitHub (pinned to 533346cc37)