Skip to main content
Hooks are small scripts that run inside the Gateway when agent events fire: commands like /new, /reset, /stop, session compaction, gateway lifecycle, and message flow. They are discovered from directories and managed with openclaw hooks. The Gateway loads internal hooks only after you enable hooks or configure at least one hook entry, hook pack, legacy handler, or extra hook directory. There are two kinds of hooks in OpenClaw:
  • Internal hooks (this page): run inside the Gateway when agent events fire.
  • Webhooks: external HTTP endpoints that let other systems trigger work in OpenClaw. See Webhooks.
Hooks can also be bundled inside plugins. openclaw hooks list shows both standalone hooks and plugin-managed hooks (displayed as plugin:<id>).

Choose the right surface

OpenClaw has several extension surfaces that look similar but solve different problems:
If you want to…Use…Why
Save a snapshot on /new, log /reset, call an external API after message:sent, or add coarse operator automationInternal hooks (HOOK.md, this page)File-based hooks are meant for operator-managed side effects and command/lifecycle automation
Rewrite prompts, block tools, cancel outbound messages, or add ordered middleware/policyTyped plugin hooks via api.on(...)Typed hooks have explicit contracts, priorities, merge rules, and block/cancel semantics
Add telemetry-only export or observabilityDiagnostic eventsObservability is a separate event bus, not a policy hook surface
Use internal hooks when you want automation that behaves like a small installed integration. Use typed plugin hooks when you need runtime lifecycle control.

Quick start

# List available hooks
openclaw hooks list

# Enable a hook
openclaw hooks enable session-memory

# Check hook status
openclaw hooks check

# Get detailed information
openclaw hooks info session-memory

Event types

Hooks subscribe to a specific key from this table, or to a bare family name (command, session, agent, gateway, message) to receive every action in that family. OpenClaw core emits nothing else, so any other name is almost always a typo that leaves the hook silently dead (only a plugin emitting a custom event could fire it). The hook loader logs a warning for such names (for example command:nwe), and openclaw hooks info <name> flags them, so a hook that never runs is diagnosable.
EventWhen it fires
command:new/new command issued
command:reset/reset command issued
command:stop/stop command issued
commandAny command event (general listener)
session:compact:beforeBefore compaction summarizes history
session:compact:afterAfter compaction completes
session:patchWhen session properties are modified
agent:bootstrapBefore workspace bootstrap files are injected
gateway:startupAfter channels start and hooks are loaded
gateway:shutdownWhen gateway shutdown begins
gateway:pre-restartBefore an expected gateway restart
message:receivedInbound message from any channel
message:transcribedAfter audio transcription completes
message:preprocessedAfter media and link preprocessing completes or is skipped
message:sentOutbound send attempted (context.success has the result)

Writing hooks

Hook structure

Each hook is a directory containing two files:
my-hook/
├── HOOK.md          # Metadata + documentation
└── handler.ts       # Handler implementation
The handler file can be handler.ts, handler.js, index.ts, or index.js.

HOOK.md format

---
name: my-hook
description: "Short description of what this hook does"
metadata:
  { "openclaw": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } }
---

# My Hook

Detailed documentation goes here.
Metadata fields (metadata.openclaw):
FieldDescription
emojiDisplay emoji for CLI
eventsArray of events to listen for
exportNamed export to use (defaults to "default")
osRequired platforms (e.g., ["darwin", "linux"])
requiresRequired bins, anyBins, env, or config paths
alwaysBypass eligibility checks (boolean)
hookKeyConfig key override (defaults to the hook name)
homepageDocs URL shown by openclaw hooks info
installInstallation methods

Handler implementation

const handler = async (event) => {
  if (event.type !== "command" || event.action !== "new") {
    return;
  }

  console.log(`[my-hook] New command triggered`);
  // Your logic here

  // Optionally send a reply on replyable surfaces
  event.messages.push("Hook executed!");
};

export default handler;
Each event includes: type, action, sessionKey, timestamp, messages, and context (event-specific data). Typed plugin hook contexts for agent and tool hooks can also include trace, a read-only W3C-compatible diagnostic trace context that plugins may pass into structured logs for OTEL correlation. Strings pushed to event.messages are delivered back to the chat only for command:new and command:reset (routed as a reply to the originating conversation) and for session:compact:before / session:compact:after (sent as compaction status notices). All other events, including command:stop, message:*, agent:bootstrap, session:patch, and gateway:*, ignore pushed messages.

