Skip to content

Automate OpenClaw Workflows with Custom Hooks

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 webhooks for 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.

The hooks system gives you the ability to:

  • Save session context to memory when /new is issued.
  • Log all commands for auditing purposes.
  • Trigger custom automations on agent lifecycle events.
  • Extend how OpenClaw behaves without modifying the core code.

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 /new or /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.md when the gateway starts (this requires internal hooks to be enabled).

List your available hooks:

Terminal window
openclaw hooks list

Enable a specific hook:

Terminal window
openclaw hooks enable session-memory

Check the status of your hooks:

Terminal window
openclaw hooks check

Get detailed information about a hook:

Terminal window
openclaw hooks info session-memory

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.

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.

Hooks are automatically discovered from these directories, following this order of increasing override precedence:

  1. Bundled hooks: These are shipped with OpenClaw and located at <openclaw>/dist/hooks/bundled/ for npm installs (or a sibling hooks/bundled/ for compiled binaries).
  2. Plugin hooks: These are hooks bundled inside your installed plugins (see Plugin hooks).
  3. 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 via hooks.internal.load.extraDirs are also treated as managed hooks.
  4. 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 implementation

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:

Terminal window
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.

The HOOK.md file is where you store metadata in YAML frontmatter and write your 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.

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 URL
  • os: The required platforms (like ["darwin", "linux"])
  • requires: Optional requirements for the hook
  • bins: Required binaries that must be on your PATH (like ["git", "node"])
  • anyBins: At least one of these binaries must be present
  • env: Required environment variables
  • config: Required config paths (like ["workspace.dir"])
  • always: A boolean to bypass eligibility checks
  • install: Installation methods (for bundled hooks, use [{"id":"bundled","kind":"bundled"}])

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;

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
}
}

These trigger when you issue agent commands:

  • command: A general listener for all command events
  • command:new: Triggered when the /new command is issued
  • command:reset: Triggered when the /reset command is issued
  • command:stop: Triggered when the /stop command is issued
  • session:compact:before: Fires right before compaction summarizes your history
  • session: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:bootstrap: This runs before workspace bootstrap files are injected. Your hooks can mutate context.bootstrapFiles here.

These trigger when the gateway starts up:

  • gateway:startup: Fires after channels start and hooks are loaded.

These trigger when session properties are modified:

  • session:patch: Fires when a session is updated.

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.

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;

These trigger when messages are received or sent:

  • message: A general listener for all message events
  • message: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 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,
}
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;

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 or undefined to leave it as-is.

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.

We are planning several new event types:

  • session:start: When a new session begins
  • session:end: When a session ends
  • agent:error: When an agent encounters an error

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.

Set up your directory like this:

Terminal window
mkdir -p ~/.openclaw/hooks/my-hook
cd ~/.openclaw/hooks/my-hook

Define your hook’s metadata:

