Mentu

Plugins

Plugins

Plugins extend mentu's execution lifecycle. A plugin is a directory under ~/.mentu/plugins/<name>/ containing a manifest.json that declares hooks and tools. Plugins are loaded once at engine startup and their hooks fire automatically during recipe execution.

Getting started

# List loaded plugins
mentu plugin list
 
# JSON output for scripting
mentu plugin list --format json

From a script:

const plugins = mentu.plugin.list();
for (const p of plugins) {
  console.log(`${p.name} v${p.version}: ${p.hooks} hooks, ${p.tools} tools`);
}

Plugin structure

~/.mentu/plugins/
  my-plugin/
    manifest.json         # Plugin declaration
    hooks/
      before-step.sh      # Hook scripts
      after-step.sh
    tools/
      my-tool.sh          # Custom dispatch tools

Each subdirectory of ~/.mentu/plugins/ is scanned at startup. If a valid manifest.json exists, the plugin is loaded. Directories without a manifest are silently skipped.

Manifest format

{
  "name": "my-plugin",
  "version": "1.0.0",
  "description": "Example plugin with hooks and tools",
  "hooks": [
    {
      "event": "before_step",
      "command": "~/.mentu/plugins/my-plugin/hooks/before-step.sh",
      "timeout": 10,
      "can_abort": true
    },
    {
      "event": "after_step",
      "command": "~/.mentu/plugins/my-plugin/hooks/after-step.sh"
    }
  ],
  "tools": [
    {
      "name": "my-custom-tool",
      "command": "~/.mentu/plugins/my-plugin/tools/my-tool.sh",
      "description": "Does something useful"
    }
  ]
}
Field Required Description
name Yes Plugin identifier
version Yes Semver string
description Yes Human-readable description
hooks Yes Array of hook registrations
tools No Array of tool registrations

Hook events

Event When it fires Abortable
before_step Before each step begins Yes
after_step After each step completes successfully No
on_error When a step fails No
before_merge Before worktree merge-back (canary phase) Yes
after_capture After a CIR signal is captured No
on_dispatch When a dispatch tool is invoked No

Abortable hooks must set "can_abort": true in the manifest. The hook aborts the operation by writing {"abort": true} to stdout. Non-zero exit codes alone do not abort; the engine checks the stdout JSON explicitly.

Hook execution model

Hook commands run via /bin/sh -c and receive JSON context on stdin. The context object contains fields relevant to the event:

{
  "step_label": "build-artifacts",
  "recipe_name": "deploy-pipeline",
  "workspace": "/Users/you/project",
  "status": "success"
}

A before_step hook that gates on recipe name:

#!/bin/bash
# ~/.mentu/plugins/gate/hooks/before-step.sh
CONTEXT=$(cat)
RECIPE=$(echo "$CONTEXT" | jq -r '.recipe_name')
 
if [[ "$RECIPE" == "dangerous-recipe" ]]; then
  echo '{"abort": true}'
fi

Hook registration fields

Field Required Default Description
event Yes (required) One of the six event types above
command Yes (required) Shell command to execute
timeout No 10 Seconds before the watchdog kills the process
can_abort No false Whether this hook can abort the operation

Timeout watchdog

Every hook execution has a timeout watchdog. If a hook script does not complete within its timeout (default 10 seconds), the process is terminated and the hook is treated as a failure. Set a custom timeout per hook:

{
  "event": "before_merge",
  "command": "~/.mentu/plugins/my-plugin/hooks/pre-merge-check.sh",
  "timeout": 30,
  "can_abort": true
}

Long-running validation (test suites, remote checks) should use a generous timeout. Keep quick hooks at the default to avoid slowing down execution.

Custom dispatch tools

Plugins can register tools that appear in mentu dispatch list and are invokable via mentu dispatch send. Tool commands receive JSON parameters on stdin and return output on stdout.

Register a tool in the manifest

{
  "tools": [
    {
      "name": "lint-check",
      "command": "~/.mentu/plugins/my-plugin/tools/lint.sh",
      "description": "Run project linter and return results"
    }
  ]
}

Invoke a plugin tool

mentu dispatch send lint-check --params '{"path": "./src"}'

Tool execution

The tool's shell command receives JSON params on stdin:

#!/bin/bash
# ~/.mentu/plugins/my-plugin/tools/lint.sh
PARAMS=$(cat)
PATH_ARG=$(echo "$PARAMS" | jq -r '.path // "."')
eslint "$PATH_ARG" --format json 2>&1

Exit code 0 means success. Any other exit code is treated as a tool failure. Output is captured from stdout and returned to the caller.

Plugin tools appear alongside built-in dispatch tools (agent_start, cir_query, formula_dispatch, etc.) in mentu dispatch list.

Multiple hooks per event

Multiple plugins can register hooks for the same event. They fire in alphabetical order by plugin name. If an abortable hook aborts, remaining hooks for that event are skipped.

Plugin "alpha" before_step → runs first
Plugin "beta" before_step  → runs second (skipped if alpha aborted)

Example: CI gate plugin

A plugin that blocks worktree merges unless CI passes:

~/.mentu/plugins/ci-gate/
  manifest.json
  hooks/
    pre-merge.sh

manifest.json:

{
  "name": "ci-gate",
  "version": "1.0.0",
  "description": "Block merges unless CI passes",
  "hooks": [
    {
      "event": "before_merge",
      "command": "~/.mentu/plugins/ci-gate/hooks/pre-merge.sh",
      "timeout": 60,
      "can_abort": true
    }
  ]
}

hooks/pre-merge.sh:

#!/bin/bash
CONTEXT=$(cat)
WORKSPACE=$(echo "$CONTEXT" | jq -r '.workspace')
 
cd "$WORKSPACE"
if ! gh run list --branch HEAD --json conclusion -q '.[0].conclusion' | grep -q success; then
  echo '{"abort": true}'
  exit 0
fi

In scripts

// List all plugins
const plugins = mentu.plugin.list();
for (const p of plugins) {
  console.log(`${p.name} v${p.version}`);
  console.log(`  ${p.description}`);
  console.log(`  Hooks: ${p.hooks}, Tools: ${p.tools}`);
  console.log(`  Hook events: ${p.hook_events.join(', ')}`);
  console.log(`  Tool names: ${p.tool_names.join(', ')}`);
}

The JSON output from mentu plugin list --format json includes hook_events and tool_names arrays for programmatic inspection.

Debugging plugins

If a plugin is not loading:

  1. Verify the directory exists: ls ~/.mentu/plugins/<name>/manifest.json
  2. Validate the manifest: cat ~/.mentu/plugins/<name>/manifest.json | jq .
  3. Check that hook commands are executable: chmod +x ~/.mentu/plugins/<name>/hooks/*.sh
  4. Run mentu plugin list to confirm the plugin appears

Hook failures are logged to the engine log (category: PluginRegistry). Check stderr output and exit codes if hooks are not behaving as expected.

© 2026 Mentu.