mirror of https://github.com/openclaw/openclaw.git
1376 lines
43 KiB
Markdown
1376 lines
43 KiB
Markdown
---
|
|
summary: "Hooks: event-driven automation for commands and lifecycle events"
|
|
read_when:
|
|
- You want event-driven automation for /new, /reset, /stop, and agent lifecycle events
|
|
- You want to build, install, or debug hooks
|
|
title: "Hooks"
|
|
---
|
|
|
|
# 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 inspected with `openclaw hooks`, while hook-pack installation and updates now go through `openclaw plugins`.
|
|
|
|
## 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](/automation/webhook) or use `openclaw webhooks` for Gmail helper commands.
|
|
|
|
Hooks can also be bundled inside plugins; see [Plugin hooks](/plugins/architecture#provider-runtime-hooks). `openclaw hooks list` shows both standalone hooks and plugin-managed hooks.
|
|
|
|
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. Managed and bundled hooks are trusted local code. Workspace hooks are discovered automatically, but OpenClaw keeps them disabled until you explicitly enable them via the CLI or config.
|
|
|
|
## Overview
|
|
|
|
The hooks system allows you to:
|
|
|
|
- Save session context to memory when `/new` is 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` 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 (requires internal hooks enabled)
|
|
|
|
List available hooks:
|
|
|
|
```bash
|
|
openclaw hooks list
|
|
```
|
|
|
|
Enable a hook:
|
|
|
|
```bash
|
|
openclaw hooks enable session-memory
|
|
```
|
|
|
|
Check hook status:
|
|
|
|
```bash
|
|
openclaw hooks check
|
|
```
|
|
|
|
Get detailed information:
|
|
|
|
```bash
|
|
openclaw hooks info session-memory
|
|
```
|
|
|
|
### Onboarding
|
|
|
|
During onboarding (`openclaw onboard`), you'll be prompted to enable recommended hooks. The wizard automatically discovers eligible hooks and presents them for selection.
|
|
|
|
### Trust Boundary
|
|
|
|
Hooks run inside the Gateway process. Treat bundled hooks, managed hooks, and `hooks.internal.load.extraDirs` as trusted local code. Workspace hooks under `<workspace>/hooks/` are repo-local code, so OpenClaw requires an explicit enable step before loading them.
|
|
|
|
## Hook Discovery
|
|
|
|
Hooks are automatically discovered from these directories, in order of increasing override precedence:
|
|
|
|
1. **Bundled hooks**: shipped with OpenClaw; located at `<openclaw>/dist/hooks/bundled/` for npm installs (or a sibling `hooks/bundled/` for compiled binaries)
|
|
2. **Plugin hooks**: hooks bundled inside installed plugins (see [Plugin hooks](/plugins/architecture#provider-runtime-hooks))
|
|
3. **Managed hooks**: `~/.openclaw/hooks/` (user-installed, shared across workspaces; can override bundled and plugin hooks). **Extra hook directories** configured via `hooks.internal.load.extraDirs` are also treated as managed hooks and share the same override precedence.
|
|
4. **Workspace hooks**: `<workspace>/hooks/` (per-agent, disabled by default until explicitly enabled; 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 with the same name.
|
|
|
|
Managed hook directories can be either a **single hook** or a **hook pack** (package directory).
|
|
|
|
Each hook is a directory containing:
|
|
|
|
```
|
|
my-hook/
|
|
├── HOOK.md # Metadata + documentation
|
|
└── handler.ts # Handler implementation
|
|
```
|
|
|
|
## 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:
|
|
|
|
```bash
|
|
openclaw plugins install <path-or-spec>
|
|
```
|
|
|
|
Npm specs are registry-only (package name + optional exact version or dist-tag).
|
|
Git/URL/file specs and semver ranges are rejected.
|
|
|
|
Bare specs and `@latest` stay on the stable track. If npm resolves either of
|
|
those to a prerelease, OpenClaw stops and asks you to opt in explicitly with a
|
|
prerelease tag such as `@beta`/`@rc` or an exact prerelease version.
|
|
|
|
Example `package.json`:
|
|
|
|
```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 a handler file. The loader tries `handler.ts`, `handler.js`, `index.ts`, `index.js` in order.
|
|
Hook packs can ship dependencies; they will be installed under `~/.openclaw/hooks/<id>`.
|
|
Each `openclaw.hooks` entry must stay inside the package directory after symlink
|
|
resolution; entries that escape are rejected.
|
|
|
|
Security note: `openclaw plugins install` installs hook-pack 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:
|
|
|
|
```markdown
|
|
---
|
|
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 URL
|
|
- **`os`**: Required platforms (e.g., `["darwin", "linux"]`)
|
|
- **`requires`**: Optional requirements
|
|
- **`bins`**: Required binaries on PATH (e.g., `["git", "node"]`)
|
|
- **`anyBins`**: At least one of these binaries must be present
|
|
- **`env`**: Required environment variables
|
|
- **`config`**: Required config paths (e.g., `["workspace.dir"]`)
|
|
- **`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:
|
|
|
|
```typescript
|
|
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
|
|
|
|
Each event includes:
|
|
|
|
```typescript
|
|
{
|
|
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[],
|
|
sessionKey?: string, // routing session key
|
|
sessionId?: string, // internal session UUID
|
|
agentId?: string, // resolved agent ID
|
|
// 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
|
|
|
|
### Command Events
|
|
|
|
Triggered when agent commands are issued:
|
|
|
|
- **`command`**: All command events (general listener)
|
|
- **`command:new`**: When `/new` command is issued
|
|
- **`command:reset`**: When `/reset` command is issued
|
|
- **`command:stop`**: When `/stop` command is issued
|
|
|
|
### Session Events
|
|
|
|
- **`session:compact:before`**: Right before compaction summarizes history
|
|
- **`session:compact:after`**: After compaction completes with summary metadata
|
|
|
|
Internal hook payloads emit these as `type: "session"` with `action: "compact:before"` / `action: "compact:after"`; listeners subscribe with the combined keys above.
|
|
Specific handler registration uses the literal key format `${type}:${action}`. For these events, register `session:compact:before` and `session:compact:after`.
|
|
|
|
`session:compact:before` context fields:
|
|
|
|
- `sessionId`: internal session UUID
|
|
- `missingSessionKey`: true when no session key was available
|
|
- `messageCount`: number of messages before compaction
|
|
- `tokenCount`: token count before compaction (may be absent)
|
|
- `messageCountOriginal`: message count from the full untruncated session history
|
|
- `tokenCountOriginal`: token count of the full original history (may be absent)
|
|
|
|
`session:compact:after` context fields (in addition to `sessionId` and `missingSessionKey`):
|
|
|
|
- `messageCount`: message count after compaction
|
|
- `tokenCount`: token count after compaction (may be absent)
|
|
- `compactedCount`: number of messages that were compacted/removed
|
|
- `summaryLength`: character length of the generated compaction summary
|
|
- `tokensBefore`: token count from before compaction (for delta calculation)
|
|
- `tokensAfter`: token count after compaction
|
|
- `firstKeptEntryId`: ID of the first message entry retained after compaction
|
|
|
|
### Agent Events
|
|
|
|
- **`agent:bootstrap`**: Before workspace bootstrap files are injected (hooks may mutate `context.bootstrapFiles`)
|
|
|
|
### Gateway Events
|
|
|
|
Triggered when the gateway starts:
|
|
|
|
- **`gateway:startup`**: After channels start and hooks are loaded
|
|
|
|
### Session Patch Events
|
|
|
|
Triggered when session properties are modified:
|
|
|
|
- **`session:patch`**: When a session is updated
|
|
|
|
#### Session Event Context
|
|
|
|
Session events include rich context about the session and changes:
|
|
|
|
```typescript
|
|
{
|
|
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-sonnet-4-6")
|
|
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" | "on" | null, // Usage display mode ("on" is backwards-compat alias for "full")
|
|
fastMode?: boolean | null, // Fast/turbo mode toggle
|
|
spawnedWorkspaceDir?: string | null, // Workspace dir override for spawned subagents
|
|
subagentRole?: "orchestrator" | "leaf" | null, // Subagent role assignment
|
|
subagentControlScope?: "children" | "none" | null, // Scope of subagent control
|
|
|
|
// 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
|
|
}
|
|
```
|
|
|
|
**Security note:** Only privileged clients (including the Control UI) can trigger `session:patch` events. Standard WebChat clients are blocked from patching sessions, so the hook will not fire from those connections.
|
|
|
|
See `SessionsPatchParamsSchema` in `src/gateway/protocol/schema/sessions.ts` for the complete type definition.
|
|
|
|
#### Example: Session Patch Logger Hook
|
|
|
|
```typescript
|
|
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
|
|
|
|
Triggered when messages are received or sent:
|
|
|
|
- **`message`**: All message events (general listener)
|
|
- **`message:received`**: When an inbound message is received from any channel. Fires early in processing before media understanding. Content may contain raw placeholders like `<media:audio>` for media attachments that haven't been processed yet.
|
|
- **`message:transcribed`**: When a message has been fully processed, including audio transcription and link understanding. At this point, `transcript` contains the full transcript text for audio messages. Use this hook when you need access to transcribed audio content.
|
|
- **`message:preprocessed`**: Fires for every message after all media + link understanding completes, giving hooks access to the fully enriched body (transcripts, image descriptions, link summaries) before the agent sees it.
|
|
- **`message:sent`**: When an outbound message is successfully sent
|
|
|
|
#### Message Event Context
|
|
|
|
Message events include rich context about the message:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```typescript
|
|
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)
|
|
|
|
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 or `undefined` to keep it as-is. See [Agent Loop](/concepts/agent-loop).
|
|
|
|
### Plugin Hook Events
|
|
|
|
#### before_tool_call
|
|
|
|
Runs before each tool call. Plugins can modify parameters, block the call, or request user approval.
|
|
|
|
Return fields:
|
|
|
|
- **`params`**: Override tool parameters (merged with original params)
|
|
- **`block`**: Set to `true` to block the tool call
|
|
- **`blockReason`**: Reason shown to the agent when blocked
|
|
- **`requireApproval`**: Pause execution and wait for user approval via channels
|
|
|
|
The `requireApproval` field triggers native platform approval (Telegram buttons, Discord components, `/approve` command) instead of relying on the agent to cooperate:
|
|
|
|
```typescript
|
|
{
|
|
requireApproval: {
|
|
title: "Sensitive operation",
|
|
description: "This tool call modifies production data",
|
|
severity: "warning", // "info" | "warning" | "critical"
|
|
timeoutMs: 120000, // default: 120s
|
|
timeoutBehavior: "deny", // "allow" | "deny" (default)
|
|
onResolution: async (decision) => {
|
|
// Called after the user resolves: "allow-once", "allow-always", "deny", "timeout", or "cancelled"
|
|
},
|
|
}
|
|
}
|
|
```
|
|
|
|
The `onResolution` callback is invoked with the final decision string after the approval resolves, times out, or is cancelled. It runs in-process within the plugin (not sent to the gateway). Use it to persist decisions, update caches, or perform cleanup.
|
|
|
|
The `pluginId` field is stamped automatically by the hook runner from the plugin registration. When multiple plugins return `requireApproval`, the first one (highest priority) wins.
|
|
|
|
`block` takes precedence over `requireApproval`: if the merged hook result has both `block: true` and a `requireApproval` field, the tool call is blocked immediately without triggering the approval flow. This ensures a higher-priority plugin's block cannot be overridden by a lower-priority plugin's approval request.
|
|
|
|
If the gateway is unavailable or does not support plugin approvals, the tool call falls back to a soft block using the `description` as the block reason.
|
|
|
|
#### before_install
|
|
|
|
Runs after the built-in install security scan and before installation continues. OpenClaw fires this hook for interactive skill installs as well as plugin bundle, package, and single-file installs.
|
|
|
|
Default behavior differs by target type:
|
|
|
|
- Plugin installs fail closed on built-in scan `critical` findings and scan errors unless the operator explicitly uses `openclaw plugins install --dangerously-force-unsafe-install`.
|
|
- Skill installs still surface built-in scan findings and scan errors as warnings and continue by default.
|
|
|
|
Return fields:
|
|
|
|
- **`findings`**: Additional scan findings to surface as warnings
|
|
- **`block`**: Set to `true` to block the install
|
|
- **`blockReason`**: Human-readable reason shown when blocked
|
|
|
|
Event fields:
|
|
|
|
- **`targetType`**: Install target category (`skill` or `plugin`)
|
|
- **`targetName`**: Human-readable skill name or plugin id for the install target
|
|
- **`sourcePath`**: Absolute path to the install target content being scanned
|
|
- **`sourcePathKind`**: Whether the scanned content is a `file` or `directory`
|
|
- **`origin`**: Normalized install origin when available (for example `openclaw-bundled`, `openclaw-workspace`, `plugin-bundle`, `plugin-package`, or `plugin-file`)
|
|
- **`request`**: Provenance for the install request, including `kind`, `mode`, and optional `requestedSpecifier`
|
|
- **`builtinScan`**: Structured result of the built-in scanner, including `status`, summary counts, findings, and optional `error`
|
|
- **`skill`**: Skill install metadata when `targetType` is `skill`, including `installId` and the selected `installSpec`
|
|
- **`plugin`**: Plugin install metadata when `targetType` is `plugin`, including the canonical `pluginId`, normalized `contentType`, optional `packageName` / `manifestId` / `version`, and `extensions`
|
|
|
|
Example event (plugin package install):
|
|
|
|
```json
|
|
{
|
|
"targetType": "plugin",
|
|
"targetName": "acme-audit",
|
|
"sourcePath": "/var/folders/.../openclaw-plugin-acme-audit/package",
|
|
"sourcePathKind": "directory",
|
|
"origin": "plugin-package",
|
|
"request": {
|
|
"kind": "plugin-npm",
|
|
"mode": "install",
|
|
"requestedSpecifier": "@acme/openclaw-plugin-audit@1.4.2"
|
|
},
|
|
"builtinScan": {
|
|
"status": "ok",
|
|
"scannedFiles": 12,
|
|
"critical": 0,
|
|
"warn": 1,
|
|
"info": 0,
|
|
"findings": [
|
|
{
|
|
"severity": "warn",
|
|
"ruleId": "network_fetch",
|
|
"file": "dist/index.js",
|
|
"line": 88,
|
|
"message": "Dynamic network fetch detected during install review."
|
|
}
|
|
]
|
|
},
|
|
"plugin": {
|
|
"pluginId": "acme-audit",
|
|
"contentType": "package",
|
|
"packageName": "@acme/openclaw-plugin-audit",
|
|
"manifestId": "acme-audit",
|
|
"version": "1.4.2",
|
|
"extensions": ["./dist/index.js"]
|
|
}
|
|
}
|
|
```
|
|
|
|
Skill installs use the same event shape with `targetType: "skill"` and a `skill` object instead of `plugin`.
|
|
|
|
Decision semantics:
|
|
|
|
- `before_install`: `{ block: true }` is terminal and stops lower-priority handlers.
|
|
- `before_install`: `{ block: false }` is treated as no decision.
|
|
|
|
Use this hook for external security scanners, policy engines, or enterprise approval gates that need to audit install sources before they are installed.
|
|
|
|
#### Compaction lifecycle
|
|
|
|
Compaction lifecycle hooks exposed through the plugin hook runner:
|
|
|
|
- **`before_compaction`**: Runs before compaction with count/token metadata
|
|
- **`after_compaction`**: Runs after compaction with compaction summary metadata
|
|
|
|
### Complete Plugin Hook Reference
|
|
|
|
All 27 hooks registered via the Plugin SDK. Hooks marked **sequential** run in priority order and can modify results; **parallel** hooks are fire-and-forget.
|
|
|
|
#### Model and prompt hooks
|
|
|
|
| Hook | When | Execution | Returns |
|
|
| ---------------------- | -------------------------------------------- | ---------- | ---------------------------------------------------------- |
|
|
| `before_model_resolve` | Before model/provider lookup | Sequential | `{ modelOverride?, providerOverride? }` |
|
|
| `before_prompt_build` | After model resolved, session messages ready | Sequential | `{ systemPrompt?, prependContext?, appendSystemContext? }` |
|
|
| `before_agent_start` | Legacy combined hook (prefer the two above) | Sequential | Union of both result shapes |
|
|
| `llm_input` | Immediately before the LLM API call | Parallel | `void` |
|
|
| `llm_output` | Immediately after LLM response received | Parallel | `void` |
|
|
|
|
#### Agent lifecycle hooks
|
|
|
|
| Hook | When | Execution | Returns |
|
|
| ------------------- | ---------------------------------------------- | --------- | ------- |
|
|
| `agent_end` | After agent run completes (success or failure) | Parallel | `void` |
|
|
| `before_reset` | When `/new` or `/reset` clears a session | Parallel | `void` |
|
|
| `before_compaction` | Before compaction summarizes history | Parallel | `void` |
|
|
| `after_compaction` | After compaction completes | Parallel | `void` |
|
|
|
|
#### Session lifecycle hooks
|
|
|
|
| Hook | When | Execution | Returns |
|
|
| --------------- | ------------------------- | --------- | ------- |
|
|
| `session_start` | When a new session begins | Parallel | `void` |
|
|
| `session_end` | When a session ends | Parallel | `void` |
|
|
|
|
#### Message flow hooks
|
|
|
|
| Hook | When | Execution | Returns |
|
|
| ---------------------- | ------------------------------------------------- | -------------------- | ----------------------------- |
|
|
| `inbound_claim` | Before command/agent dispatch; first-claim wins | Sequential | `{ handled: boolean }` |
|
|
| `message_received` | After an inbound message is received | Parallel | `void` |
|
|
| `before_dispatch` | After commands parsed, before model dispatch | Sequential | `{ handled: boolean, text? }` |
|
|
| `message_sending` | Before an outbound message is delivered | Sequential | `{ content?, cancel? }` |
|
|
| `message_sent` | After an outbound message is delivered | Parallel | `void` |
|
|
| `before_message_write` | Before a message is written to session transcript | **Sync**, sequential | `{ block?, message? }` |
|
|
|
|
#### Tool execution hooks
|
|
|
|
| Hook | When | Execution | Returns |
|
|
| --------------------- | --------------------------------------------- | -------------------- | ----------------------------------------------------- |
|
|
| `before_tool_call` | Before each tool call | Sequential | `{ params?, block?, blockReason?, requireApproval? }` |
|
|
| `after_tool_call` | After a tool call completes | Parallel | `void` |
|
|
| `tool_result_persist` | Before a tool result is written to transcript | **Sync**, sequential | `{ message? }` |
|
|
|
|
#### Subagent hooks
|
|
|
|
| Hook | When | Execution | Returns |
|
|
| -------------------------- | ------------------------------------------ | ---------- | --------------------------------- |
|
|
| `subagent_spawning` | Before a subagent session is created | Sequential | `{ status, threadBindingReady? }` |
|
|
| `subagent_delivery_target` | After spawning, to resolve delivery target | Sequential | `{ origin? }` |
|
|
| `subagent_spawned` | After a subagent is fully spawned | Parallel | `void` |
|
|
| `subagent_ended` | When a subagent session terminates | Parallel | `void` |
|
|
|
|
#### Gateway hooks
|
|
|
|
| Hook | When | Execution | Returns |
|
|
| --------------- | ------------------------------------------ | --------- | ------- |
|
|
| `gateway_start` | After the gateway process is fully started | Parallel | `void` |
|
|
| `gateway_stop` | When the gateway is shutting down | Parallel | `void` |
|
|
|
|
#### Install hooks
|
|
|
|
| Hook | When | Execution | Returns |
|
|
| ---------------- | ----------------------------------------------------- | ---------- | ------------------------------------- |
|
|
| `before_install` | After built-in security scan, before install proceeds | Sequential | `{ findings?, block?, blockReason? }` |
|
|
|
|
<Note>
|
|
Two hooks (`tool_result_persist` and `before_message_write`) are **synchronous only** — they must not return a Promise. Returning a Promise from these hooks is caught at runtime and the result is discarded with a warning.
|
|
</Note>
|
|
|
|
For full handler signatures and context types, see [Plugin Architecture](/plugins/architecture).
|
|
|
|
### Future Events
|
|
|
|
The following event types are planned for the internal hook event stream.
|
|
Note that `session_start` and `session_end` already exist as [Plugin Hook API](/plugins/architecture#provider-runtime-hooks) hooks
|
|
but are not yet available as internal hook event keys in `HOOK.md` metadata:
|
|
|
|
- **`session:start`**: When a new session begins (planned for internal hook stream; available as plugin hook `session_start`)
|
|
- **`session:end`**: When a session ends (planned for internal hook stream; available as plugin hook `session_end`)
|
|
- **`agent:error`**: When an agent encounters an error
|
|
|
|
## Creating Custom Hooks
|
|
|
|
### 1. Choose Location
|
|
|
|
- **Workspace hooks** (`<workspace>/hooks/`): Per-agent; can add new hook names but cannot override bundled, managed, or plugin hooks with the same name
|
|
- **Managed hooks** (`~/.openclaw/hooks/`): Shared across workspaces; can override bundled and plugin hooks
|
|
|
|
### 2. Create Directory Structure
|
|
|
|
```bash
|
|
mkdir -p ~/.openclaw/hooks/my-hook
|
|
cd ~/.openclaw/hooks/my-hook
|
|
```
|
|
|
|
### 3. Create HOOK.md
|
|
|
|
```markdown
|
|
---
|
|
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
|
|
|
|
```typescript
|
|
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
|
|
|
|
```bash
|
|
# 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)
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"internal": {
|
|
"enabled": true,
|
|
"entries": {
|
|
"session-memory": { "enabled": true },
|
|
"command-logger": { "enabled": false }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Per-Hook Configuration
|
|
|
|
Hooks can have custom configuration:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"internal": {
|
|
"enabled": true,
|
|
"entries": {
|
|
"my-hook": {
|
|
"enabled": true,
|
|
"env": {
|
|
"MY_CUSTOM_VAR": "value"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Extra Directories
|
|
|
|
Load hooks from additional directories (treated as managed hooks, same override precedence):
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"internal": {
|
|
"enabled": true,
|
|
"load": {
|
|
"extraDirs": ["/path/to/more/hooks"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Legacy Config Format (Still Supported)
|
|
|
|
The old config format still works for backwards compatibility:
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# Show detailed info about a hook
|
|
openclaw hooks info session-memory
|
|
|
|
# JSON output
|
|
openclaw hooks info session-memory --json
|
|
```
|
|
|
|
### Check Eligibility
|
|
|
|
```bash
|
|
# Show eligibility summary
|
|
openclaw hooks check
|
|
|
|
# JSON output
|
|
openclaw hooks check --json
|
|
```
|
|
|
|
### Enable/Disable
|
|
|
|
```bash
|
|
# 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` or `/reset`.
|
|
|
|
**Events**: `command:new`, `command:reset`
|
|
|
|
**Requirements**: `workspace.dir` must be configured
|
|
|
|
**Output**: `<workspace>/memory/YYYY-MM-DD-slug.md` (defaults to `~/.openclaw/workspace`)
|
|
|
|
**What it does**:
|
|
|
|
1. Uses the pre-reset session entry to locate the correct transcript
|
|
2. Extracts the last 15 user/assistant messages from the conversation (configurable)
|
|
3. Uses LLM to generate a descriptive filename slug
|
|
4. Saves session metadata to a dated memory file
|
|
|
|
**Example output**:
|
|
|
|
```markdown
|
|
# 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` (fallback timestamp if slug generation fails)
|
|
|
|
**Enable**:
|
|
|
|
```bash
|
|
openclaw hooks enable session-memory
|
|
```
|
|
|
|
### 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**:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"internal": {
|
|
"enabled": true,
|
|
"entries": {
|
|
"bootstrap-extra-files": {
|
|
"enabled": true,
|
|
"paths": ["packages/*/AGENTS.md", "packages/*/TOOLS.md"]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Config options**:
|
|
|
|
- `paths` (string[]): glob/path patterns to resolve from the workspace.
|
|
- `patterns` (string[]): alias of `paths`.
|
|
- `files` (string[]): alias of `paths`.
|
|
|
|
**Notes**:
|
|
|
|
- Paths are resolved relative to workspace.
|
|
- Files must stay inside workspace (realpath-checked).
|
|
- Only recognized bootstrap basenames are loaded (`AGENTS.md`, `SOUL.md`, `TOOLS.md`, `IDENTITY.md`, `USER.md`, `HEARTBEAT.md`, `BOOTSTRAP.md`, `MEMORY.md`, `memory.md`).
|
|
- For subagent/cron sessions a narrower allowlist applies (`AGENTS.md`, `TOOLS.md`, `SOUL.md`, `IDENTITY.md`, `USER.md`).
|
|
|
|
**Enable**:
|
|
|
|
```bash
|
|
openclaw hooks enable bootstrap-extra-files
|
|
```
|
|
|
|
### command-logger
|
|
|
|
Logs all command events to a centralized audit file.
|
|
|
|
**Events**: `command`
|
|
|
|
**Requirements**: None
|
|
|
|
**Output**: `~/.openclaw/logs/commands.log`
|
|
|
|
**What it does**:
|
|
|
|
1. Captures event details (command action, timestamp, session key, sender ID, source)
|
|
2. Appends to log file in JSONL format
|
|
3. Runs silently in the background
|
|
|
|
**Example log entries**:
|
|
|
|
```jsonl
|
|
{"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**:
|
|
|
|
```bash
|
|
# 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**:
|
|
|
|
```bash
|
|
openclaw hooks enable command-logger
|
|
```
|
|
|
|
### 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**:
|
|
|
|
1. Reads `BOOT.md` from your workspace
|
|
2. Runs the instructions via the agent runner
|
|
3. Sends any requested outbound messages via the message tool
|
|
|
|
**Enable**:
|
|
|
|
```bash
|
|
openclaw hooks enable boot-md
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### Keep Handlers Fast
|
|
|
|
Hooks run during command processing. Keep them lightweight:
|
|
|
|
```typescript
|
|
// ✓ 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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```yaml
|
|
metadata: { "openclaw": { "events": ["command:new"] } } # Specific
|
|
```
|
|
|
|
Rather than:
|
|
|
|
```yaml
|
|
metadata: { "openclaw": { "events": ["command"] } } # General - more overhead
|
|
```
|
|
|
|
## Debugging
|
|
|
|
### Enable Hook Logging
|
|
|
|
The gateway logs hook loading at startup:
|
|
|
|
```text
|
|
Registered hook: session-memory -> command:new, command:reset
|
|
Registered hook: bootstrap-extra-files -> agent:bootstrap
|
|
Registered hook: command-logger -> command
|
|
Registered hook: boot-md -> gateway:startup
|
|
```
|
|
|
|
### Check Discovery
|
|
|
|
List all discovered hooks:
|
|
|
|
```bash
|
|
openclaw hooks list --verbose
|
|
```
|
|
|
|
### Check Registration
|
|
|
|
In your handler, log when it's called:
|
|
|
|
```typescript
|
|
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:
|
|
|
|
```bash
|
|
openclaw hooks info my-hook
|
|
```
|
|
|
|
Look for missing requirements in the output.
|
|
|
|
## Testing
|
|
|
|
### Gateway Logs
|
|
|
|
Monitor gateway logs to see hook execution:
|
|
|
|
```bash
|
|
# macOS
|
|
./scripts/clawlog.sh -f
|
|
|
|
# Other platforms
|
|
tail -f ~/.openclaw/gateway.log
|
|
```
|
|
|
|
### Test Hooks Directly
|
|
|
|
Test your handlers in isolation:
|
|
|
|
```typescript
|
|
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
|
|
|
|
### Core Components
|
|
|
|
- **`src/hooks/types.ts`**: Type definitions
|
|
- **`src/hooks/workspace.ts`**: Directory scanning and loading
|
|
- **`src/hooks/frontmatter.ts`**: HOOK.md metadata parsing
|
|
- **`src/hooks/config.ts`**: Eligibility checking
|
|
- **`src/hooks/hooks-status.ts`**: Status reporting
|
|
- **`src/hooks/loader.ts`**: Dynamic module loader
|
|
- **`src/cli/hooks-cli.ts`**: CLI commands
|
|
- **`src/gateway/server-startup.ts`**: Loads hooks at gateway start
|
|
- **`src/auto-reply/reply/commands-core.ts`**: Triggers command events
|
|
|
|
### Discovery Flow
|
|
|
|
```
|
|
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
|
|
```
|
|
|
|
### Event Flow
|
|
|
|
```
|
|
User sends /new
|
|
↓
|
|
Command validation
|
|
↓
|
|
Create hook event
|
|
↓
|
|
Trigger hook (all registered handlers)
|
|
↓
|
|
Command processing continues
|
|
↓
|
|
Session reset
|
|
```
|
|
|
|
## Troubleshooting
|
|
|
|
### Hook Not Discovered
|
|
|
|
1. Check directory structure:
|
|
|
|
```bash
|
|
ls -la ~/.openclaw/hooks/my-hook/
|
|
# Should show: HOOK.md, handler.ts
|
|
```
|
|
|
|
2. Verify HOOK.md format:
|
|
|
|
```bash
|
|
cat ~/.openclaw/hooks/my-hook/HOOK.md
|
|
# Should have YAML frontmatter with name and metadata
|
|
```
|
|
|
|
3. List all discovered hooks:
|
|
|
|
```bash
|
|
openclaw hooks list
|
|
```
|
|
|
|
### Hook Not Eligible
|
|
|
|
Check requirements:
|
|
|
|
```bash
|
|
openclaw hooks info my-hook
|
|
```
|
|
|
|
Look for missing:
|
|
|
|
- Binaries (check PATH)
|
|
- Environment variables
|
|
- Config values
|
|
- OS compatibility
|
|
|
|
### Hook Not Executing
|
|
|
|
1. Verify hook is enabled:
|
|
|
|
```bash
|
|
openclaw hooks list
|
|
# Should show ✓ next to enabled hooks
|
|
```
|
|
|
|
2. Restart your gateway process so hooks reload.
|
|
|
|
3. Check gateway logs for errors:
|
|
|
|
```bash
|
|
./scripts/clawlog.sh | grep hook
|
|
```
|
|
|
|
### Handler Errors
|
|
|
|
Check for TypeScript/import errors:
|
|
|
|
```bash
|
|
# Test import directly
|
|
node -e "import('./path/to/handler.ts').then(console.log)"
|
|
```
|
|
|
|
## Migration Guide
|
|
|
|
### From Legacy Config to Discovery
|
|
|
|
**Before**:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"internal": {
|
|
"enabled": true,
|
|
"handlers": [
|
|
{
|
|
"event": "command:new",
|
|
"module": "./hooks/handlers/my-handler.ts"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
**After**:
|
|
|
|
1. Create hook directory:
|
|
|
|
```bash
|
|
mkdir -p ~/.openclaw/hooks/my-hook
|
|
mv ./hooks/handlers/my-handler.ts ~/.openclaw/hooks/my-hook/handler.ts
|
|
```
|
|
|
|
2. Create HOOK.md:
|
|
|
|
```markdown
|
|
---
|
|
name: my-hook
|
|
description: "My custom hook"
|
|
metadata: { "openclaw": { "emoji": "🎯", "events": ["command:new"] } }
|
|
---
|
|
|
|
# My Hook
|
|
|
|
Does something useful.
|
|
```
|
|
|
|
3. Update config:
|
|
|
|
```json
|
|
{
|
|
"hooks": {
|
|
"internal": {
|
|
"enabled": true,
|
|
"entries": {
|
|
"my-hook": { "enabled": true }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
4. Verify and restart your gateway process:
|
|
|
|
```bash
|
|
openclaw hooks list
|
|
# Should show: 🎯 my-hook ✓
|
|
```
|
|
|
|
**Benefits of migration**:
|
|
|
|
- Automatic discovery
|
|
- CLI management
|
|
- Eligibility checking
|
|
- Better documentation
|
|
- Consistent structure
|
|
|
|
## See Also
|
|
|
|
- [CLI Reference: hooks](/cli/hooks)
|
|
- [Bundled Hooks README](https://github.com/openclaw/openclaw/tree/main/src/hooks/bundled)
|
|
- [Webhook Hooks](/automation/webhook)
|
|
- [Configuration](/gateway/configuration-reference#hooks)
|