Hooks¶
Hooks provide an extensible event-driven system for automating actions in response to agent commands and events. Hooks are automatically discovered from directories and can be managed via CLI commands, similar to how skills work in OpenClaw.
Getting Oriented¶
Hooks are small scripts that run when something happens. There are two kinds:
- Hooks (this page): run inside the Gateway when agent events fire, like
/new,/reset,/stop, or lifecycle events. - Webhooks: external HTTP webhooks that let other systems trigger work in OpenClaw. See Webhook Hooks or use
openclaw webhooksfor Gmail helper commands.
Hooks can also be bundled inside plugins; see Plugins.
Common uses:
- Save a memory snapshot when you reset a session
- Keep an audit trail of commands for troubleshooting or compliance
- Trigger follow-up automation when a session starts or ends
- Write files into the agent workspace or call external APIs when events fire
If you can write a small TypeScript function, you can write a hook. Hooks are discovered automatically, and you enable or disable them via the CLI.
Overview¶
The hooks system allows you to:
- Save session context to memory when
/newis issued - Log all commands for auditing
- Trigger custom automations on agent lifecycle events
- Extend OpenClaw's behavior without modifying core code
Getting Started¶
Bundled Hooks¶
OpenClaw ships with four bundled hooks that are automatically discovered:
- 💾 session-memory: Saves session context to your agent workspace (default
~/.openclaw/workspace/memory/) when you issue/new - 📎 bootstrap-extra-files: Injects additional workspace bootstrap files from configured glob/path patterns during
agent:bootstrap - 📝 command-logger: Logs all command events to
~/.openclaw/logs/commands.log - 🚀 boot-md: Runs
BOOT.mdwhen the gateway starts (requires internal hooks enabled)
List available hooks:
Enable a hook:
Check hook status:
Get detailed information:
Onboarding¶
During onboarding (openclaw onboard), you'll be prompted to enable recommended hooks. The wizard automatically discovers eligible hooks and presents them for selection.
Hook Discovery¶
Hooks are automatically discovered from three directories (in order of precedence):
- Workspace hooks:
<workspace>/hooks/(per-agent, highest precedence) - Managed hooks:
~/.openclaw/hooks/(user-installed, shared across workspaces) - Bundled hooks:
<openclaw>/dist/hooks/bundled/(shipped with OpenClaw)
Managed hook directories can be either a single hook or a hook pack (package directory).
Each hook is a directory containing:
Hook Packs (npm/archives)¶
Hook packs are standard npm packages that export one or more hooks via openclaw.hooks in
package.json. Install them with:
Npm specs are registry-only (package name + optional version/tag). Git/URL/file specs are rejected.
Example package.json:
{
"name": "@acme/my-hooks",
"version": "0.1.0",
"openclaw": {
"hooks": ["./hooks/my-hook", "./hooks/other-hook"]
}
}
Each entry points to a hook directory containing HOOK.md and handler.ts (or index.ts).
Hook packs can ship dependencies; they will be installed under ~/.openclaw/hooks/<id>.
Security note: openclaw hooks install installs dependencies with npm install --ignore-scripts
(no lifecycle scripts). Keep hook pack dependency trees "pure JS/TS" and avoid packages that rely
on postinstall builds.
Hook Structure¶
HOOK.md Format¶
The HOOK.md file contains metadata in YAML frontmatter plus Markdown documentation:
---
name: my-hook
description: "Short description of what this hook does"
homepage: https://docs.openclaw.ai/automation/hooks#my-hook
metadata:
{ "openclaw": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } }
---
# My Hook
Detailed documentation goes here...
## What It Does
- Listens for `/new` commands
- Performs some action
- Logs the result
## Requirements
- Node.js must be installed
## Configuration
No configuration needed.
Metadata Fields¶
The metadata.openclaw object supports:
emoji: Display emoji for CLI (e.g.,"💾")events: Array of events to listen for (e.g.,["command:new", "command:reset"])export: Named export to use (defaults to"default")homepage: Documentation URLrequires: Optional requirementsbins: Required binaries on PATH (e.g.,["git", "node"])anyBins: At least one of these binaries must be presentenv: Required environment variablesconfig: Required config paths (e.g.,["workspace.dir"])os: Required platforms (e.g.,["darwin", "linux"])always: Bypass eligibility checks (boolean)install: Installation methods (for bundled hooks:[{"id":"bundled","kind":"bundled"}])
Handler Implementation¶
The handler.ts file exports a HookHandler function:
import type { HookHandler } from "../../src/hooks/hooks.js";
const myHandler: HookHandler = async (event) => {
// Only trigger on 'new' command
if (event.type !== "command" || event.action !== "new") {
return;
}
console.log(`[my-hook] New command triggered`);
console.log(` Session: ${event.sessionKey}`);
console.log(` Timestamp: ${event.timestamp.toISOString()}`);
// Your custom logic here
// Optionally send message to user
event.messages.push("✨ My hook executed!");
};
export default myHandler;
Event Context¶
Each event includes:
{
type: 'command' | 'session' | 'agent' | 'gateway',
action: string, // e.g., 'new', 'reset', 'stop'
sessionKey: string, // Session identifier
timestamp: Date, // When the event occurred
messages: string[], // Push messages here to send to user
context: {
sessionEntry?: SessionEntry,
sessionId?: string,
sessionFile?: string,
commandSource?: string, // e.g., 'whatsapp', 'telegram'
senderId?: string,
workspaceDir?: string,
bootstrapFiles?: WorkspaceBootstrapFile[],
cfg?: OpenClawConfig
}
}
Event Types¶
Command Events¶
Triggered when agent commands are issued:
command: All command events (general listener)command:new: When/newcommand is issuedcommand:reset: When/resetcommand is issuedcommand:stop: When/stopcommand is issued
Agent Events¶
agent:bootstrap: Before workspace bootstrap files are injected (hooks may mutatecontext.bootstrapFiles)
Gateway Events¶
Triggered when the gateway starts:
gateway:startup: After channels start and hooks are loaded
Tool Result Hooks (Plugin API)¶
These hooks are not event-stream listeners; they let plugins synchronously adjust tool results before OpenClaw persists them.
tool_result_persist: transform tool results before they are written to the session transcript. Must be synchronous; return the updated tool result payload orundefinedto keep it as-is. See Agent Loop.
Future Events¶
Planned event types:
session:start: When a new session beginssession:end: When a session endsagent:error: When an agent encounters an errormessage:sent: When a message is sentmessage:received: When a message is received
Creating Custom Hooks¶
1. Choose Location¶
- Workspace hooks (
<workspace>/hooks/): Per-agent, highest precedence - Managed hooks (
~/.openclaw/hooks/): Shared across workspaces
2. Create Directory Structure¶
3. Create HOOK.md¶
---
name: my-hook
description: "Does something useful"
metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } }
---
# My Custom Hook
This hook does something useful when you issue `/new`.
4. Create handler.ts¶
import type { HookHandler } from "../../src/hooks/hooks.js";
const handler: HookHandler = async (event) => {
if (event.type !== "command" || event.action !== "new") {
return;
}
console.log("[my-hook] Running!");
// Your logic here
};
export default handler;
5. Enable and Test¶
# Verify hook is discovered
openclaw hooks list
# Enable it
openclaw hooks enable my-hook
# Restart your gateway process (menu bar app restart on macOS, or restart your dev process)
# Trigger the event
# Send /new via your messaging channel
Configuration¶
New Config Format (Recommended)¶
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"session-memory": { "enabled": true },
"command-logger": { "enabled": false }
}
}
}
}
Per-Hook Configuration¶
Hooks can have custom configuration:
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"my-hook": {
"enabled": true,
"env": {
"MY_CUSTOM_VAR": "value"
}
}
}
}
}
}
Extra Directories¶
Load hooks from additional directories:
Legacy Config Format (Still Supported)¶
The old config format still works for backwards compatibility:
{
"hooks": {
"internal": {
"enabled": true,
"handlers": [
{
"event": "command:new",
"module": "./hooks/handlers/my-handler.ts",
"export": "default"
}
]
}
}
}
Note: module must be a workspace-relative path. Absolute paths and traversal outside the workspace are rejected.
Migration: Use the new discovery-based system for new hooks. Legacy handlers are loaded after directory-based hooks.
CLI Commands¶
List Hooks¶
# List all hooks
openclaw hooks list
# Show only eligible hooks
openclaw hooks list --eligible
# Verbose output (show missing requirements)
openclaw hooks list --verbose
# JSON output
openclaw hooks list --json
Hook Information¶
# Show detailed info about a hook
openclaw hooks info session-memory
# JSON output
openclaw hooks info session-memory --json
Check Eligibility¶
Enable/Disable¶
# Enable a hook
openclaw hooks enable session-memory
# Disable a hook
openclaw hooks disable command-logger
Bundled hook reference¶
session-memory¶
Saves session context to memory when you issue /new.
Events: command:new
Requirements: workspace.dir must be configured
Output: <workspace>/memory/YYYY-MM-DD-slug.md (defaults to ~/.openclaw/workspace)
What it does:
- Uses the pre-reset session entry to locate the correct transcript
- Extracts the last 15 lines of conversation
- Uses LLM to generate a descriptive filename slug
- Saves session metadata to a dated memory file
Example output:
# Session: 2026-01-16 14:30:00 UTC
- **Session Key**: agent:main:main
- **Session ID**: abc123def456
- **Source**: telegram
Filename examples:
2026-01-16-vendor-pitch.md2026-01-16-api-design.md2026-01-16-1430.md(fallback timestamp if slug generation fails)
Enable:
bootstrap-extra-files¶
Injects additional bootstrap files (for example monorepo-local AGENTS.md / TOOLS.md) during agent:bootstrap.
Events: agent:bootstrap
Requirements: workspace.dir must be configured
Output: No files written; bootstrap context is modified in-memory only.
Config:
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"bootstrap-extra-files": {
"enabled": true,
"paths": ["packages/*/AGENTS.md", "packages/*/TOOLS.md"]
}
}
}
}
}
Notes:
- Paths are resolved relative to workspace.
- Files must stay inside workspace (realpath-checked).
- Only recognized bootstrap basenames are loaded.
- Subagent allowlist is preserved (
AGENTS.mdandTOOLS.mdonly).
Enable:
command-logger¶
Logs all command events to a centralized audit file.
Events: command
Requirements: None
Output: ~/.openclaw/logs/commands.log
What it does:
- Captures event details (command action, timestamp, session key, sender ID, source)
- Appends to log file in JSONL format
- Runs silently in the background
Example log entries:
{"timestamp":"2026-01-16T14:30:00.000Z","action":"new","sessionKey":"agent:main:main","senderId":"+1234567890","source":"telegram"}
{"timestamp":"2026-01-16T15:45:22.000Z","action":"stop","sessionKey":"agent:main:main","senderId":"[email protected]","source":"whatsapp"}
View logs:
# View recent commands
tail -n 20 ~/.openclaw/logs/commands.log
# Pretty-print with jq
cat ~/.openclaw/logs/commands.log | jq .
# Filter by action
grep '"action":"new"' ~/.openclaw/logs/commands.log | jq .
Enable:
boot-md¶
Runs BOOT.md when the gateway starts (after channels start).
Internal hooks must be enabled for this to run.
Events: gateway:startup
Requirements: workspace.dir must be configured
What it does:
- Reads
BOOT.mdfrom your workspace - Runs the instructions via the agent runner
- Sends any requested outbound messages via the message tool
Enable:
Best Practices¶
Keep Handlers Fast¶
Hooks run during command processing. Keep them lightweight:
// ✓ Good - async work, returns immediately
const handler: HookHandler = async (event) => {
void processInBackground(event); // Fire and forget
};
// ✗ Bad - blocks command processing
const handler: HookHandler = async (event) => {
await slowDatabaseQuery(event);
await evenSlowerAPICall(event);
};
Handle Errors Gracefully¶
Always wrap risky operations:
const handler: HookHandler = async (event) => {
try {
await riskyOperation(event);
} catch (err) {
console.error("[my-handler] Failed:", err instanceof Error ? err.message : String(err));
// Don't throw - let other handlers run
}
};
Filter Events Early¶
Return early if the event isn't relevant:
const handler: HookHandler = async (event) => {
// Only handle 'new' commands
if (event.type !== "command" || event.action !== "new") {
return;
}
// Your logic here
};
Use Specific Event Keys¶
Specify exact events in metadata when possible:
Rather than:
Debugging¶
Enable Hook Logging¶
The gateway logs hook loading at startup:
Registered hook: session-memory -> command:new
Registered hook: bootstrap-extra-files -> agent:bootstrap
Registered hook: command-logger -> command
Registered hook: boot-md -> gateway:startup
Check Discovery¶
List all discovered hooks:
Check Registration¶
In your handler, log when it's called:
const handler: HookHandler = async (event) => {
console.log("[my-handler] Triggered:", event.type, event.action);
// Your logic
};
Verify Eligibility¶
Check why a hook isn't eligible:
Look for missing requirements in the output.
Testing¶
Gateway Logs¶
Monitor gateway logs to see hook execution:
Test Hooks Directly¶
Test your handlers in isolation:
import { test } from "vitest";
import { createHookEvent } from "./src/hooks/hooks.js";
import myHandler from "./hooks/my-hook/handler.js";
test("my handler works", async () => {
const event = createHookEvent("command", "new", "test-session", {
foo: "bar",
});
await myHandler(event);
// Assert side effects
});
Architecture¶
Core Components¶
src/hooks/types.ts: Type definitionssrc/hooks/workspace.ts: Directory scanning and loadingsrc/hooks/frontmatter.ts: HOOK.md metadata parsingsrc/hooks/config.ts: Eligibility checkingsrc/hooks/hooks-status.ts: Status reportingsrc/hooks/loader.ts: Dynamic module loadersrc/cli/hooks-cli.ts: CLI commandssrc/gateway/server-startup.ts: Loads hooks at gateway startsrc/auto-reply/reply/commands-core.ts: Triggers command events
Discovery Flow¶
Gateway startup
↓
Scan directories (workspace → managed → bundled)
↓
Parse HOOK.md files
↓
Check eligibility (bins, env, config, os)
↓
Load handlers from eligible hooks
↓
Register handlers for events
Event Flow¶
User sends /new
↓
Command validation
↓
Create hook event
↓
Trigger hook (all registered handlers)
↓
Command processing continues
↓
Session reset
Troubleshooting¶
Hook Not Discovered¶
- Check directory structure:
- Verify HOOK.md format:
- List all discovered hooks:
Hook Not Eligible¶
Check requirements:
Look for missing:
- Binaries (check PATH)
- Environment variables
- Config values
- OS compatibility
Hook Not Executing¶
- Verify hook is enabled:
-
Restart your gateway process so hooks reload.
-
Check gateway logs for errors:
Handler Errors¶
Check for TypeScript/import errors:
Migration Guide¶
From Legacy Config to Discovery¶
Before:
{
"hooks": {
"internal": {
"enabled": true,
"handlers": [
{
"event": "command:new",
"module": "./hooks/handlers/my-handler.ts"
}
]
}
}
}
After:
- Create hook directory:
mkdir -p ~/.openclaw/hooks/my-hook
mv ./hooks/handlers/my-handler.ts ~/.openclaw/hooks/my-hook/handler.ts
- Create HOOK.md:
---
name: my-hook
description: "My custom hook"
metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } }
---
# My Hook
Does something useful.
- Update config:
- Verify and restart your gateway process:
Benefits of migration:
- Automatic discovery
- CLI management
- Eligibility checking
- Better documentation
- Consistent structure