Event context highlights

Command events (command:new, command:reset): context.sessionEntry, context.previousSessionEntry, context.commandSource, context.senderId, context.workspaceDir, context.cfg. Command events (command:stop): context.sessionEntry, context.sessionId, context.commandSource, context.senderId. Message events (message:received): context.from, context.content, context.channelId, context.metadata (provider-specific data including senderId, senderName, guildId). context.content prefers a nonblank command body for command-like messages, then falls back to the raw inbound body and generic body; it does not include agent-only enrichment such as thread history or link summaries. Message events (message:sent): context.to, context.content, context.success, context.channelId, plus context.error when sending failed. Message events (message:transcribed): context.transcript, context.from, context.channelId, context.mediaPath. Message events (message:preprocessed): context.bodyForAgent (final enriched body), context.from, context.channelId. Bootstrap events (agent:bootstrap): context.bootstrapFiles (mutable array), context.agentId. Session patch events (session:patch): context.sessionEntry, context.patch (only changed fields), context.cfg. Only privileged clients can trigger patch events; the context is a clone, so handlers cannot mutate the live session entry. Compaction events: session:compact:before includes messageCount, tokenCount. session:compact:after adds compactedCount, summaryLength, tokensBefore, tokensAfter. command:stop observes the user issuing /stop; it is cancellation/command lifecycle, not an agent-finalization gate. Plugins that need to inspect a natural final answer and ask the agent for one more pass should use the typed plugin hook before_agent_finalize instead. See Plugin hooks. Gateway lifecycle events: gateway:shutdown includes reason and restartExpectedMs and fires when gateway shutdown begins. gateway:pre-restart includes the same context but only fires when shutdown is part of an expected restart and a finite restartExpectedMs value is supplied. During shutdown, each lifecycle hook wait is best-effort and bounded so shutdown continues if a handler stalls. The default wait budget is 5 seconds for gateway:shutdown and 10 seconds for gateway:pre-restart. Use gateway:pre-restart for short restart notices while channels are still available:
import { execFile } from "node:child_process";
import { promisify } from "node:util";

const execFileAsync = promisify(execFile);

export default async function handler(event) {
  if (event.type !== "gateway" || event.action !== "pre-restart") {
    return;
  }

  const restartInSeconds = Math.ceil(event.context.restartExpectedMs / 1000);
  await execFileAsync("openclaw", [
    "system",
    "event",
    "--mode",
    "now",
    "--text",
    `Gateway restarting in ~${restartInSeconds}s (${event.context.reason}). Checkpoint now.`,
  ]);
}
Between the gateway:shutdown (or gateway:pre-restart) event and the rest of the shutdown sequence, the gateway also fires a typed session_end plugin hook for every session that was still active when the process stopped. The event’s reason is shutdown for a plain SIGTERM/SIGINT stop and restart when the close was scheduled as part of an expected restart. This drain is bounded so a slow session_end handler cannot block process exit, and sessions that have already been finalized through replace / reset / delete / compaction are skipped to avoid double-firing.

Hook discovery

Hooks are discovered from four sources:
  1. Bundled hooks: shipped with OpenClaw
  2. Plugin hooks: bundled inside installed plugins; can override bundled hooks with the same name
  3. Managed hooks: ~/.openclaw/hooks/ (user-installed, shared across workspaces); can override bundled and plugin hooks. Extra directories from hooks.internal.load.extraDirs share this precedence.
  4. Workspace hooks: <workspace>/hooks/ (per-agent, disabled by default until explicitly enabled)
Workspace hooks can add new hook names but cannot override bundled, managed, or plugin-provided hooks with the same name. The Gateway skips internal hook discovery on startup until internal hooks are configured. Enable a bundled or managed hook with openclaw hooks enable <name>, install a hook pack, or set hooks.internal.enabled=true to opt in. When you enable one named hook, the Gateway loads only that hook’s handler; hooks.internal.enabled=true, extra hook directories, and legacy handlers opt into broad discovery.

Hook packs

Hook packs are npm packages that export hooks via openclaw.hooks in package.json. Install with:
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. The older openclaw hooks install and openclaw hooks update commands are deprecated aliases for openclaw plugins install / openclaw plugins update.

Bundled hooks

