Automate OpenClaw Workflows with Custom Hooks
Getting Oriented
Section titled “Getting Oriented”Hooks are essentially small scripts that run when specific events occur. You will work with two main types:
- Hooks (what we are covering here): These run inside the Gateway when agent events fire, such as
/new,/reset,/stop, or other lifecycle events. - Webhooks: These are external HTTP webhooks that allow other systems to trigger work in OpenClaw. You can check out Webhook Hooks or use
openclaw webhooksfor Gmail helper commands.
You can also find hooks bundled inside plugins; take a look at Plugin hooks. Running openclaw hooks list will show you both standalone hooks and those managed by plugins.
Common ways to use them:
- 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. Managed and bundled hooks are treated as trusted local code. While workspace hooks are discovered automatically, OpenClaw keeps them disabled until you explicitly enable them through the CLI or your configuration.
Overview
Section titled “Overview”The hooks system gives you the ability to:
- Save session context to memory when
/newis issued. - Log all commands for auditing purposes.
- Trigger custom automations on agent lifecycle events.
- Extend how OpenClaw behaves without modifying the core code.
Getting Started
Section titled “Getting Started”Bundled Hooks
Section titled “Bundled Hooks”OpenClaw comes with four bundled hooks that are automatically discovered:
- 💾 session-memory: Saves session context to your agent workspace (default
~/.openclaw/workspace/memory/) when you issue/newor/reset. - 📎 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 (this requires internal hooks to be enabled).
List your available hooks:
openclaw hooks listEnable a specific hook:
openclaw hooks enable session-memoryCheck the status of your hooks:
openclaw hooks checkGet detailed information about a hook:
openclaw hooks info session-memoryOnboarding
Section titled “Onboarding”During the onboarding process (openclaw onboard), you will be prompted to enable recommended hooks. The wizard automatically finds eligible hooks and presents them for you to select.
Trust Boundary
Section titled “Trust Boundary”Hooks run inside the Gateway process. You should treat bundled hooks, managed hooks, plugin hooks, and hooks.internal.load.extraDirs as trusted local code. Workspace hooks located under <workspace>/hooks/ are repo-local code, so OpenClaw requires an explicit enable step before it will load them.
Hook Discovery
Section titled “Hook Discovery”Hooks are automatically discovered from these directories, following this order of increasing override precedence:
- Bundled hooks: These are shipped with OpenClaw and located at
<openclaw>/dist/hooks/bundled/for npm installs (or a siblinghooks/bundled/for compiled binaries). - Plugin hooks: These are hooks bundled inside your installed plugins (see Plugin hooks).
- Managed hooks: Located at
~/.openclaw/hooks/. These are user-installed and shared across workspaces; they can override bundled and plugin hooks. Extra hook directories configured viahooks.internal.load.extraDirsare also treated as managed hooks. - Workspace hooks: Located at
<workspace>/hooks/. These are per-agent and disabled by default until you enable them. They cannot override hooks from other sources.
Workspace hooks can add new hook names for a repo, but they cannot override bundled, managed, or plugin-provided hooks that use the same name.
Managed hook directories can be either a single hook or a hook pack (a package directory). Each hook is a directory containing:
my-hook/├── HOOK.md # Metadata + documentation└── handler.ts # Handler implementationHook Packs (npm/archives)
Section titled “Hook Packs (npm/archives)”Hook packs are standard npm packages that export one or more hooks via openclaw.hooks in your package.json. You can install them using this command:
openclaw plugins install <path-or-spec>Keep in mind that npm specs are registry-only, meaning you use the package name plus an optional exact version or dist-tag. Git, URL, or file specs and semver ranges won’t work here.
If you use bare specs or @latest, you stay on the stable track. If npm resolves one of these to a prerelease version, OpenClaw will stop and ask you to opt in explicitly using a tag like @beta or @rc, or an exact prerelease version.
Here is an example package.json:
{ "name": "@acme/my-hooks", "version": "0.1.0", "openclaw": { "hooks": ["./hooks/my-hook", "./hooks/other-hook"] }}Each entry in that list points to a hook directory that contains a HOOK.md and a handler.ts (or index.ts). Hook packs can ship with their own dependencies, which get installed under ~/.openclaw/hooks/<id>. Just ensure that each openclaw.hooks entry stays inside the package directory after symlinks are resolved; if an entry escapes, it will be rejected.
For security, openclaw plugins install runs dependencies with npm install --ignore-scripts, so no lifecycle scripts will execute. You should keep your hook pack dependency trees “pure JS/TS” and avoid any packages that need postinstall builds.
Hook Structure
Section titled “Hook Structure”HOOK.md Format
Section titled “HOOK.md Format”The HOOK.md file is where you store metadata in YAML frontmatter and write your Markdown documentation:
---name: my-hookdescription: "Short description of what this hook does"homepage: https://docs.openclaw.ai/automation/hooks#my-hookmetadata: { "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
Section titled “Metadata Fields”The metadata.openclaw object supports these fields:
emoji: A display emoji for the CLI (like"💾")events: An array of events you want to listen for (like["command:new", "command:reset"])export: The named export to use, which defaults to"default"homepage: Your documentation URLos: The required platforms (like["darwin", "linux"])requires: Optional requirements for the hookbins: Required binaries that must be on your PATH (like["git", "node"])anyBins: At least one of these binaries must be presentenv: Required environment variablesconfig: Required config paths (like["workspace.dir"])always: A boolean to bypass eligibility checksinstall: Installation methods (for bundled hooks, use[{"id":"bundled","kind":"bundled"}])
Handler Implementation
Section titled “Handler Implementation”Your handler.ts file exports a HookHandler function that looks like this:
const myHandler = 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
Section titled “Event Context”Every event you receive includes this data:
{ type: 'command' | 'session' | 'agent' | 'gateway' | 'message', action: string, // e.g., 'new', 'reset', 'stop', 'received', 'sent' sessionKey: string, // Session identifier timestamp: Date, // When the event occurred messages: string[], // Push messages here to send to user context: { // Command events (command:new, command:reset): sessionEntry?: SessionEntry, // current session entry previousSessionEntry?: SessionEntry, // pre-reset entry (preferred for session-memory) commandSource?: string, // e.g., 'whatsapp', 'telegram' senderId?: string, workspaceDir?: string, cfg?: OpenClawConfig, // Command events (command:stop only): sessionId?: string, // Agent bootstrap events (agent:bootstrap): bootstrapFiles?: WorkspaceBootstrapFile[], // Message events (see Message Events section for full details): from?: string, // message:received to?: string, // message:sent content?: string, channelId?: string, success?: boolean, // message:sent }}Event Types
Section titled “Event Types”Command Events
Section titled “Command Events”These trigger when you issue agent commands:
command: A general listener for all command eventscommand:new: Triggered when the/newcommand is issuedcommand:reset: Triggered when the/resetcommand is issuedcommand:stop: Triggered when the/stopcommand is issued
Session Events
Section titled “Session Events”session:compact:before: Fires right before compaction summarizes your historysession:compact:after: Fires after compaction finishes with the summary metadata
Internal hook payloads emit these with type: "session" and actions like "compact:before". You can subscribe using the combined keys like session:compact:before.
Agent Events
Section titled “Agent Events”agent:bootstrap: This runs before workspace bootstrap files are injected. Your hooks can mutatecontext.bootstrapFileshere.
Gateway Events
Section titled “Gateway Events”These trigger when the gateway starts up:
gateway:startup: Fires after channels start and hooks are loaded.
Session Patch Events
Section titled “Session Patch Events”These trigger when session properties are modified:
session:patch: Fires when a session is updated.
Session Event Context
Section titled “Session Event Context”Session events provide detailed context about the session and the specific changes:
{ sessionEntry: SessionEntry, // The complete updated session entry patch: { // The patch object (only changed fields) // Session identity & labeling label?: string | null, // Human-readable session label
// AI model configuration model?: string | null, // Model override (e.g., "claude-opus-4-5") thinkingLevel?: string | null, // Thinking level ("off"|"low"|"med"|"high") verboseLevel?: string | null, // Verbose output level reasoningLevel?: string | null, // Reasoning mode override elevatedLevel?: string | null, // Elevated mode override responseUsage?: "off" | "tokens" | "full" | null, // Usage display mode
// Tool execution settings execHost?: string | null, // Exec host (sandbox|gateway|node) execSecurity?: string | null, // Security mode (deny|allowlist|full) execAsk?: string | null, // Approval mode (off|on-miss|always) execNode?: string | null, // Node ID for host=node
// Subagent coordination spawnedBy?: string | null, // Parent session key (for subagents) spawnDepth?: number | null, // Nesting depth (0 = root)
// Communication policies sendPolicy?: "allow" | "deny" | null, // Message send policy groupActivation?: "mention" | "always" | null, // Group chat activation }, cfg: OpenClawConfig // Current gateway config}Only privileged clients, such as the Control UI, can trigger session:patch events. Standard WebChat clients cannot patch sessions, so the hook won’t fire from those connections.
Example: Session Patch Logger Hook
Section titled “Example: Session Patch Logger Hook”const handler = async (event) => { if (event.type !== "session" || event.action !== "patch") { return; } const { patch } = event.context; console.log(`[session-patch] Session updated: ${event.sessionKey}`); console.log(`[session-patch] Changes:`, patch);};
export default handler;Message Events
Section titled “Message Events”These trigger when messages are received or sent:
message: A general listener for all message eventsmessage:received: Fires when an inbound message arrives. This happens early, before media processing, so content might contain placeholders like<media:audio>.message:transcribed: Fires after a message is fully processed, including audio transcription. Use this if you need the actual text from an audio message.message:preprocessed: Fires for every message after all media and link understanding is done. This gives you access to the enriched body before the agent sees it.message:sent: Fires when an outbound message is successfully sent.
Message Event Context
Section titled “Message Event Context”Message events include specific context depending on the action:
// message:received context{ from: string, // Sender identifier (phone number, user ID, etc.) content: string, // Message content timestamp?: number, // Unix timestamp when received channelId: string, // Channel (e.g., "whatsapp", "telegram", "discord") accountId?: string, // Provider account ID for multi-account setups conversationId?: string, // Chat/conversation ID messageId?: string, // Message ID from the provider metadata?: { // Additional provider-specific data to?: string, provider?: string, surface?: string, threadId?: string | number, senderId?: string, senderName?: string, senderUsername?: string, senderE164?: string, guildId?: string, // Discord guild / server ID channelName?: string, // Channel name (e.g., Discord channel name) }}
// message:sent context{ to: string, // Recipient identifier content: string, // Message content that was sent success: boolean, // Whether the send succeeded error?: string, // Error message if sending failed channelId: string, // Channel (e.g., "whatsapp", "telegram", "discord") accountId?: string, // Provider account ID conversationId?: string, // Chat/conversation ID messageId?: string, // Message ID returned by the provider isGroup?: boolean, // Whether this outbound message belongs to a group/channel context groupId?: string, // Group/channel identifier for correlation with message:received}
// message:transcribed context{ from?: string, // Sender identifier to?: string, // Recipient identifier body?: string, // Raw inbound body before enrichment bodyForAgent?: string, // Enriched body visible to the agent transcript: string, // Audio transcript text timestamp?: number, // Unix timestamp when received channelId: string, // Channel (e.g., "telegram", "whatsapp") conversationId?: string, messageId?: string, senderId?: string, // Sender user ID senderName?: string, // Sender display name senderUsername?: string, provider?: string, // Provider name surface?: string, // Surface name mediaPath?: string, // Path to the media file that was transcribed mediaType?: string, // MIME type of the media}
// message:preprocessed context{ from?: string, // Sender identifier to?: string, // Recipient identifier body?: string, // Raw inbound body bodyForAgent?: string, // Final enriched body after media/link understanding transcript?: string, // Transcript when audio was present timestamp?: number, // Unix timestamp when received channelId: string, // Channel (e.g., "telegram", "whatsapp") conversationId?: string, messageId?: string, senderId?: string, // Sender user ID senderName?: string, // Sender display name senderUsername?: string, provider?: string, // Provider name surface?: string, // Surface name mediaPath?: string, // Path to the media file mediaType?: string, // MIME type of the media isGroup?: boolean, groupId?: string,}Example: Message Logger Hook
Section titled “Example: Message Logger Hook”const isMessageReceivedEvent = (event: { type: string; action: string }) => event.type === "message" && event.action === "received";const isMessageSentEvent = (event: { type: string; action: string }) => event.type === "message" && event.action === "sent";
const handler = async (event) => { if (isMessageReceivedEvent(event as { type: string; action: string })) { console.log(`[message-logger] Received from ${event.context.from}: ${event.context.content}`); } else if (isMessageSentEvent(event as { type: string; action: string })) { console.log(`[message-logger] Sent to ${event.context.to}: ${event.context.content}`); }};
export default handler;Tool Result Hooks (Plugin API)
Section titled “Tool Result Hooks (Plugin API)”These are not standard event listeners. They allow plugins to synchronously adjust tool results before OpenClaw saves them.
tool_result_persist: Use this to transform tool results before they are written to the session transcript. It must be synchronous. Return the updated payload orundefinedto leave it as-is.
Plugin Hook Events
Section titled “Plugin Hook Events”These compaction lifecycle hooks are exposed through the plugin hook runner:
before_compaction: Runs before compaction with token and count metadata.after_compaction: Runs after compaction with summary metadata.
Future Events
Section titled “Future Events”We are planning several new event types:
session:start: When a new session beginssession:end: When a session endsagent:error: When an agent encounters an error
Creating Custom Hooks
Section titled “Creating Custom Hooks”1. Choose Location
Section titled “1. Choose Location”You have two options for where to put your hooks:
- Workspace hooks (
<workspace>/hooks/): These are specific to an agent. You can add new hook names here, but you cannot override bundled or plugin hooks. - Managed hooks (
~/.openclaw/hooks//): These are shared across all your workspaces and can override bundled or plugin hooks.
2. Create Directory Structure
Section titled “2. Create Directory Structure”Set up your directory like this:
mkdir -p ~/.openclaw/hooks/my-hookcd ~/.openclaw/hooks/my-hook3. Create HOOK.md
Section titled “3. Create HOOK.md”Define your hook’s metadata:
---name: my-hookdescription: "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
Section titled “4. Create handler.ts”Write your logic in the handler:
const handler = 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
Section titled “5. Enable and Test”Finally, get your hook running:
# Verify hook is discoveredopenclaw hooks list
# Enable itopenclaw 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 channelConfiguration
Section titled “Configuration”You’ll want to get your configuration right to make the most of hooks. I recommend using the new format because it’s much cleaner and easier to manage.
New Config Format (Recommended)
Section titled “New Config Format (Recommended)”This is the preferred way to set things up. It gives you a clear view of which hooks are active.
{ "hooks": { "internal": { "enabled": true, "entries": { "session-memory": { "enabled": true }, "command-logger": { "enabled": false } } } }}Per-Hook Configuration
Section titled “Per-Hook Configuration”If you need to pass specific environment variables or custom settings to a hook, you can do that directly in the entry.
{ "hooks": { "internal": { "enabled": true, "entries": { "my-hook": { "enabled": true, "env": { "MY_CUSTOM_VAR": "value" } } } } }}Extra Directories
Section titled “Extra Directories”You can also load hooks from other folders. These are treated just like managed hooks, so they follow the same rules for overriding.
{ "hooks": { "internal": { "enabled": true, "load": { "extraDirs": ["/path/to/more/hooks"] } } }}Legacy Config Format (Still Supported)
Section titled “Legacy Config Format (Still Supported)”If you’re still using the old format, it still works for now. This is mainly kept around so things don’t break for older setups.
{ "hooks": { "internal": { "enabled": true, "handlers": [ { "event": "command:new", "module": "./hooks/handlers/my-handler.ts", "export": "default" } ] } }}Note that module has to be a path relative to your workspace. The system will reject absolute paths or anything that tries to go outside the workspace.
Migration: You should use the new discovery-based system for any new hooks you build. Legacy handlers will load after the directory-based ones.
CLI Commands
Section titled “CLI Commands”The CLI is your best friend for managing hooks. You can check status, enable features, or get details without touching your config files.
List Hooks
Section titled “List Hooks”Use these commands to see what’s available and what’s actually running.
# List all hooksopenclaw hooks list
# Show only eligible hooksopenclaw hooks list --eligible
# Verbose output (show missing requirements)openclaw hooks list --verbose
# JSON outputopenclaw hooks list --jsonHook Information
Section titled “Hook Information”If you need to dig into the details of a specific hook, use the info command.
# Show detailed info about a hookopenclaw hooks info session-memory
# JSON outputopenclaw hooks info session-memory --jsonCheck Eligibility
Section titled “Check Eligibility”This is a quick way to see if your environment meets the requirements for your hooks.
# Show eligibility summaryopenclaw hooks check
# JSON outputopenclaw hooks check --jsonEnable/Disable
Section titled “Enable/Disable”You can toggle hooks on and off instantly from the terminal.
# Enable a hookopenclaw hooks enable session-memory
# Disable a hookopenclaw hooks disable command-loggerBundled hook reference
Section titled “Bundled hook reference”OpenClaw comes with several built-in hooks that handle common tasks. Here is what you can use right away.
session-memory
Section titled “session-memory”This hook saves your session context to memory whenever you use /new or /reset. It’s great for keeping a history of your interactions.
Events: command:new, command:reset
Requirements: You need to have workspace.dir configured.
Output: Files are saved to <workspace>/memory/YYYY-MM-DD-slug.md (this defaults to ~/.openclaw/workspace).
What it does:
- It uses the pre-reset session entry to find the right transcript.
- It pulls the last 15 user and assistant messages from the chat.
- It uses an LLM to create a descriptive filename.
- It saves all that metadata into a dated memory file.
Example output:
# Session: 2026-01-16 14:30:00 UTC
- **Session Key**: agent:main:main- **Session ID**: abc123def456- **Source**: telegram
## Conversation Summary
user: Can you help me design the API?assistant: Sure! Let's start with the endpoints...Filename examples:
2026-01-16-vendor-pitch.md2026-01-16-api-design.md2026-01-16-1430.md(this is the fallback if the slug generation fails)
Enable:
openclaw hooks enable session-memorybootstrap-extra-files
Section titled “bootstrap-extra-files”This one is perfect for monorepos. It lets you inject extra files like AGENTS.md or TOOLS.md during the agent:bootstrap process.
Events: agent:bootstrap
Requirements: You need to have workspace.dir configured.
Output: It doesn’t write new files; it just modifies the bootstrap context in memory.
Config:
{ "hooks": { "internal": { "enabled": true, "entries": { "bootstrap-extra-files": { "enabled": true, "paths": ["packages/*/AGENTS.md", "packages/*/TOOLS.md"] } } } }}Config options:
paths(string[]): These are the glob patterns to find your files.patterns(string[]): This is just an alias forpaths.files(string[]): Another alias forpaths.
Notes:
- All paths are relative to your workspace.
- Files must stay inside the workspace for security.
- It only loads recognized names like
AGENTS.md,SOUL.md, orTOOLS.md. - Subagent sessions use a more restricted list of allowed files.
Enable:
openclaw hooks enable bootstrap-extra-filescommand-logger
Section titled “command-logger”If you need an audit trail, this hook logs every command event to a central file.
Events: command
Requirements: None.
Output: Logs go to ~/.openclaw/logs/commands.log.
What it does:
- It grabs details like the action, timestamp, and session key.
- It appends them to a JSONL file.
- It runs quietly in the background without bothering you.
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":"user@example.com","source":"whatsapp"}View logs:
# View recent commandstail -n 20 ~/.openclaw/logs/commands.log
# Pretty-print with jqcat ~/.openclaw/logs/commands.log | jq .
# Filter by actiongrep '"action":"new"' ~/.openclaw/logs/commands.log | jq .Enable:
openclaw hooks enable command-loggerboot-md
Section titled “boot-md”This hook runs a BOOT.md file as soon as the gateway starts up. It’s a great way to automate initial tasks.
Events: gateway:startup
Requirements: You need to have workspace.dir configured.
What it does:
- It reads
BOOT.mdfrom your workspace. - It runs those instructions through the agent runner.
- It sends out any messages you’ve requested via the message tool.
Enable:
openclaw hooks enable boot-mdBest Practices
Section titled “Best Practices”Writing good hooks makes your whole system more reliable. Here are a few tips to keep things running smoothly.
Keep Handlers Fast
Section titled “Keep Handlers Fast”Hooks run while commands are being processed. If a hook is slow, your whole command feels laggy. Keep them lightweight.
// ✓ Good - async work, returns immediatelyconst handler: HookHandler = async (event) => { void processInBackground(event); // Fire and forget};
// ✗ Bad - blocks command processingconst handler: HookHandler = async (event) => { await slowDatabaseQuery(event); await evenSlowerAPICall(event);};Handle Errors Gracefully
Section titled “Handle Errors Gracefully”Don’t let one bad hook crash your system. Always wrap your logic in try/catch blocks.
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
Section titled “Filter Events Early”If a hook doesn’t need to handle a specific event, exit as fast as possible. This saves resources and keeps things snappy.
const handler: HookHandler = async (event) => { // Only handle 'new' commands if (event.type !== "command" || event.action !== "new") { return; }
// Your logic here};Use Specific Event Keys
Section titled “Use Specific Event Keys”When you define your metadata, try to be as specific as possible. This reduces the overhead of the system calling your hook for events it doesn’t care about.
metadata: { "openclaw": { "events": ["command:new"] } } # SpecificAvoid using general keys if you don’t have to:
metadata: { "openclaw": { "events": ["command"] } } # General - more overheadDebugging
Section titled “Debugging”Enable Hook Logging
Section titled “Enable Hook Logging”The gateway logs hook loading when it starts up. You can check the output to see if your hook is registered correctly:
Registered hook: session-memory -> command:newRegistered hook: bootstrap-extra-files -> agent:bootstrapRegistered hook: command-logger -> commandRegistered hook: boot-md -> gateway:startupCheck Discovery
Section titled “Check Discovery”If you want to see every hook the system has found, you can list them all with this command:
openclaw hooks list --verboseCheck Registration
Section titled “Check Registration”When you need to know if your code is actually running, you can log a message inside your handler. This helps you track when the gateway triggers your logic:
const handler: HookHandler = async (event) => { console.log("[my-handler] Triggered:", event.type, event.action); // Your logic};Verify Eligibility
Section titled “Verify Eligibility”If a hook is registered but isn’t running, it might be missing a requirement. You can check why a hook isn’t eligible by running:
openclaw hooks info my-hookLook through the output to find any requirements that are not being met.
Testing
Section titled “Testing”Gateway Logs
Section titled “Gateway Logs”You can monitor the gateway logs to watch hook execution as it happens. Use the command that fits your platform:
# macOS./scripts/clawlog.sh -f
# Other platformstail -f ~/.openclaw/gateway.logTest Hooks Directly
Section titled “Test Hooks Directly”You do not have to run the entire gateway to verify your logic. You can test your handlers by themselves to speed up your workflow. Here is an example of how to do that with Vitest:
import { test } from "vitest";import myHandler from "./hooks/my-hook/handler.js";
test("my handler works", async () => { const event = { type: "command", action: "new", sessionKey: "test-session", timestamp: new Date(), messages: [], context: { foo: "bar" }, };
await myHandler(event);
// Assert side effects});Architecture
Section titled “Architecture”Understanding how the system handles hooks helps when you’re building your own. The architecture relies on a few core files and a specific sequence for finding and running your code.
Core Components
Section titled “Core Components”src/hooks/types.ts: This file holds all the type definitions.src/hooks/workspace.ts: This part handles scanning your directories and loading the hooks.src/hooks/frontmatter.ts: It parses the metadata found in yourHOOK.mdfiles.src/hooks/config.ts: This checks if a hook is eligible to run based on your setup.src/hooks/hooks-status.ts: This component manages status reporting.src/hooks/loader.ts: The dynamic module loader that brings everything in.src/cli/hooks-cli.ts: This contains the logic for the CLI commands you use.src/gateway/server-startup.ts: This ensures hooks are loaded right when the gateway starts.src/auto-reply/reply/commands-core.ts: This is the piece that triggers command events.
Discovery Flow
Section titled “Discovery Flow”When the system starts up, it follows this flow to find your hooks:
Gateway startup ↓Scan directories (bundled → plugin → managed + extra dirs → workspace) ↓Parse HOOK.md files ↓Sort by override precedence (bundled < plugin < managed < workspace) ↓Check eligibility (bins, env, config, os) ↓Load handlers from eligible hooks ↓Register handlers for eventsEvent Flow
Section titled “Event Flow”Once a hook is registered, it moves through this flow when a user interacts with the system:
User sends /new ↓Command validation ↓Create hook event ↓Trigger hook (all registered handlers) ↓Command processing continues ↓Session resetTroubleshooting
Section titled “Troubleshooting”If your hook isn’t working as expected, you can use these steps to figure out what’s wrong.
Hook Not Discovered
Section titled “Hook Not Discovered”- Check your directory structure to make sure everything is in the right place:
ls -la ~/.openclaw/hooks/my-hook/ # Should show: HOOK.md, handler.ts- Verify that your
HOOK.mdformat is correct:
cat ~/.openclaw/hooks/my-hook/HOOK.md # Should have YAML frontmatter with name and metadata- Use the CLI to list all hooks the system has found:
openclaw hooks listHook Not Eligible
Section titled “Hook Not Eligible”If the hook is found but won’t run, check its requirements with this command:
openclaw hooks info my-hookLook for these common issues:
- Missing binaries (check your PATH)
- Missing environment variables
- Incorrect config values
- OS compatibility problems
Hook Not Executing
Section titled “Hook Not Executing”- First, verify that the hook is actually enabled:
openclaw hooks list # Should show ✓ next to enabled hooks-
Restart your gateway process so the system can reload the hooks.
-
Check the gateway logs for any specific error messages:
./scripts/clawlog.sh | grep hookHandler Errors
Section titled “Handler Errors”If you suspect there’s a problem with the code itself, like a TypeScript or import error, you can test the import directly:
# Test import directlynode -e "import('./path/to/handler.ts').then(console.log)"Migration Guide
Section titled “Migration Guide”If you are still using the old way to configure your hooks, you should move to the Discovery system. It makes managing your custom logic much simpler.
From Legacy Config to Discovery
Section titled “From Legacy Config to Discovery”Before, your configuration likely looked like this:
{ "hooks": { "internal": { "enabled": true, "handlers": [ { "event": "command:new", "module": "./hooks/handlers/my-handler.ts" } ] } }}After you migrate, follow these steps:
- 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:
{ "hooks": { "internal": { "enabled": true, "entries": { "my-hook": { "enabled": true } } } } }- Verify and restart your gateway process:
openclaw hooks list # Should show: 🎯 my-hook ✓Benefits of migration:
- Automatic discovery
- CLI management
- Eligibility checking
- Better documentation
- Consistent structure
See Also
Section titled “See Also”Check out these resources for more details:
OpenClaw Expert
Still stuck?
If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.