mirror of https://github.com/openclaw/openclaw.git
1094 lines
28 KiB
Markdown
1094 lines
28 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 `handler.ts` (or `index.ts`).
|
|
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[],
|
|
// 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`.
|
|
|
|
### 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
|
|
|
|
### 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
|
|
|
|
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
|
|
|
|
### Future Events
|
|
|
|
Planned event types:
|
|
|
|
- **`session:start`**: When a new session begins
|
|
- **`session:end`**: When a session ends
|
|
- **`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:
|
|
|
|
```
|
|
Registered hook: session-memory -> command:new
|
|
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)
|