HookEventsWhat it does
session-memorycommand:new, command:resetSaves session context to <workspace>/memory/
bootstrap-extra-filesagent:bootstrapInjects additional bootstrap files from glob patterns
command-loggercommandLogs all commands to ~/.openclaw/logs/commands.log
compaction-notifiersession:compact:before, session:compact:afterSends visible chat notices when session compaction starts/ends
boot-mdgateway:startupRuns BOOT.md when the gateway starts
Enable any bundled hook:
openclaw hooks enable <hook-name>

session-memory details

Extracts the last user/assistant messages (default 15, configurable with hooks.internal.entries.session-memory.messages) and saves them to <workspace>/memory/YYYY-MM-DD-HHMM.md using the host local date. Memory capture runs in the background so /new and /reset acknowledgements are not delayed by transcript reads or optional slug generation. Set hooks.internal.entries.session-memory.llmSlug: true to generate descriptive filename slugs with the configured model (falls back to timestamp slugs when unavailable). Requires workspace.dir to be configured.

bootstrap-extra-files config

{
  "hooks": {
    "internal": {
      "entries": {
        "bootstrap-extra-files": {
          "enabled": true,
          "paths": ["packages/*/AGENTS.md", "packages/*/TOOLS.md"]
        }
      }
    }
  }
}
patterns and files are accepted as aliases of paths. Paths resolve relative to the workspace and must stay inside it. Only recognized bootstrap basenames are loaded (AGENTS.md, SOUL.md, TOOLS.md, IDENTITY.md, USER.md, HEARTBEAT.md, BOOTSTRAP.md, MEMORY.md).

command-logger details

Logs every slash command as a JSON line (timestamp, action, session key, sender ID, source) to ~/.openclaw/logs/commands.log.

compaction-notifier details

Sends short status messages into the current conversation when OpenClaw starts and finishes compacting the session transcript. This makes long turns less confusing on chat surfaces because the user can see that the assistant is summarizing context and will continue after compaction.

boot-md details

Runs BOOT.md at gateway startup for each configured agent scope, if the file exists in that agent’s resolved workspace.

Plugin hooks

Plugins can register typed hooks through the Plugin SDK for deeper integration: intercepting tool calls, modifying prompts, controlling message flow, and more. Use plugin hooks when you need before_tool_call, before_agent_reply, before_install, or other in-process lifecycle hooks. Plugin-managed internal hooks are different: they participate in this page’s coarse command/lifecycle event system and show up in openclaw hooks list as plugin:<id>. Use those for side effects and compatibility with hook packs, not for ordered middleware or policy gates. For the complete plugin hook reference, see Plugin hooks.

Configuration

{
  "hooks": {
    "internal": {
      "enabled": true,
      "entries": {
        "session-memory": { "enabled": true },
        "command-logger": { "enabled": false }
      }
    }
  }
}
Per-hook environment values satisfy a hook’s requires.env eligibility checks (alongside the process environment), and handlers can read them from their hook config entry:
{
  "hooks": {
    "internal": {
      "entries": {
        "my-hook": {
          "enabled": true,
          "env": { "MY_CUSTOM_VAR": "value" }
        }
      }
    }
  }
}
Extra hook directories:
{
  "hooks": {
    "internal": {
      "load": {
        "extraDirs": ["/path/to/more/hooks"]
      }
    }
  }
}
The legacy hooks.internal.handlers array config format is still supported for backwards compatibility, but new hooks should use the discovery-based system.

CLI reference

# List all hooks (add --eligible, --verbose, or --json)
openclaw hooks list

# Show detailed info about a hook
openclaw hooks info <hook-name>

# Show eligibility summary
openclaw hooks check

# Enable/disable
openclaw hooks enable <hook-name>
openclaw hooks disable <hook-name>

Best practices

  • Keep handlers fast. Hooks run during command processing. Fire-and-forget heavy work with void processInBackground(event).
  • Handle errors gracefully. Wrap risky operations in try/catch; do not throw so other handlers can run.
  • Filter events early. Return immediately if the event type/action is not relevant.
  • Use specific event keys. Prefer "events": ["command:new"] over "events": ["command"] to reduce overhead.

Troubleshooting

Hook not discovered

# Verify directory structure
ls -la ~/.openclaw/hooks/my-hook/
# Should show: HOOK.md, handler.ts

# List all discovered hooks
openclaw hooks list

Hook not eligible

openclaw hooks info my-hook
Check for missing binaries (PATH), environment variables, config values, or OS compatibility.

Hook not executing

  1. Verify the hook is enabled: openclaw hooks list
  2. Restart your gateway process so hooks reload.
  3. Check gateway logs: openclaw logs --follow | grep -i hook