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 jsonFrom 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 toolsEach 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}'
fiHook 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>&1Exit 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.shmanifest.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
fiIn 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:
- Verify the directory exists:
ls ~/.mentu/plugins/<name>/manifest.json - Validate the manifest:
cat ~/.mentu/plugins/<name>/manifest.json | jq . - Check that hook commands are executable:
chmod +x ~/.mentu/plugins/<name>/hooks/*.sh - Run
mentu plugin listto 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.