Hooks and Plugins
Hooks and plugins let other programs observe or change Anode runs. Use shell hooks for simple lifecycle commands. Use process plugins when you need a richer protocol or want to contribute tools.
Both fire on agent lifecycle events. Shell hooks are simpler to set up. Process plugins give you more control.
Shell Hooks
Section titled “Shell Hooks”Configuration
Section titled “Configuration”Shell hooks are defined in JSON config files:
| Scope | Path |
|---|---|
| User | ~/.config/anode/hooks.json |
| Project | .anode/hooks.json |
| Project | hooks.json |
All existing files are loaded. User hooks load first; project hooks are appended.
Hook Events
Section titled “Hook Events”| Event | Description | Can reject? |
|---|---|---|
PreToolUse | Before a tool executes | Yes |
PostToolUse | After a tool executes | No |
SessionStart | When a session begins | No |
SessionEnd | When a session ends | No |
UserPromptSubmit | After a run starts and before the submitted prompt reaches the model | Yes |
PreCompact | Immediately before conversation history is compacted for context budget | No |
hooks.json Format
Section titled “hooks.json Format”{ "hooks": { "PreToolUse": [ { "matcher": "bash|edit_file", "hooks": [ { "type": "command", "command": "/path/to/hook.sh", "timeout": 60, "description": "Custom validation" } ] } ], "PostToolUse": [ { "matcher": ".*", "hooks": [ { "type": "command", "command": "/path/to/logger.sh", "timeout": 10, "description": "Log tool results" } ] } ] }}Each event key maps to an array of matcher groups. Each group has a regex matcher for tool-name filtering and an array of hooks to execute.
Matcher
Section titled “Matcher”The matcher field is a regex pattern tested against the tool name. Use ".*" to match all tools. Use "bash|edit_file" to match specific tools. Omit matcher to match everything.
Hook Input
Section titled “Hook Input”Anode sends a JSON object on stdin to each hook command:
{ "event": "PreToolUse", "tool_name": "bash", "tool_args": {"command": "npm test"}, "file_path": "", "command": "npm test", "workspace_root": "/home/user/project"}| Field | Description |
|---|---|
event | Hook event name |
tool_name | Name of the tool being called |
tool_args | Full tool arguments object |
file_path | File path argument (if applicable) |
command | Command argument (if applicable) |
prompt | Submitted user prompt for UserPromptSubmit |
workspace_root | Absolute path to the workspace root |
Environment Variables
Section titled “Environment Variables”Anode sets these environment variables for every hook command:
| Variable | Description |
|---|---|
ANODE_HOOK_EVENT | Hook event name |
ANODE_TOOL_NAME | Tool name being called |
ANODE_FILE_PATH | File path argument |
ANODE_COMMAND | Command argument |
ANODE_WORKSPACE | Workspace root directory |
ANODE_PROJECT_DIR | Workspace root directory |
Hook command strings may also interpolate ${FACTORY_PROJECT_DIR} and ${CLAUDE_PROJECT_DIR}; Anode expands those placeholders to the workspace root before running the command.
Decision Control
Section titled “Decision Control”PreToolUse hooks can control whether the tool executes. Write a JSON decision to stdout:
{"decision": "reject", "reason": "This command modifies production data"}| Decision | Effect |
|---|---|
allow | Permit the tool call |
reject | Block this call with an error message |
block | Block and prevent retries |
If the hook exits without a decision, the tool call proceeds. Other currently wired shell hook events do not control execution.
Example: block destructive commands
Section titled “Example: block destructive commands”#!/usr/bin/env bash# .anode/hooks/no-rm-rf.shset -euo pipefail
INPUT=$(cat)CMD=$(echo "$INPUT" | jq -r '.tool_args.command // ""')
if echo "$CMD" | grep -qE 'rm\s+-rf\s+/'; then echo '{"decision": "reject", "reason": "Refusing rm -rf on root paths"}' exit 0fi
echo '{"decision": "allow"}'Execution Behavior
Section titled “Execution Behavior”- Hooks within a matcher group run in parallel.
- Default timeout: 60 seconds per hook.
- Hooks that exceed the timeout are killed.
- A non-zero exit code does not reject by itself. To reject, print JSON such as
{"decision":"reject","reason":"..."}to stdout. - Non-JSON stdout is treated as feedback content by the shell hook runner. In the current app adapter,
PreToolUsefeedback is surfaced as warnings;SessionStartoutput is not injected into agent context.
Process Plugins
Section titled “Process Plugins”Process plugins are external executables that speak a structured JSON protocol. They can observe events, modify tool calls, and contribute custom tools.
Configuration
Section titled “Configuration”Set plugin paths via environment variable or config:
export ANODE_PLUGINS="/path/to/plugin1:/path/to/plugin-dir"Or in config.json:
{ "plugins": { "paths": ["/path/to/plugin1", "/path/to/plugin-dir"], "timeoutSeconds": 5 }}Paths can be executable files or directories of executables. Anode skips invalid entries silently.
Protocol
Section titled “Protocol”Plugins use protocol version 1. Anode sends a JSON envelope on stdin and sets the ANODE_PLUGIN_EVENT environment variable for each invocation.
Plugin events
Section titled “Plugin events”| Event | Description | Can modify? |
|---|---|---|
on_run_start | Run begins | No |
on_run_end | Run completes | No |
before_model_request | Before sending to the model | No |
pre_compact | Immediately before conversation history is compacted for context budget | No |
after_model_response | After receiving model response | No |
before_tool_call | Before a tool executes | Yes |
after_tool_call | After a tool executes | No |
on_approval_request | Tool needs approval | No |
on_check_result | Validation check completed | No |
describe | Plugin self-description (returns tools) | N/A |
tool_call | Custom tool invocation | N/A |
before_tool_call responses
Section titled “before_tool_call responses”before_tool_call is the most powerful event. A plugin can:
Rewrite the call - return a replacement call object:
{"call": {"name": "bash", "args": {"command": "echo safe"}}}Reject the call - block execution with a reason:
{"reject_reason": "Not permitted by policy"}Synthesize a result - return a complete Anode tool result without executing the original tool:
{ "result": { "tool": "bash", "ok": true, "summary": "Synthetic result", "stdout": "mocked output for testing" }}Return an empty object {} or nothing to let the call proceed unchanged.
Tool contribution
Section titled “Tool contribution”Plugins can contribute custom tools. Anode sends a describe event at startup. The plugin returns a JSON array of tool definitions:
{ "tools": [ { "name": "lint", "description": "Run project linter", "args": { "type": "object", "properties": { "path": {"type": "string", "description": "File to lint"} }, "required": ["path"] } } ]}Plugin tools register as plugin__<plugin>__<tool>. For example, a plugin named quality contributing a lint tool registers as plugin__quality__lint.
Plugin tools:
- Appear in
anode tools list. - Obey normal permission policy.
- Receive
tool_callevents when invoked.
Timeouts and limits
Section titled “Timeouts and limits”| Setting | Default |
|---|---|
| Per-hook timeout | 5 seconds |
| Output limit | 1 MB |
Configure the timeout in config.json under plugins.timeoutSeconds.
Example plugin
Section titled “Example plugin”#!/usr/bin/env python3"""A simple process plugin that logs tool calls."""import jsonimport osimport sys
event = os.environ.get("ANODE_PLUGIN_EVENT", "")data = json.load(sys.stdin)
if event == "describe": json.dump({"tools": []}, sys.stdout)elif event == "before_tool_call": tool = data.get("payload", {}).get("call", {}).get("name", "") with open("/tmp/anode-plugin.log", "a") as f: f.write(f"tool_call: {tool}\n") # Proceed without modification json.dump({}, sys.stdout)elif event == "tool_call": # Handle custom tool invocations json.dump({ "result": { "tool": data.get("payload", {}).get("name", "plugin_tool"), "ok": True, "summary": "plugin tool completed", "stdout": "ok" } }, sys.stdout)Make it executable and add it to your plugin paths.
Hook CLI
Section titled “Hook CLI”Use the hooks command to inspect shell hooks and process plugin hooks:
anode hooks listanode hooks lsanode hooks doctorhooks list shows configured shell hooks and process plugins without failing
on diagnostics. hooks doctor validates hook and process plugin configuration;
successful checks end with Hook diagnostics passed..
Plugin CLI
Section titled “Plugin CLI”Use the plugins command to inspect and reload process plugins:
anode pluginsanode plugins listanode plugins lsanode plugins reloadanode plugins activityanode plugins docanode plugins exec /path/to/pluginplugins is the same as plugins list; plugins ls is an alias. plugins doc prints the process plugin protocol reference. plugins exec discovers one
executable and lists its contributed tools without adding it to config.
plugins activity explains that process plugin activity is not persisted and
points to anode plugins list for configured plugin paths and contributed
tools.
Internal Go Hooks
Section titled “Internal Go Hooks”The run engine has Go-native hook interfaces used internally by Anode, process
plugins, shell hook adapters, and tests. The public pkg/anode SDK does not
currently expose hook registration through anode.Options.
Available hooks
Section titled “Available hooks”| Hook | Signature | Can modify? |
|---|---|---|
OnRunStart | func(runID string) | No |
OnRunEnd | func(runID string, err error) | No |
OnUserPromptSubmit | func(prompt string) *UserPromptSubmitDecision | Yes |
BeforeModelRequest | func(req *Request) | No |
OnPreCompact | func(event *PreCompactEvent) | No |
AfterModelResponse | func(resp *Response) | No |
BeforeToolCall | func(call *ToolCall) *ToolCallDecision | Yes |
AfterToolCall | func(call *ToolCall, result *ToolResult) | No |
OnApprovalRequest | func(req *ApprovalRequest) | No |
OnCheckResult | func(result *CheckResult) | No |
BeforeToolCall can return a decision to rewrite the call, reject it, or
synthesize a result — the same behavior process plugins expose.
Keep Going
Section titled “Keep Going”- Headless Execution - run Anode in scripts and CI/CD pipelines
- Permissions - control what tools are allowed to do
- Tools - built-in tools and the tool protocol
- Configuration - global and project-level settings