Building Custom Hooks

+15 Mana ✨

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:

EventTriggerCommon Use Case
command:newNew session createdLogging, notifications
command:resetSession resetCleanup, archival
message:receivedUser message arrivesValidation, filtering
message:sentAgent response sentLogging, analytics
tool:beforeBefore tool executionAccess control, audit
tool:afterAfter tool executionResult validation
error:agentAgent error occursAlerting, fallback
session:idleSession goes idleResource 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:

typescript
import 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:

typescript
import 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:

typescript
import 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:

typescript
import 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:

typescript
import 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:

CriteriaHooksHeartbeat
TriggerEvent-driven (reacts to something)Time-driven (runs on a schedule)
Use caseNotifications, validation, loggingPolling, periodic checks, cron tasks
ExecutionImmediate, synchronous with eventPeriodic, independent of events
Can block?Yes (tool:before can throw)No, runs in its own loop
StateStateless 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 name or events in 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:before hooks: 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 check after every change to your hook files to catch configuration errors early.
  • Write descriptive HOOK.md documentation 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>/ with HOOK.md and handler.ts.
  • HOOK.md frontmatter declares name, description, and metadata with emoji and events.
  • handler.ts exports a default async HookHandler function.
  • 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, and disable commands for hook management.
  • Hook packs distribute multiple hooks via npm using openclaw.hooks in package.json.
✓ Completed