---
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`.

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;

Finally, get your hook running:

Terminal window
# 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

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.

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 }
}
}
}
}

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"
}
}
}
}
}
}

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"]
}
}
}
}

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.

The CLI is your best friend for managing hooks. You can check status, enable features, or get details without touching your config files.

Use these commands to see what’s available and what’s actually running.

Terminal window
# 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

If you need to dig into the details of a specific hook, use the info command.

Terminal window
# Show detailed info about a hook
openclaw hooks info session-memory
# JSON output
openclaw hooks info session-memory --json

This is a quick way to see if your environment meets the requirements for your hooks.

Terminal window
# Show eligibility summary
openclaw hooks check
# JSON output
openclaw hooks check --json

You can toggle hooks on and off instantly from the terminal.

Terminal window
# Enable a hook
openclaw hooks enable session-memory
# Disable a hook
openclaw hooks disable command-logger

OpenClaw comes with several built-in hooks that handle common tasks. Here is what you can use right away.

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:

  1. It uses the pre-reset session entry to find the right transcript.
  2. It pulls the last 15 user and assistant messages from the chat.
  3. It uses an LLM to create a descriptive filename.
  4. 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.md
  • 2026-01-16-api-design.md
  • 2026-01-16-1430.md (this is the fallback if the slug generation fails)

Enable:

Terminal window
openclaw hooks enable session-memory

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 for paths.
  • files (string[]): Another alias for paths.

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, or TOOLS.md.
  • Subagent sessions use a more restricted list of allowed files.

Enable:

Terminal window
openclaw hooks enable bootstrap-extra-files

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:

  1. It grabs details like the action, timestamp, and session key.
  2. It appends them to a JSONL file.
  3. 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:

Terminal window
# 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:

Terminal window
openclaw hooks enable command-logger

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:

  1. It reads BOOT.md from your workspace.
  2. It runs those instructions through the agent runner.
  3. It sends out any messages you’ve requested via the message tool.

Enable:

Terminal window
openclaw hooks enable boot-md

Writing good hooks makes your whole system more reliable. Here are a few tips to keep things running smoothly.

Hooks run while commands are being processed. If a hook is slow, your whole command feels laggy. 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);
};

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
}
};

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
};

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"] } } # Specific

Avoid using general keys if you don’t have to:

metadata: { "openclaw": { "events": ["command"] } } # General - more overhead

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:new
Registered hook: bootstrap-extra-files -> agent:bootstrap
Registered hook: command-logger -> command
Registered hook: boot-md -> gateway:startup

If you want to see every hook the system has found, you can list them all with this command:

Terminal window
openclaw hooks list --verbose

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
};

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:

Terminal window
openclaw hooks info my-hook

Look through the output to find any requirements that are not being met.

You can monitor the gateway logs to watch hook execution as it happens. Use the command that fits your platform:

Terminal window
# macOS
./scripts/clawlog.sh -f
# Other platforms
tail -f ~/.openclaw/gateway.log

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
});

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.

  • 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 your HOOK.md files.
  • 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.

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 events

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 reset

If your hook isn’t working as expected, you can use these steps to figure out what’s wrong.

  1. Check your directory structure to make sure everything is in the right place:
Terminal window
ls -la ~/.openclaw/hooks/my-hook/
# Should show: HOOK.md, handler.ts
  1. Verify that your HOOK.md format is correct:
Terminal window
cat ~/.openclaw/hooks/my-hook/HOOK.md
# Should have YAML frontmatter with name and metadata
  1. Use the CLI to list all hooks the system has found:
Terminal window
openclaw hooks list

If the hook is found but won’t run, check its requirements with this command:

Terminal window
openclaw hooks info my-hook

Look for these common issues:

  • Missing binaries (check your PATH)
  • Missing environment variables
  • Incorrect config values
  • OS compatibility problems
  1. First, verify that the hook is actually enabled:
Terminal window
openclaw hooks list
# Should show ✓ next to enabled hooks
  1. Restart your gateway process so the system can reload the hooks.

  2. Check the gateway logs for any specific error messages:

Terminal window
./scripts/clawlog.sh | grep hook

If you suspect there’s a problem with the code itself, like a TypeScript or import error, you can test the import directly:

Terminal window
# Test import directly
node -e "import('./path/to/handler.ts').then(console.log)"

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.

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:

  1. Create hook directory:
Terminal window
mkdir -p ~/.openclaw/hooks/my-hook
mv ./hooks/handlers/my-handler.ts ~/.openclaw/hooks/my-hook/handler.ts
  1. Create HOOK.md:
---
name: my-hook
description: "My custom hook"
metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } }
---
# My Hook
Does something useful.
  1. Update config:
{
"hooks": {
"internal": {
"enabled": true,
"entries": {
"my-hook": { "enabled": true }
}
}
}
}
  1. Verify and restart your gateway process:
Terminal window
openclaw hooks list
# Should show: 🎯 my-hook ✓

Benefits of migration:

  • Automatic discovery
  • CLI management
  • Eligibility checking
  • Better documentation
  • Consistent structure

Check out these resources for more details:

AI Setup Assistant

OpenClaw

OpenClaw Expert

Still stuck?

If this page didn't answer your case, ask OpenClaw Expert for step-by-step guidance.