Introduction
OpenClaw hooks are automation scripts that respond to lifecycle events inside the gateway. While bundled hooks cover common scenarios, custom hooks let you build workflow-specific automations: Slack notifications on session start, git auto-commits after code edits, data validation before tool execution, or metric collection after every agent response. Custom hooks live in their own directory, declare metadata in a HOOK.md file, and export a handler function that responds to the events you choose.
What are OpenClaw hooks? OpenClaw hooks are event-driven automation scripts that subscribe to gateway lifecycle events (session creation, message processing, tool execution, errors) and run custom logic in response. They follow a plugin architecture similar to ESLint plugins or VS Code extensions.
How Do OpenClaw Hooks Work?
Hooks follow a plugin architecture used by tools like ESLint plugins, Webpack loaders, and VS Code extensions. You define metadata to describe what your plugin does, implement a handler to provide the behavior, and register it with the system. This pattern scales from simple single-purpose hooks to full hook packs distributed via npm.
Every custom hook lives in its own directory under ~/.openclaw/hooks/. Here is the required structure:
text~/.openclaw/hooks/my-custom-hook/ HOOK.md # metadata and event subscriptions handler.ts # the hook handler implementation
The directory name serves as the hook's identifier. You need exactly two files: HOOK.md for metadata and handler.ts for the handler logic.
What Hook Lifecycle Events Are Available?
OpenClaw dispatches events at key points in the gateway lifecycle. Here are the events you can subscribe to:
| Event | Trigger | Common Use Case |
|---|---|---|
command:new | New session created | Logging, notifications |
command:reset | Session reset | Cleanup, archival |
message:received | User message arrives | Validation, filtering |
message:sent | Agent response sent | Logging, analytics |
tool:before | Before tool execution | Access control, audit |
tool:after | After tool execution | Result validation |
error:agent | Agent error occurs | Alerting, fallback |
session:idle | Session goes idle | Resource cleanup |
Subscribe to specific events in your HOOK.md frontmatter. Hooks only receive events they subscribe to, so subscribing to tool:before and tool:after means your handler is never called for session or message events.
How Do You Build a Custom Hook in OpenClaw?
Step 1: Create the HOOK.md Metadata
The HOOK.md file uses YAML frontmatter to declare the hook's identity and event subscriptions:
markdown--- name: my-custom-hook description: Sends a notification when a session is created metadata: emoji: "🔔" events: - command:new - command:reset --- # My Custom Hook This hook sends a desktop notification whenever a new session is started or an existing session is reset.
The frontmatter includes name and description fields that appear in openclaw hooks list. The metadata object contains an emoji for display purposes and an events array listing which events this hook subscribes to. Events use the type:action format. The body of the markdown can contain any documentation you want.
Step 2: Implement the Handler
The handler.ts file exports the async handler function:
typescriptimport type { HookHandler } from '@openclaw/types'; const handler: HookHandler = async (event) => { if (event.type === 'command') { const message = event.action === 'new' ? `New session: ${event.sessionKey}` : `Session reset: ${event.sessionKey}`; await sendNotification({ title: 'OpenClaw', body: message, timestamp: event.timestamp }); } }; export default handler;
The handler imports the HookHandler type for type safety, checks the event type and action, and performs the desired automation. Even though OpenClaw only dispatches events your hook subscribes to, defensive filtering is good practice.
Practical Hook Examples
Example 1: Slack Notification Hook
Send a Slack message whenever a new session starts:
typescriptimport type { HookHandler } from '@openclaw/types'; const handler: HookHandler = async (event) => { if (event.type === 'command' && event.action === 'new') { await fetch(process.env.SLACK_WEBHOOK_URL!, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: `New OpenClaw session started: ${event.sessionKey}`, channel: '#openclaw-activity', }), }); } }; export default handler;
HOOK.md for this hook subscribes to command:new only.
Example 2: Git Auto-Commit Hook
Automatically commit file changes after a tool writes to disk:
typescriptimport type { HookHandler } from '@openclaw/types'; import { execSync } from 'child_process'; const handler: HookHandler = async (event) => { if (event.type === 'tool' && event.action === 'after') { const toolName = event.payload?.toolName; if (toolName === 'file_write' || toolName === 'file_edit') { const filePath = event.payload?.args?.path; if (filePath) { execSync(`git add "${filePath}" && git commit -m "auto: ${toolName} via OpenClaw"`, { cwd: process.env.PROJECT_DIR, }); } } } }; export default handler;
HOOK.md subscribes to tool:after. The hook checks which tool ran and only commits for file-write operations.
Example 3: Data Validation Hook
Validate tool inputs before execution to enforce security policies:
typescriptimport type { HookHandler } from '@openclaw/types'; const BLOCKED_PATHS = ['/etc/', '/usr/', '/System/']; const handler: HookHandler = async (event) => { if (event.type === 'tool' && event.action === 'before') { const filePath = event.payload?.args?.path; if (filePath && BLOCKED_PATHS.some(p => filePath.startsWith(p))) { throw new Error(`Blocked: cannot access ${filePath}`); } } }; export default handler;
Throwing an error in a tool:before hook prevents the tool from executing. This is useful for enforcing file system boundaries.
Example 4: Conditional Error Alert Hook
Send alerts only for repeated errors, not one-off failures:
typescriptimport type { HookHandler } from '@openclaw/types'; const errorCounts = new Map<string, number>(); const handler: HookHandler = async (event) => { if (event.type === 'error' && event.action === 'agent') { const key = event.payload?.errorCode || 'unknown'; const count = (errorCounts.get(key) || 0) + 1; errorCounts.set(key, count); if (count >= 3) { await fetch(process.env.ALERT_WEBHOOK!, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ severity: 'high', message: `Repeated error (${count}x): ${key}`, sessionKey: event.sessionKey, }), }); errorCounts.delete(key); } } }; export default handler;
When Should You Use Hooks vs Heartbeat?
OpenClaw provides two automation mechanisms: hooks and heartbeat. Here is when to use each:
| Criteria | Hooks | Heartbeat |
|---|---|---|
| Trigger | Event-driven (reacts to something) | Time-driven (runs on a schedule) |
| Use case | Notifications, validation, logging | Polling, periodic checks, cron tasks |
| Execution | Immediate, synchronous with event | Periodic, independent of events |
| Can block? | Yes (tool:before can throw) | No, runs in its own loop |
| State | Stateless by default (use external storage) | Maintains heartbeat interval state |
| Example | "Alert me when an error happens" | "Check API health every 5 minutes" |
Rule of thumb: If you need to react to something that just happened, use a hook. If you need to check or do something on a schedule, use heartbeat or cron jobs.
Managing Hooks with the CLI
bash# List all discovered hooks (workspace, managed, bundled) openclaw hooks list # Show detailed info about a specific hook openclaw hooks info my-custom-hook # Validate hook configuration openclaw hooks check # Enable a disabled hook openclaw hooks enable my-custom-hook # Disable a hook without deleting it openclaw hooks disable my-custom-hook
The check command is particularly useful for verifying that your HOOK.md frontmatter is valid and that handler.ts exports a proper handler.
Distributing Hooks as npm Packages
For sharing hooks across teams, create hook packs:
json{ "name": "@myorg/openclaw-hooks", "version": "1.0.0", "openclaw": { "hooks": [ "hooks/notification-hook", "hooks/metrics-hook" ] } }
The openclaw.hooks field in package.json lists the relative paths to hook directories within the package. Each directory follows the same structure with HOOK.md and handler.ts. This allows teams to share hook collections through their existing npm workflow.
Common Pitfalls
- Missing HOOK.md frontmatter fields: Omitting
nameoreventsin the YAML frontmatter will cause the hook to fail validation and not be discovered. - Forgetting to export default: The handler must be the default export of
handler.ts; named exports are not recognized by the hook loader. - Not running
openclaw hooks check: After creating or modifying a hook, skipping validation can leave you with a silently broken hook. - Subscribing to too many events: Each event subscription adds processing overhead. Subscribe only to events your hook actually needs.
- Blocking in
tool:beforehooks: Throwing errors in pre-execution hooks blocks the tool entirely. Use this power carefully and always log when you block.
Best Practices
- Run
openclaw hooks checkafter every change to your hook files to catch configuration errors early. - Write descriptive
HOOK.mddocumentation so other team members understand the hook's purpose and event subscriptions. - Start with a single event subscription and expand as needed rather than subscribing to all events upfront.
- Use environment variables for webhook URLs and secrets — never hardcode them in handler code.
- For complex hooks, consider using sub-agents to offload processing to a dedicated agent.
Summary
- Custom hooks live in
~/.openclaw/hooks/<hook-name>/withHOOK.mdandhandler.ts. HOOK.mdfrontmatter declaresname,description, andmetadatawithemojiandevents.handler.tsexports a default asyncHookHandlerfunction.- Eight lifecycle events cover the full gateway pipeline: session, message, tool, and error stages.
- Use hooks for event-driven automation and heartbeat for time-driven tasks.
- The CLI provides
list,info,check,enable, anddisablecommands for hook management. - Hook packs distribute multiple hooks via npm using
openclaw.hooksinpackage.json.