From 7eebd6d4a6f4ffaab473111f1f9169bfeabeef77 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 26 Aug 2026 23:12:06 -0700 Subject: [PATCH] docs(hooks): clarify setup and execution contracts (#130734) --- docs/automation/cron-jobs.md | 211 +++-- docs/automation/hooks.md | 929 ++++++++++++++++------- docs/automation/imap.md | 7 +- docs/automation/index.md | 31 +- docs/cli/hooks.md | 277 +++++-- docs/cli/webhooks.md | 106 ++- docs/concepts/agent-loop.md | 12 +- docs/concepts/agent-workspace.md | 2 +- docs/concepts/compaction.md | 3 +- docs/concepts/models.md | 2 + docs/gateway/config-agents.md | 4 + docs/gateway/configuration-reference.md | 356 +++++++-- docs/plugins/hooks.md | 565 +++++++++----- docs/plugins/webhooks.md | 176 ++++- docs/tools/plugin.md | 17 +- src/hooks/bundled/README.md | 243 +----- src/hooks/bundled/boot-md/HOOK.md | 5 +- src/hooks/bundled/session-memory/HOOK.md | 7 +- 18 files changed, 1952 insertions(+), 1001 deletions(-) diff --git a/docs/automation/cron-jobs.md b/docs/automation/cron-jobs.md index a6c2a1178fcf..b2b2ca3233d2 100644 --- a/docs/automation/cron-jobs.md +++ b/docs/automation/cron-jobs.md @@ -1,4 +1,5 @@ --- +doc-schema-version: 1 summary: "Automations: scheduled jobs, webhooks, and Gmail PubSub triggers for the Gateway scheduler" read_when: - Scheduling background jobs or wakeups @@ -607,121 +608,175 @@ Model override note: ## Webhooks -Gateway can expose HTTP webhook endpoints for external triggers. Enable in config: +Gateway HTTP hooks let an external service wake an agent or submit an agent turn. +They are disabled by default. These endpoints are separate from [internal event +hooks](/automation/hooks) (`HOOK.md` handlers) and the [Webhooks +plugin](/plugins/webhooks), which manages TaskFlow records. They also differ from +outbound automation webhook delivery: here, the external service calls OpenClaw. -A configured mapping `id` is retained only as bounded ingress-source -attribution when that mapping reaches agent admission. It is not an -authenticated service principal or invoker. Direct `/hooks/agent` and requests -authenticated only by the shared hook token stay unattributed unless another -authoritative principal source exists. If a transform returns `null`, the -request keeps its visible HTTP 204 outcome and stops before creating a run, -task, execution identity, or audit receipt. +### Enable and test an agent hook + +Start with a running Gateway and an agent that can complete a normal turn. Merge +this into your config, replacing the token with a long random value and `main` +with the intended configured agent: ```json5 { hooks: { enabled: true, - token: "shared-secret", + token: "", path: "/hooks", + allowedAgentIds: ["main"], + allowRequestSessionKey: false, }, } ``` +Use a token dedicated to hooks, not the Gateway auth token or password. Run these +commands on the Gateway host with its profile/config. Validate the configuration, +restart the installed service to load it, and watch the logs: + +```bash +openclaw config validate +``` + +```bash +openclaw gateway restart +``` + +```bash +openclaw logs --follow +``` + +If you run the Gateway in the foreground rather than as an installed service, +stop and start that process instead. + +In another terminal, send a harmless test to the local Gateway. Replace the token, +agent id, and port to match your configuration: + +```bash +curl --include http://127.0.0.1:18789/hooks/agent \ + -H 'Authorization: Bearer ' \ + -H 'Content-Type: application/json' \ + -H 'Idempotency-Key: webhook-smoke-001' \ + --data '{"message":"Summarize this test event: the sample import completed.","name":"Webhook smoke test","agentId":"main","deliver":false}' +``` + +The expected admission response is HTTP `200`: + +```json +{ "ok": true, "runId": "" } +``` + +This means the run acquired session/global placement admission. It does **not** +mean the model finished, a tool succeeded, or a message was delivered. A single +agent request can wait up to 15 seconds for admission; the model runtime may still +be preparing when the response arrives. + +For this `deliver: false` test, look for `hook agent run completed without +announcement` in the Gateway logs. Non-ok runs log `hook agent run returned +non-ok status`; thrown failures log `hook agent failed`. Inspect the agent's run +session for its actual output. The HTTP `runId` correlates hook logs; it is not a +TaskFlow id or a task id to pass to `openclaw tasks show`. + +`sessionMode` defaults to `isolated`, so this test gets a fresh run session and +a generated logical `hook:` key. The stored session can use a +`cron:...:run:...` key; the logical hook key is not a promise about the transcript's +storage key. A fixed `defaultSessionKey` serializes requests sharing that key, +even in isolated mode; use it only when that ordering is intended. + ### Authentication -Every request must include the hook token via header: +Every request must include the hook token via one of these headers: -- `Authorization: Bearer ` (recommended) -- `x-openclaw-token: ` +- `Authorization: Bearer ` (recommended). +- `x-openclaw-token: `. -Query-string tokens are rejected. +Query-string `?token=...` authentication is rejected. Send JSON with +`Content-Type: application/json`. All hook endpoints accept `POST` only. The +[Hooks reference](/gateway/configuration-reference#hooks) lists payload fields, +limits, routing policy, and error responses. - Enqueue a system event for the selected agent's main session: + Enqueue a trusted notification for the selected agent's main session and optionally request an immediate heartbeat: ```bash - curl -X POST http://127.0.0.1:18789/hooks/wake \ - -H 'Authorization: Bearer SECRET' \ + curl --include http://127.0.0.1:18789/hooks/wake \ + -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ - -d '{"text":"New email received","mode":"now","agentId":"main"}' + --data '{"text":"The sample import completed","mode":"now","agentId":"main"}' ``` - - Event description. - - - `now` or `next-heartbeat`. - - - Target agent. When supplied, it must name a configured agent. It is required when the configured agent fleet has no implicit or retained legacy owner. - - - Target session. Requires `mode: "now"` and `hooks.allowRequestSessionKey: true`, and must match `hooks.allowedSessionKeyPrefixes` when configured. Deferred `next-heartbeat` wakes use the agent's main session. - + HTTP `200` with `{ "ok": true, "mode": "now" }` means the event was enqueued and a wake was requested, not that a heartbeat completed. Use `mode: "next-heartbeat"` to enqueue without requesting an immediate wake. + + A supplied `agentId` must name a configured agent. Supply it explicitly when the fleet has no implicit or retained legacy owner. A caller-selected `sessionKey` requires `mode: "now"`, `hooks.allowRequestSessionKey: true`, and the configured prefix policy; deferred wakes use the main session. + + Wake text is a system event, not an isolated, safety-wrapped email reader turn. Send only a short notification you control. Route raw email, documents, or other untrusted content through an `agent` action with a restricted reader. - Run an agent turn. Sessions are isolated by default: + Submit an agent turn with a required `message`. Optional routing, model, thinking, timeout, and idempotency fields are documented in the [payload reference](/gateway/configuration-reference#hook-agent-payload). - ```bash - curl -X POST http://127.0.0.1:18789/hooks/agent \ - -H 'Authorization: Bearer SECRET' \ - -H 'Content-Type: application/json' \ - -d '{"message":"Summarize inbox","name":"Email","model":"openai/gpt-5.6-sol"}' - ``` + Keep `sessionMode: "isolated"` for fresh context. Set `"persistent"` only when repeated events should reuse prior context: direct requests then require an explicit `sessionKey`, `hooks.allowRequestSessionKey: true`, and nonempty `hooks.allowedSessionKeyPrefixes`. - Fields: `message` (required), `name`, `agentId` (must name a configured agent when supplied), `sessionKey` (requires `hooks.allowRequestSessionKey=true`), `sessionMode` (`isolated` or `persistent`), `idempotencyKey`, `wakeMode`, `deliver`, `channel`, `to`, `accountId`, `model`, `thinking`, `timeoutSeconds`. + For direct channel delivery, supply both a concrete `channel` and `to`; add `accountId` to select an enabled channel account. Supplying only part of a destination, using `channel: "last"`, or selecting an invalid account returns `400` before dispatch. Direct hooks do not inherit the main session's last recipient. - Set `sessionMode: "persistent"` only when repeated deliveries should reuse prior context. Direct persistent hooks require an explicit `sessionKey`, `hooks.allowRequestSessionKey: true`, and a non-empty `hooks.allowedSessionKeyPrefixes` allowlist. Omit `sessionMode` or use `"isolated"` for a fresh run session. - - Hook delivery is bound before the isolated run is scheduled: - - - Omit both `channel` and `to` to run completion-only; the result is surfaced through the hook completion event. - - While delivery is enabled, supplying only one of `channel` or `to` fails the request with `400` and schedules no run. - - Announce delivery requires a concrete channel; webhook hooks never inherit the main session's `last` channel or recipient. - - Setting `deliver: false` keeps the run completion-only and ignores any delivery destination. - - Supplying both a concrete `channel` and `to` enables direct announce delivery. - - Set `accountId` with `channel` and `to` to select a configured, enabled account on multi-account channels. Unknown, disabled, or invalid account IDs return `400` and schedule no run. - - The HTTP response waits only for canonical session/global placement admission, not for the agent turn to finish. A `200` may take up to 15 seconds and means the execution path acquired that admission; the run may still be preparing its model runtime. Pre-admission failures return `{ ok: false, error, runId }` with: - - - `400` when delivery coordinates or account selection are invalid; correct the request before retrying. - - `409` when the target session changed or otherwise rejects new work; retry after resolving the session conflict. - - `502` when Gateway or cron preparation fails before placement admission. - - `503` when placement admission does not occur within 15 seconds. Timed-out queued work is canceled and does not start later. + With no destination, the default `deliver: true` allows a completion system event on the target agent's main session. Set `deliver: false` to suppress successful announcements and ignore destination fields; completion is logged instead. Non-ok outcomes still produce a failure event. Disabling announcement is not a tool restriction: restrict the agent's tools separately if it must not send messages. - Custom hook names resolve via `hooks.mappings` in config. Mappings can transform arbitrary payloads into `wake` or `agent` actions with templates or code transforms. Mapped `agent` actions use the same 15-second admission and `200`/`400`/`409`/`502`/`503` response contract as `POST /hooks/agent`. + Custom paths resolve through `hooks.mappings`. The first matching mapping wins, ahead of presets. Templates or trusted local JS/TS transforms turn the payload into `wake` or `agent` actions; a transform returning `null` produces HTTP `204` without a run. See [Mapping details](/gateway/configuration-reference#mapping-details). - Persistent mapped hooks require a stable mapping `sessionKey` or `hooks.defaultSessionKey`. Template-derived keys retain the request-key opt-in and prefix policy above. + Persistent mapped hooks require a stable mapping `sessionKey` or `hooks.defaultSessionKey`. Template-derived keys require the same caller-key opt-in and prefix policy as request keys. - Set `forEach: ""` on a mapping to fan out over a top-level payload array: each element dispatches its own action, and templates/transforms see a payload whose array holds only the current element. The Gmail preset uses `forEach: "messages"`, so a batched push dispatches one isolated run per email. Fan-out batches answer within ~8 seconds; a partially dispatched batch returns non-2xx so the producer retries, and already-dispatched items are replayed from a dedupe cache instead of running twice. + `forEach: ""` fans out over a top-level payload array. Each item sees a one-element array, so the Gmail preset's `messages[0]` means the current email. Agent fan-out admission answers after at most about 8 seconds of dispatch waiting; pending items continue in the background and a partial batch returns non-2xx. Retrying the same batch reuses pending or admitted agent items while the bounded in-memory replay cache retains them. It is not durable exactly-once delivery, and mapped wake actions are not deduplicated. The reference covers batch caps and response shapes. - -Keep hook endpoints behind loopback, tailnet, or a trusted reverse proxy. +### Verify and troubleshoot hook requests -- Use a dedicated hook token; do not reuse gateway auth tokens. -- Keep `hooks.path` on a dedicated subpath; `/` is rejected. -- Set `hooks.allowedAgentIds` to limit which effective agent a hook can target, including the default agent when `agentId` is omitted. -- Keep `hooks.allowRequestSessionKey=false` unless you require caller-selected sessions. -- If you enable `hooks.allowRequestSessionKey`, also set `hooks.allowedSessionKeyPrefixes` to constrain allowed session key shapes. -- Hook payloads are wrapped with safety boundaries by default. +| Observation | Check or next action | +| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `401` | Check the hook token, not Gateway auth; ensure the proxy forwards the auth header. | +| `404` | Check `hooks.enabled`, `hooks.path`, and whether the custom path matches a mapping. | +| `400` | Read the response error: JSON, agent selection, session policy, or delivery coordinates may be invalid. Correct the request before retrying. | +| `405`, `408`, or `413` | Use `POST`; send the body promptly; stay within the documented body limit. | +| `429` | Repeated authentication failures were throttled. Correct the token and honor `Retry-After`. | +| `409` | Resolve the target session conflict before retrying. | +| `502` or `503` | Check Gateway logs for preparation, capacity, or restart/suspension failures. Single-run admission timeout cancels queued work; fan-out pending work can still start. | +| `200`, but no chat message | Check completion logs first. `deliver: false` intentionally suppresses successful announcements; direct delivery needs both `channel` and `to`. HTTP admission does not prove delivery. | +| `204` | The mapping intentionally produced no actions, such as a `null` transform or an empty fan-out array. | + +For delivery-enabled requests, also verify receipt at the intended channel, +account, and recipient. A delivery-only failure can leave execution successful; +check delivery diagnostics as well as hook logs. + +For retried agent requests, reuse an `Idempotency-Key` and the same payload. The +[reference](/gateway/configuration-reference#hook-retries-and-fan-out) explains its +scope and lifetime. Use a new key for a new test; a replayed `200` does not run the +agent again. + + +Keep endpoints behind loopback, a tailnet, or a trusted reverse proxy. Use HTTPS +for remote calls and expose only the required path. + +- Use a dedicated hook token and a dedicated subpath; `/` is rejected. +- Restrict `hooks.allowedAgentIds`, including the effective default-agent path. +- Keep `hooks.allowRequestSessionKey: false` unless required; when enabled, constrain `hooks.allowedSessionKeyPrefixes`. +- Treat external event content as data. Agent hook content is safety-wrapped by default, but wrapping does not remove tools or workspace access. Use a restricted agent for untrusted inputs and keep unsafe-content overrides disabled. ## Gmail PubSub integration -Wire Gmail inbox triggers to OpenClaw via Google PubSub. +Wire Gmail inbox triggers to OpenClaw through Google Pub/Sub and `gog gmail watch serve`. Pub/Sub calls the watcher; the watcher forwards email data to the [Gateway HTTP hook](/automation/cron-jobs#webhooks). This does not load or invoke an internal `HOOK.md` handler. Not on Gmail? The [IMAP email trigger plugin](/automation/imap) watches an existing IMAP mailbox without Google PubSub or a public webhook. -**Prerequisites:** `gcloud` CLI, `gog` (gogcli), OpenClaw hooks enabled, Tailscale for the public HTTPS endpoint, and a working sandbox backend. The example below uses the default Docker backend; build its image first by following [Sandbox images and setup](/gateway/sandboxing#images-and-setup), or configure another supported backend. +**Prerequisites:** `gcloud` CLI, `gog` (gogcli) authorized for the watched Gmail account, OpenClaw hooks enabled, an HTTPS push endpoint reachable by Pub/Sub (Tailscale Funnel in the recommended setup), and a working sandbox backend. The example below uses the default Docker backend; build its image first by following [Sandbox images and setup](/gateway/sandboxing#images-and-setup), or configure another supported backend. ### Configure a restricted Gmail reader (recommended) @@ -787,7 +842,7 @@ Why this shape is safer: - `allowedAgentIds` prevents this hook endpoint from selecting another agent. If the Gateway serves other hook workflows, include only their intended agent ids too. - `scope: "session"` gives each Gmail message its own sandbox; `workspaceAccess: "none"` keeps the host agent workspace out of that sandbox. - `allow: ["session_status"]` is an absolute per-agent clamp, so global `tools.alsoAllow` additions cannot leak into the reader. The minimal profile and explicit deny list make the intended boundary auditable. -- `deliver: false` keeps completion inside the hook flow. To announce a summary externally after validating the reader, set `deliver: true` and add an explicit `channel` and `to`. Keep agent-to-agent handoff disabled unless you deliberately expose the exact coordination tool and pair it with a narrow [`tools.agentToAgent`](/gateway/config-tools#tools-agenttoagent) policy. +- `deliver: false` disables automatic successful announcements; completion is logged instead. To announce a summary externally after validating the reader, set `deliver: true` and add an explicit `channel` and `to`. Keep agent-to-agent handoff disabled unless you deliberately expose the exact coordination tool and pair it with a narrow [`tools.agentToAgent`](/gateway/config-tools#tools-agenttoagent) policy. Tool policies can only become more restrictive as global, provider, agent, and sandbox rules are combined. The per-agent allowlist cannot restore `session_status` if an earlier policy removed it. Ensure inherited policies retain `session_status`; an empty effective tool set aborts before the model sees the email. @@ -795,7 +850,7 @@ If you intentionally route Gmail to a more capable agent, treat that as a securi ### Authenticate the reader model -Each agent has its own auth store. Authenticate the provider selected by `mail_reader`, or ensure it can use a supported shared environment/config credential, then verify the effective route before connecting Gmail: +Authenticate the provider selected by `mail_reader`, or ensure its effective auth configuration can use a supported shared credential, then verify the route before connecting Gmail: ```bash openclaw models auth --agent mail_reader login --provider openai @@ -808,10 +863,12 @@ Use the matching provider id when you choose a different model. The live probe c ### Connect Gmail transport ```bash -openclaw webhooks gmail setup --account openclaw@gmail.com +openclaw webhooks gmail setup --account reader@example.com ``` -This writes `hooks.gmail` transport settings, enables the Gmail preset, preserves the restricted mapping above, and defaults to Tailscale Funnel for the push endpoint (`--tailscale funnel|serve|off`). The wizard does not create a reader agent or session-key policy, so apply the restricted configuration first. +This writes `hooks.gmail` transport settings, enables the Gmail preset, preserves the restricted mapping above, and defaults to Tailscale Funnel for the push endpoint (`--tailscale funnel|serve|off`). The wizard does not create a reader agent or session-key policy, so apply the restricted configuration first. `--tailscale serve` is tailnet-only; it is not a publicly reachable Pub/Sub endpoint without another ingress arrangement. Use `--tailscale off --push-endpoint ` for an externally managed endpoint. See [all setup flags](/cli/webhooks). + +The two tokens protect different hops: `hooks.gmail.pushToken` authenticates Pub/Sub to the watcher, while `hooks.token` authenticates the watcher to OpenClaw using a header. A token-bearing Pub/Sub push URL is not an example for `/hooks` authentication; query-string tokens are rejected by OpenClaw. Setup output can contain these tokens, so redact it before sharing. The built-in Gmail preset's per-message session separates conversation context; it does not restrict the target agent's tools or workspace. Without a custom mapping that sets `agentId`, Gmail hooks run as the default agent. @@ -828,16 +885,22 @@ openclaw security audit --deep openclaw logs --follow ``` -Send a test email containing an inert instruction such as “follow this link and run a command.” Confirm the hook resolves to `mail_reader`, the session key starts with `hook:gmail:`, the run is sandboxed, and the result only summarizes the message. Treat any attempted link navigation, file write, shell command, browser action, or MCP registration as a failed boundary check. +Send a test email from another account containing an inert instruction such as “follow this link and run a command.” The watcher excludes `SPAM`, `TRASH`, `DRAFT`, and `SENT`, so a sent-only message is not a useful ingress test. Confirm the selected agent is `mail_reader`, the run is sandboxed, and the output only summarizes the message. The mapping uses the logical `hook:gmail:` key; an isolated run can be stored under a generated `cron:...:run:...` session instead. + +Check forwarding and completion separately. A watcher success only acknowledges transport; a Gateway agent-hook `200` with a `runId` records admission, not a finished summary. With the configuration above, success logs `hook agent run completed without announcement`; non-ok runs produce hook warnings. Inspect the actual run transcript for output and tool use. Treat attempted link navigation, file writes, shell commands, browser actions, or MCP registration as a failed boundary check. ### Gateway auto-start When `hooks.enabled=true` and `hooks.gmail.account` is set, the Gateway starts `gog gmail watch serve` on boot and auto-renews the watch. Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to opt out. -gog batches up to 100 messages per push, and the Gateway dispatches one isolated run per message. The `/hooks/gmail` request-body limit is sized from `hooks.gmail.maxBytes` times that batch contract, so a large backlog cannot wedge delivery on `413` responses. +With `forEach: "messages"`, the Gateway prepares one action per email, up to the 200-item fan-out cap. Gmail-path mappings receive a larger request-body allowance derived from `hooks.gmail.maxBytes`, capped at 32 MiB. The upstream history page size is not a strict email count, so oversized batches can still hit limits. See the [Gmail reference](/gateway/configuration-reference#gmail-integration) for the exact allowance and [fan-out retry behavior](/gateway/configuration-reference#hook-retries-and-fan-out). + +Do not run `openclaw webhooks gmail run` or another `gog gmail watch serve` on the same listener while the Gateway-managed watcher is running. Check logs for watch-registration failures, forwarding failures, and bind conflicts; starting the serve process alone does not prove Gmail registration succeeded. ### Manual one-time setup +These steps show the project, topic, publisher permission, and watch registration. They do not yet create the push subscription or start the forwarding listener. Use the [setup command](/cli/webhooks#webhooks-gmail-setup) for the complete transport setup, then run exactly one watcher. + Select the GCP project that owns the OAuth client used by `gog`: @@ -860,7 +923,7 @@ gog batches up to 100 messages per push, and the Gateway dispatches one isolated ```bash gog gmail watch start \ - --account openclaw@gmail.com \ + --account reader@example.com \ --label INBOX \ --topic projects//topics/gog-gmail-watch ``` diff --git a/docs/automation/hooks.md b/docs/automation/hooks.md index 4a0c4d984c59..a6b8e625cb08 100644 --- a/docs/automation/hooks.md +++ b/docs/automation/hooks.md @@ -1,261 +1,420 @@ --- -summary: "Hooks: event-driven automation for commands and lifecycle events" +summary: "Internal hooks: install, write, and verify 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 + - You want event-driven automation for /new, /reset, /stop, or session and Gateway events + - You want to write, install, enable, or debug an internal hook + - You need to understand hook discovery, event data, or reply delivery title: "Hooks" +doc-schema-version: 1 --- -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, or extra hook directory. +# Hooks -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](/automation/cron-jobs#webhooks). - -Hooks can also be bundled inside plugins. `openclaw hooks list` shows both standalone hooks and plugin-managed hooks (displayed as `plugin:`). +Internal hooks are small JavaScript or TypeScript handlers that run in the +Gateway process when OpenClaw emits an event. Use them to save session context, +log reset commands, or perform short side effects during message and session +lifecycle events. OpenClaw includes [bundled hooks](/automation/hooks#bundled-hooks) +for common tasks; you do not need to write a plugin to use them. ## Choose the right surface -OpenClaw has several extension surfaces that look similar but solve different problems: +| You want to… | Use | +| -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- | +| Save context on `/new`, log commands, or react to session and message events | **Internal hooks** (`HOOK.md` plus a handler), described here | +| Modify prompts, intercept tools, control replies, or use lifecycle contracts with priorities and return values | **[Plugin hooks](/plugins/hooks)** through `api.on(...)` | +| Let another service start work through an HTTP request | **[Webhooks](/automation/cron-jobs#webhooks)** | +| Export telemetry rather than change behavior | **[Diagnostic events](/logging#diagnostics-and-opentelemetry)** | -| If you want to... | Use... | Why | -| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------- | -| Save a snapshot on `/new`, log `/reset`, call an external API after `message:sent`, or add coarse operator automation | Internal 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/policy | Typed plugin hooks via `api.on(...)` | Typed hooks have explicit contracts, priorities, merge rules, and block/cancel semantics | -| Add telemetry-only export or observability | Diagnostic events | Observability is a separate event bus, not a policy hook surface | +These are separate systems. `hooks.internal` configures this page's event +handlers; `hooks.enabled` configures HTTP ingress. Internal event names such as +`message:received` are not typed plugin names such as `message_received`. -Use internal hooks when you want automation that behaves like a small installed integration. Use typed plugin hooks when you need runtime lifecycle control. - -Internal hook handlers are request/event handlers. They must not own long-lived timers, watchers, sockets, or clients; plugins should register a service or use the typed `gateway_start` / `gateway_stop` lifecycle instead. + +Internal hooks are trusted code, not sandboxed scripts. They run with the +Gateway process's filesystem, network, and environment access. Review hook code +before enabling it, especially code from a workspace or downloaded package. + ## Quick start +Start with `command-logger`: it needs no extra binaries or model calls and gives +you a concrete file to inspect. Run these commands on the **Gateway host**, with +the same profile and config as that Gateway: + ```bash -# 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 +openclaw hooks info command-logger +openclaw hooks enable command-logger +openclaw gateway restart ``` -## Event types +`gateway restart` applies to an installed Gateway service. If you run the Gateway +in the foreground, stop and start that process instead. Add `--agent ` to +hook commands when your configuration has multiple agents and no implicit owner. -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 ` flags them, so a -hook that never runs is diagnosable. +In a conversation you can safely reset, send `/new` or `/reset` as an authorized +user. Then inspect the log on the Gateway host: -| Event | When it fires | -| ------------------------ | ---------------------------------------------------------- | -| `command:new` | `/new` command issued | -| `command:reset` | `/reset` command issued | -| `command:stop` | `/stop` command issued | -| `command` | Any command event (general listener) | -| `session:auto-reset` | A daily or idle reset replaces the current session | -| `session:compact:before` | Before compaction summarizes history | -| `session:compact:after` | After compaction completes | -| `session:patch` | When session properties are modified | -| `agent:bootstrap` | Before workspace bootstrap files are injected | -| `gateway:startup` | After channels start and hooks are loaded | -| `gateway:shutdown` | When gateway shutdown begins | -| `gateway:pre-restart` | Before an expected gateway restart | -| `message:received` | Inbound message from any channel | -| `message:transcribed` | After audio transcription completes | -| `message:preprocessed` | After media and link preprocessing completes or is skipped | -| `message:sent` | Outbound send attempted (`context.success` has the result) | +```bash +tail -n 5 ~/.openclaw/logs/commands.log +``` + +Look for a new JSON line with `"action":"new"` or `"action":"reset"`, a recent +`timestamp`, and that conversation's `sessionKey`. With a custom state directory, +read `/logs/commands.log` instead. This proves that a handler ran; +`openclaw hooks check` alone does not. + +The log contains session and sender identifiers. Disable the hook after trying +it if you do not want to retain those records: + +```bash +openclaw hooks disable command-logger +openclaw gateway restart +``` + +### Eligible, enabled, and loaded + +Keep these three checks separate: + +- **Requirements satisfied**: the hook's OS, binaries, environment, and config + requirements pass on the host doing the check. +- **Enabled by config**: the per-hook/source policy allows it. Workspace hooks + require explicit opt-in; bundled and managed hooks do not require that + per-hook flag when broad discovery is enabled. +- **Loaded**: the running Gateway selected the hook, imported its handler, and + registered its events. This also requires the master switch and configured + name selection to allow it. + +The CLI's `ready`, `eligible`, and `loadable` fields describe the first two checks +plus a nonempty event list. They do **not** prove that the Gateway imported the +handler, that the global selection includes it, or that its event has fired. +After changes, restart and verify the actual side effect or hook-specific log. + +### Local, remote, and agent scope + +`hooks list`, `info`, and `check` request the selected Gateway's inventory. An +implicit local Gateway can fall back to local discovery when unavailable or +when it lacks the report method. A configured remote Gateway or explicit +`OPENCLAW_GATEWAY_URL` does not fall back to your laptop's hooks on failure. + +`hooks enable` and `hooks disable` always inspect and modify **local config**. +They do not update a remote Gateway over RPC. Run them on the Gateway host to +change that host's hooks. + +`--agent ` selects the workspace to inspect, not an isolated hook registry. +The saved `hooks.internal.entries.` entry is global. Gateway startup +loads directory hooks from its startup workspace into a process-wide registry; +it does not load every agent's `hooks/` directory merely because you inspected +it. A loaded handler must filter the event's agent or session when it should +only act for a particular agent. See [Hook discovery](/automation/hooks#hook-discovery). ## Writing hooks +This example replies to a reset command and writes a fixed log marker. It does +not read message content, call a model, or contact an external service. + ### Hook structure -Each hook is a directory containing two files: +On the Gateway host, use a new managed hook directory. The following commands +assume the default state directory and that `reset-greeting` does not already +exist; choose another name rather than overwrite an existing hook. -```text -my-hook/ -├── HOOK.md # Metadata + documentation -└── handler.ts # Handler implementation +```bash +mkdir -p ~/.openclaw/hooks/reset-greeting + +cat > ~/.openclaw/hooks/reset-greeting/HOOK.md <<'HOOK' +--- +name: reset-greeting +description: "Confirm that a reset hook ran" +metadata: + { "openclaw": { "events": ["command:new", "command:reset"] } } +--- + +# Reset greeting + +Send a short confirmation after an authorized reset command. +HOOK + +cat > ~/.openclaw/hooks/reset-greeting/handler.js <<'HANDLER' +export default function handler(event) { + if (event.type !== "command" || !["new", "reset"].includes(event.action)) { + return; + } + + console.log("[reset-greeting] reset hook ran"); + event.messages.push("Reset hook ran."); +} +HANDLER ``` -The handler file can be `handler.ts`, `handler.js`, `index.ts`, or `index.js`. +A hook needs `HOOK.md` and a handler file. Discovery checks, in order, +`handler.ts`, `handler.js`, `index.ts`, then `index.js`, using the first file it +finds. The example uses JavaScript so no TypeScript types or SDK imports are +needed. + +Enable and load it: + +```bash +openclaw hooks info reset-greeting +openclaw hooks enable reset-greeting +openclaw gateway restart +``` + +Send `/new` in a disposable conversation on a configured chat channel that can +route replies, such as a direct message to the bot. Expect **Reset hook ran.** +in that conversation and `[reset-greeting] reset hook ran` in Gateway logs. +`/reset` triggers the same example. Normal command authorization still applies. + +Use an ordinary OpenClaw conversation, not an ACP-bound thread; bound sessions +delegate reset handling to their owning runtime. Do not use Control UI/webchat +or a `sessions.reset` RPC as the chat-reply check: +those paths do not deliver this hook's `event.messages` to the UI. The log marker +can still show that a reset event ran. See +[Reply delivery](/automation/hooks#reply-delivery) for the exact boundary. + +Disable the example when finished: + +```bash +openclaw hooks disable reset-greeting +openclaw gateway restart +``` + +Disabling leaves the files in place. To use a workspace directory instead, put +the two files in `/hooks/reset-greeting/`, then explicitly enable the +hook. Workspace placement is not an agent sandbox or a guarantee that the +Gateway will load that workspace's hooks. + +### Handler implementation + +A handler exports a function returning `void` or `Promise`. The loader uses +the default export unless `metadata.openclaw.export` names another export. +Returned values do not block, cancel, or rewrite the operation. + +Every event has these fields: + +| Field | Meaning | +| ------------ | ---------------------------------------------------------------------------------------------------------- | +| `type` | Family: `command`, `session`, `agent`, `gateway`, or `message` | +| `action` | Action within the family, such as `new` or `compact:before` | +| `sessionKey` | Session correlation key; Gateway events use a Gateway key instead | +| `timestamp` | JavaScript `Date` when the event object was created | +| `context` | Event-specific data described under [Event context highlights](/automation/hooks#event-context-highlights) | +| `messages` | Initially empty string array; only certain producers consume it as replies | + +Treat context as an observation, not a live state-editing API. Fields vary by +producer, and `cfg` is not present on every event. In particular, patch events +carry cloned snapshots. The explicit mutable exception is +`agent:bootstrap`'s `context.bootstrapFiles`. + +### Reply delivery + +Pushing to `event.messages` is not a general send-message API: + +| Producer | What happens to `event.messages` | +| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| Chat command handling for `/new` and `/reset` | Awaits handlers, joins strings with blank lines, and attempts a reply to the originating channel/recipient, preserving account and thread context | +| Gateway session reset/create RPCs that emit `command:new` or `command:reset` | Handlers run, but messages are not routed as chat replies | +| `session:compact:before` and `session:compact:after` | Forwarded to the caller's compaction-notice callback when present; that callback owns delivery | +| All other core events | Ignored as replies, including `/stop`, automatic reset, message events, bootstrap, patch, and Gateway lifecycle events | + +A missing recipient, unsupported route, send policy, or delivery failure can +prevent a reply. Append messages before the handler's promise settles; detached +work that pushes later can miss the producer's delivery step. To control normal +agent replies or send cancellation, use the appropriate +[typed plugin hook](/plugins/hooks). ### HOOK.md format +`HOOK.md` uses YAML frontmatter followed by human-readable Markdown: + ```markdown --- name: my-hook description: "Short description of what this hook does" +homepage: https://example.com/my-hook metadata: { "openclaw": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } } --- # My Hook -Detailed documentation goes here. +Explain the side effects, configuration, and verification steps here. ``` -**Metadata fields** (`metadata.openclaw`): +`name` defaults to the directory name; use a unique, stable name. +`description` is shown in reports. The following fields belong under +`metadata.openclaw`: -| Field | Description | -| ---------- | ---------------------------------------------------- | -| `emoji` | Display emoji for CLI | -| `events` | Array of events to listen for | -| `export` | Named export to use (defaults to `"default"`) | -| `os` | Required platforms (e.g., `["darwin", "linux"]`) | -| `requires` | Required `bins`, `anyBins`, `env`, or `config` paths | -| `always` | Bypass `requires.*` checks on a compatible OS | -| `hookKey` | Config key override (defaults to the hook name) | -| `homepage` | Docs URL shown by `openclaw hooks info` | -| `install` | Installation methods | +| Field | Contract | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `events` | Event-key array. At least one is needed to register a handler. | +| `export` | Function export name; defaults to `default`. | +| `hookKey` | Config-entry key; defaults to the hook name. Discovery collisions still use the hook name. | +| `emoji` | Display emoji. | +| `homepage` | Documentation URL; overrides top-level `homepage`, `website`, or `url`. | +| `os` | Allowed Node platform names, for example `darwin`, `linux`, or `win32`. | +| `requires.bins` | Every named executable must be on `PATH`. | +| `requires.anyBins` | At least one named executable must be on `PATH`. | +| `requires.env` | Every named variable needs a nonblank process value or per-hook `env` value. | +| `requires.config` | Every dotted config path must be truthy. | +| `always` | Bypass binary, environment, and config requirements; does not bypass OS or enablement policy. | +| `install` | Informational install descriptors: `kind` is `bundled`, `npm`, or `git`; optional `id`, `label`, `package`, `repository`, and `bins`. This metadata does not install dependencies or make Git specs accepted by the CLI. | -### Handler implementation +Use `hooks.internal.entries..enabled` to control activation, not a +top-level `enabled` flag in `HOOK.md`. For historical requirement metadata, +`workspace.dir`, `browser.enabled`, and `browser.evaluateEnabled` default to true +when absent. `workspace.dir` is not a new setting you need to add to your config. -```typescript -const handler = async (event) => { - if (event.type !== "command" || event.action !== "new") { - return; +## Configuration + +For a predictable selection, enable named hooks rather than turning on broad +discovery: + +```json +{ + "hooks": { + "internal": { + "enabled": true, + "entries": { + "command-logger": { "enabled": true }, + "session-memory": { "enabled": false } + } + } } - - 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`. - -**Automatic reset events** (`session:auto-reset`): `context.sessionEntry`, `context.reason` (`daily` or `idle`), `context.transcriptArchived`, `context.nextSessionId`, `context.nextSessionKey`, `context.agentId`, `context.workspaceDir`, `context.storePath`, and `context.cfg`. - -**Message events** (`message:received`): `context.from`, `context.content`, `context.channelId`, `context.media` (ordered staged attachment facts), `context.originalMedia` plus `context.mediaStagingPending` when remote media is not locally staged yet, and `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. Legacy media aliases inside `metadata` are deprecated. - -**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`, and `context.media`. `context.mediaPath` and `context.mediaType` remain deprecated aliases for the first fact. - -**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](/plugins/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: - -```typescript -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. +The master switch and selection rules for directory-loaded hooks are: + +| Configuration | Selection | +| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `hooks.internal.enabled: false` | Internal hooks are off. | +| No master flag and no enabled entries, extra directories, or tracked installs | Gateway skips directory-hook loading. | +| Named entries, with master flag omitted or true | Enabled names form an allowlist; `enabled: true` on the master does not broaden it. An entry without `enabled: false` contributes its name. | +| Master flag true with no named entries or named installs | Open-ended discovery of eligible hooks. | +| Tracked hook packs declaring hook names | Those names join the selection; an explicit per-hook `enabled: false` still disables a non-plugin hook. | +| Nonempty `load.extraDirs`, or a tracked install without a hook-name list | Open-ended discovery, not a selection restricted to that directory or pack. | + +Workspace hooks always need `entries..enabled: true`, even with +open-ended discovery. For other file hooks, an entry can be selected by its +name or `hookKey`, but settings are read under `hookKey`. The CLI resolves the +name and writes the correct key for you. Adding the first named entry can narrow +a previously broad selection; inspect existing hooks before changing it. + +Per-hook entries accept arbitrary handler-defined fields. The core types +`enabled` as a boolean and `env` as a string-to-string map; it does not validate +custom handler options. For example: + +```json +{ + "hooks": { + "internal": { + "entries": { + "my-hook": { + "enabled": true, + "env": { "MY_HOOK_LABEL": "example" } + } + } + } + } +} +``` + +Per-hook `env` satisfies eligibility checks but **does not mutate `process.env`**. +On events carrying config, a handler can read it from +`event.context.cfg?.hooks?.internal?.entries?.["my-hook"]?.env`. Other events do +not promise a `cfg` field. Do not log entire config objects or put secrets in +examples. + + +`hooks.internal.handlers` is retired and fails normal config validation. Before +running `openclaw doctor --fix`, migrate each registered module into a managed or +workspace hook directory with `HOOK.md` and a handler. Doctor removes the old +registrations; it does not create executable files. For a legacy-only config +with `hooks.internal.enabled: true`, it also removes that flag to avoid broad +discovery. Named entries, nonempty extra directories, and explicit +`enabled: false` are preserved. + ## Hook discovery -Hooks are discovered from four sources: +Directory discovery merges hooks by **name** using these rules: -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**: `/hooks/` (per-agent, disabled by default until explicitly enabled) +| Source | Location and collision behavior | +| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Bundled | Shipped with OpenClaw. | +| Plugin | Hook directories declared by active plugins; can replace bundled names. | +| Managed | `/hooks/`, normally `~/.openclaw/hooks/`; can replace bundled and plugin names. | +| Extra directories | `hooks.internal.load.extraDirs`; same source policy as managed hooks. Later extra directories win over earlier ones; the managed directory wins over extra directories. | +| Workspace | `/hooks/`; can add names but cannot replace bundled, plugin, or managed names. Explicit opt-in required. | -Workspace hooks can add new hook names but cannot override bundled, managed, or plugin-provided hooks with the same name. +Each scanned directory contains child hook directories or child packages whose +`package.json` declares `openclaw.hooks`. The scanner does not treat the scan +root itself as a hook. For example, with +`/opt/openclaw-hook-library/my-hook/HOOK.md`, add the parent directory: -The Gateway skips internal hook discovery on startup until internal hooks are configured. Enable a bundled or managed hook with `openclaw hooks enable `, install a hook pack, or set `hooks.internal.enabled=true` to opt in. Named entries remain an allowlist even when the master flag is true. A bare `hooks.internal.enabled=true` with no named entries enables broad discovery; non-empty extra hook directories and hook-pack installs that do not declare their hook names are also open-ended. +```json +{ + "hooks": { + "internal": { + "load": { + "extraDirs": ["/opt/openclaw-hook-library"] + } + } + } +} +``` + +Only add trusted directories: this also opens selection beyond named entries. +Handler files must stay within their hook directory; package and plugin hook +paths must stay within their package root. Symlinks escaping those boundaries +are rejected. Restart after changing hook files, metadata, or configuration. ### Hook packs -Hook packs are npm packages that export hooks via `openclaw.hooks` in `package.json`. Install with: +A hook pack is a package whose `package.json` declares hook directories in +`openclaw.hooks`. Install a reviewed package or local directory through the +unified installer: ```bash openclaw plugins install ``` -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`. +Installation and update flags, npm restrictions, linked-directory caveats, and +the deprecated `hooks install` / `hooks update` aliases are documented in +[Install and update hook packs](/cli/hooks#install-and-update-hook-packs). ## Bundled hooks -| Hook | Events | What it does | -| --------------------- | ---------------------------------------------------- | -------------------------------------------------------------- | -| session-memory | `command:new`, `command:reset`, `session:auto-reset` | Saves session context to `/memory/` | -| bootstrap-extra-files | `agent:bootstrap` | Injects additional bootstrap files from glob patterns | -| command-logger | `command` | Logs emitted command events to `~/.openclaw/logs/commands.log` | -| compaction-notifier | `session:compact:before`, `session:compact:after` | Sends visible chat notices when session compaction starts/ends | -| boot-md | `gateway:startup` | Runs `BOOT.md` when the gateway starts | +| Hook | Events | Purpose | +| ----------------------- | ---------------------------------------------------- | ---------------------------------------------------------- | +| `boot-md` | `gateway:startup` | Run workspace `BOOT.md` instructions at startup. | +| `bootstrap-extra-files` | `agent:bootstrap` | Add matching workspace bootstrap files to context. | +| `command-logger` | `command` | Append emitted command events to a JSONL log. | +| `compaction-notifier` | `session:compact:before`, `session:compact:after` | Add compaction status notices on supported delivery paths. | +| `session-memory` | `command:new`, `command:reset`, `session:auto-reset` | Save recent conversation excerpts to workspace memory. | -Enable any bundled hook: +Enable one with `openclaw hooks enable `, then restart and verify its +side effect. The following sections describe what to expect. -```bash -openclaw hooks enable -``` + - +### boot-md details -### session-memory details +Runs a nonempty `BOOT.md` from each configured agent's resolved workspace. +Workspaces shared by multiple agents run only once, under the first agent +selected for that workspace. Startup tasks run sequentially; a failed task is +logged and does not prevent later tasks. -On `/new`, `/reset`, daily reset, or idle expiry, extracts the last user/assistant messages (default 15, configurable with `hooks.internal.entries.session-memory.messages`) and saves them to `/memory/YYYY-MM-DD-HHMM.md` using `agents.defaults.userTimezone`. When no user timezone is configured, it falls back to the host timezone. Memory capture runs in the background so reset handling and replacement sessions are not delayed by transcript reads or optional slug generation. Set `hooks.internal.entries.session-memory.llmSlug: true` to generate descriptive filename slugs, and optionally set `hooks.internal.entries.session-memory.model` to a configured alias such as `sonnet`, a bare model ID on the agent's default provider, or a `provider/model` ref. Slug generation uses the agent's default model when `model` is omitted and falls back to timestamp slugs when unavailable. Requires `workspace.dir` to be configured. +This executes instructions through an agent run, not as a shell script and not +as a bootstrap file injection. It uses a temporary `agent::boot` session and +preserves the prior session mapping. Normal final-response delivery is disabled; +if the instructions need to notify someone, they must specify a channel and +target for the message tool. Missing or empty files are skipped. - -The `memory` source already indexes this hook's saved conversation excerpts. If -[session transcript indexing](/reference/memory-config#session-memory-search) -is also enabled, the same conversation can appear from both `memory` and -`sessions`, producing overlapping search results and additional embedding work. -For hook-only recall, set `memory.search.sources: ["memory"]` and -`memory.search.rememberAcrossConversations: false`; `sources` alone does not -prevent cross-conversation recall from adding `sessions`. For full-transcript -recall instead, run `openclaw hooks disable session-memory`. Enable both only -when you intentionally want both representations. - +Keep boot instructions short and safe to repeat on every restart. They can use +model and tool capabilities, so enabling this hook can cause model calls and +outbound side effects. @@ -276,155 +435,337 @@ when you intentionally want both representations. } ``` -`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`, `IDENTITY.md`, `USER.md`, `BOOTSTRAP.md`, `MEMORY.md`). +`paths` is preferred. If it is empty, the handler tries `patterns`, then `files`; +these are alternatives, not merged lists. Without patterns, the hook does nothing. -`TOOLS.md` is no longer a recognized bootstrap basename and is not loaded into runtime context. `openclaw doctor --fix` migrates the workspace-root `TOOLS.md` into the `## Tools` section of `AGENTS.md`; patterns that name other `TOOLS.md` files are not migrated and should be repointed at `AGENTS.md`. +Paths resolve relative to the event's workspace and must remain inside it, +including after symlink resolution. Only these basenames load: `AGENTS.md`, +`SOUL.md`, `IDENTITY.md`, `USER.md`, `BOOTSTRAP.md`, and `MEMORY.md`. + +Extra files go through normal bootstrap filtering and injection limits. Reads +are capped at 2 MiB per file. Injection defaults to 20,000 characters per file +and 60,000 total, controlled by `bootstrapMaxChars` and +`bootstrapTotalMaxChars` in agent defaults or overrides; `USER.md` has a separate +4,000-character cap. Duplicate paths are removed. Subagents retain only +`AGENTS.md`; cron and non-private conversations have additional context/privacy +filters. Inspect the actual injected result with `/context detail`; see +[Context](/concepts/context). + +`TOOLS.md` is not a recognized runtime bootstrap basename. +`openclaw doctor --fix` archives workspace-root `TOOLS.md` and merges customized +content into the `## Tools` section of `AGENTS.md`. Other `TOOLS.md` files named +by patterns are not migrated; +point those patterns at `AGENTS.md` instead. ### command-logger details -Logs each emitted command event as a JSON line (timestamp, action, session key, sender ID, source) to `~/.openclaw/logs/commands.log`. Current core command events are `/new`, `/reset`, and `/stop`; plugins may emit additional actions. +Appends one JSON line per emitted command event to +`/logs/commands.log`. Fields are `timestamp`, `action`, `sessionKey`, +`senderId`, and `source`; absent sender/source values become `unknown`. +Core emits `/new`, `/reset`, and `/stop`, not every slash command. + +The handler awaits the append, logs write errors, and sends no chat confirmation. +It does not rotate the log. Set appropriate access and retention for the session +and sender identifiers it records. See [Log inspection](/cli/hooks#command-logger-log-file). ### 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. +Adds a short notice before compaction and a completion notice after successful +compaction. Notices can include message counts and before/after token counts +when available. They travel through the compaction caller's notice callback; +without a callback that delivers them, enabling the hook does not guarantee a +visible message. A before notice without an after notice can indicate a +skipped, failed, or interrupted compaction, not a stuck hook. Manual `/compact` +does not supply this hook-message delivery callback, so it is not a reliable +way to test the notices. - + -### boot-md details +### session-memory details -Runs `BOOT.md` at gateway startup for each configured agent scope, if the file exists in that agent's resolved workspace. +Saves the ended session's recent user/assistant text on `/new`, `/reset` +(including soft reset), or automatic daily/idle rollover. Automatic rollover +emits `session:auto-reset`, not a synthetic command event. Expiry is checked when +a subsequent turn is admitted; this is not a timer that writes memory at the +daily boundary while the session is idle. + +The artifact is `/memory/YYYY-MM-DD-HHMM.md` by default, with a +numeric suffix if that filename already exists. Dates use +`agents.defaults.userTimezone`, then process `TZ` when no user timezone is set, +and the host timezone as fallback. The file records session identity and the +command source or automatic reset reason. + +| Entry option | Default | Behavior | +| ------------ | ------------- | --------------------------------------------------------------------------------------------------------------- | +| `messages` | `15` | Recent user/assistant messages to include; use a positive integer. | +| `llmSlug` | `false` | Ask a model for a descriptive filename slug. | +| `model` | Agent default | Optional configured alias, bare model ID on the default provider, or `provider/model` used for slug generation. | + +The hook captures a bounded transcript snapshot before background writing +(up to 4,096 scanned messages and 8 MiB for that capture). +Manual resets do not await the file write or optional slug-model call; automatic +reset dispatch also runs independently of the successor turn. Wait for +`Session context saved to ...` in logs before expecting the file. + +This is a filtered excerpt, not a complete transcript or a model-written +summary. It omits slash-command text, tool messages, inter-session user input, +silent reply markers, and duplicate delivery-mirror text. If transcript reading +fails, the artifact can record that content was unavailable. The workspace is +resolved from event/agent config; you do not need to add a `workspace.dir` key. + +With `llmSlug: true`, conversation text is sent to the configured model to name +the file. Failure falls back to a timestamp slug. Leave it off if you want no +extra model call for naming. + + +Saved excerpts are workspace memory artifacts. If +[session transcript indexing](/reference/memory-config#session-memory-search) +is also enabled, one conversation can be represented by both `memory` and +`sessions`, adding overlapping results and embedding work. For hook-only recall, +set `memory.search.sources: ["memory"]` and +`memory.search.rememberAcrossConversations: false`; `sources` alone does not stop +cross-conversation recall from adding `sessions`. For full-transcript recall +instead, disable `session-memory`. These search settings do not disable the +hook's file writes or ordinary transcript persistence. + + +## Event types + +Subscribe to an exact key below or a bare family (`command`, `session`, `agent`, +`gateway`, `message`). Family subscriptions receive all actions in that family. +Do not subscribe the same handler to both `command` and `command:new` unless you +want it called twice for a new command. `session:compact` is not a family or a +wildcard; subscribe to the two exact compaction keys. + +| Event | Trigger and wait behavior | +| ------------------------ | ------------------------------------------------------------------------------------------------------------------------- | +| `command:new` | Authorized new-session command handling, or a Gateway session operation that emits new-command hooks; awaited. | +| `command:reset` | Authorized reset-command handling or Gateway session reset; awaited. | +| `command:stop` | Stop-command handling after the abort request; awaited, with no hook reply delivery. | +| `session:auto-reset` | Existing session replaced due to daily/idle policy; dispatched independently of the successor turn. | +| `session:compact:before` | Before compaction work; awaited. | +| `session:compact:after` | After successful compaction; awaited. | +| `session:patch` | An authorized Gateway patch is applied, or a supported model-selection path persists a change; asynchronous notification. | +| `agent:bootstrap` | Workspace bootstrap resolution before context injection; awaited. | +| `gateway:startup` | Scheduled after hook loading and sidecar/channel startup work; does not delay initial Gateway bind. | +| `gateway:shutdown` | Shutdown begins, before channel/plugin teardown; bounded wait. | +| `gateway:pre-restart` | Shutdown has a finite expected-restart delay; bounded wait. | +| `message:received` | Accepted inbound dispatch with a session key; asynchronous observation. | +| `message:transcribed` | Pre-agent preprocessing has nonempty audio transcript text and a session key; asynchronous observation. | +| `message:preprocessed` | Media/link preprocessing completed or was skipped, with a session key; asynchronous observation. | +| `message:sent` | A delivery owner reports a send outcome with a session key; asynchronous observation. Inspect `context.success`. | + +Not every incoming transport update or attempted low-level send produces an +internal message event. Suppressed/duplicate inbound dispatches and paths with +no session key can omit them. These are observation points, not a complete +transport audit or a way to block message processing. Fast native-command paths +can skip preprocessing events. `preprocessed` means that phase was passed, not +that every attachment or link was successfully understood. Likewise, compaction +can skip or fail after its before event, and retries can emit before again. + +Unknown subscriptions such as `command:nwe` are still registered, but the loader +warns and `hooks info` reports them. Core does not emit them. A custom key only +fires if custom code explicitly emits it; declaring it in metadata does not +create a trigger. + +`command:stop` observes cancellation command handling. It is not a natural +agent-finalization gate. For that contract, see `before_agent_finalize` in +[Plugin hooks](/plugins/hooks). + +### Event context highlights + +Fields below describe the producer payloads. Values marked optional may be +absent; do not assume fields from one event exist on another. + +**`command:new` and `command:reset`:** `agentId`, `sessionEntry`, +`previousSessionEntry`, `commandSource`, `senderId`, `workspaceDir`, `storePath`, +and `cfg` on the chat command path. Entries and routing metadata depend on the +caller. Gateway reset uses `commandSource: "gateway:sessions.reset"`; Gateway +agent reset uses `gateway:agent`, and session creation can use `webchat`. +Gateway callers omit `senderId`. Session creation emits new-command hooks only +when requested with `emitCommandHooks` for an existing parent. Prefer +`previousSessionEntry` for the session being replaced: chat and Gateway paths +emit at different points in reset, so this is not a universal pre-reset or +successful-reset receipt. +A `sessionFile` value can be a transcript identifier rather than a readable file +path; do not assume it is JSONL on disk. + +**`command:stop`:** optional `sessionEntry`, `sessionId`, `commandSource`, and +`senderId`. It does not carry the full new/reset context. + +**`session:auto-reset`:** `cfg`, `agentId`, `workspaceDir`, `storePath`, +`sessionEntry` identifying the ended `sessionId` and optional `sessionFile`, +`reason` (`daily` or `idle`), and optional `transcriptArchived`, `nextSessionId`, +and `nextSessionKey`. + +**`agent:bootstrap`:** `workspaceDir`, mutable `bootstrapFiles`, and optional +`cfg`, `sessionKey`, `sessionId`, `agentId`. Each bootstrap record has `name`, +`path`, `missing`, and optional `content`. A handler can replace or extend the +array, but final path deduplication, session/privacy filtering, and context +budgets still apply. + +**`session:patch`:** cloned post-operation `sessionEntry`, request-shaped `patch`, +and `cfg`. The patch contains target/expectation fields and submitted settings, +not a computed changed-fields diff. Successful Gateway patches can emit even +when a submitted value was already present. Supported model-selection paths +also emit, including `/model`, the model picker, and model changes through +`session_status`; a read-only status query does not. This is not a notification +for every session-store write. + +**Compaction:** both phases include `sessionId`, `missingSessionKey`, +`messageCount`, and optional `tokenCount`. Before also includes +`messageCountOriginal` and optional `tokenCountOriginal`. After includes +`compactedCount` and optional `summaryLength`, `tokensBefore`, `tokensAfter`, and +`firstKeptEntryId`. Do not infer unavailable token counts as zero. + +**`gateway:startup`:** `cfg`, `deps`, and `workspaceDir`. **Shutdown and +pre-restart:** `reason` and `restartExpectedMs` (null when no restart is expected +on shutdown). The shutdown wait defaults to 5 seconds; pre-restart adds a +separate 10-second budget. These bound the caller's wait, not the handler's work: +timeout does not cancel promises. Channels have not yet been torn down, but +neither queued agent work nor message delivery is guaranteed to finish before +shutdown. Typed `session_end` drain behavior belongs to [Plugin hooks](/plugins/hooks). + +#### Message context + +`message:received` contains `from`, `content`, `channelId`, and optional +`timestamp`, `accountId`, `conversationId`, `messageId`, `media`, `originalMedia`, +`mediaStagingPending`, and `metadata`. Content prefers a nonblank command body, +then raw body, then generic body. It does not select `BodyForAgent`; the fallback +body is surface-defined rather than stripped of all enrichment by the mapper. + +Received `metadata` can contain `to`, `provider`, `surface`, `threadId`, +`senderId`, `senderName`, `senderUsername`, `senderE164`, `guildId`, `channelName`, +and `topicName`. Legacy attachment aliases are `mediaPath`, `mediaUrl`, +`mediaType`, `mediaPaths`, `mediaUrls`, and `mediaTypes`; remote-staging metadata +can also include `mediaRemoteHost`, `mediaStagingPending`, and corresponding +`originalMediaPath`, `originalMediaUrl`, `originalMediaType`, `originalMediaPaths`, +`originalMediaUrls`, and `originalMediaTypes`. Prefer the structured media arrays. + +`message:transcribed` and `message:preprocessed` contain `channelId`, `cfg`, and +optional `from`, `to`, `body`, `bodyForAgent`, `timestamp`, `conversationId`, +`messageId`, `senderId`, `senderName`, `senderUsername`, `provider`, `surface`, +and the structured media fields. Transcribed adds required `transcript` text; +preprocessed adds optional `transcript`, `isGroup`, and `groupId`. +`bodyForAgent` is the enriched body prepared for the agent. `mediaPath` and +`mediaType` remain deprecated first-attachment aliases. These contexts do not +promise `accountId` or the received event's `metadata` object. + +Each structured media fact can contain `path`, `url`, `contentType`, `kind`, +`transcribed`, `messageId`, and `workspaceDir`. Facts preserve source order. +When `mediaStagingPending` is true, `media` is withheld and `originalMedia` +describes the original attachments; do not treat remote paths as local files. + +`message:sent` contains `to`, `content`, `success`, `channelId`, and optional +`error`, `accountId`, `conversationId`, `messageId`, `isGroup`, and `groupId`. +`success: false` reports failure on a path that emitted an outcome; absence of +an event is not proof of either success or failure. Outbound delivery can report +one outcome per logical payload rather than per text chunk, and a partial +failure can include a message ID for a part already sent. Durable outbound +queue settlement can defer the observation; it does not make the hook durable. +Do not blindly resend on failure: you can duplicate a delivered part. A send +result is not proof that the recipient read the message. ## 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 appear as `plugin:` in `hooks list`. They +participate in this event system, but you enable or disable the owning plugin +rather than toggling them with `hooks enable` or `hooks disable`. The directory +loader's configured-name selection is not a policy gate for typed `api.on` +hooks or a substitute for plugin activation. -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:`. Use those for side effects and compatibility with hook packs, not -for ordered middleware or policy gates. - -The legacy Plugin SDK `api.registerHook` registers into the internal event -system only (`command:new`, `gateway:startup`, `message:received`, ...). Typed -lifecycle event names such as `before_tool_call`, `message_received`, or -`session_start` are dispatched exclusively by the typed hook runner and are -**not** invoked through `registerHook`. Registering a typed name with -`registerHook` emits a registration warning pointing to the public `api.on(...)` -API as the replacement; it never silently no-ops. - -For the complete plugin hook reference, see [Plugin hooks](/plugins/hooks). - -## Configuration - -```json -{ - "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: - -```json -{ - "hooks": { - "internal": { - "entries": { - "my-hook": { - "enabled": true, - "env": { "MY_CUSTOM_VAR": "value" } - } - } - } - } -} -``` - -Extra hook directories: - -```json -{ - "hooks": { - "internal": { - "load": { - "extraDirs": ["/path/to/more/hooks"] - } - } - } -} -``` - - -`hooks.internal.handlers` is retired and is no longer loaded or accepted by normal config validation. Before running `openclaw doctor --fix`, move each registered module into a managed or workspace hook directory with `HOOK.md` and a handler file. Doctor removes the retired registrations; it does not create executable hook files. For a legacy-only configuration with `hooks.internal.enabled: true`, Doctor also removes `enabled` to avoid enabling unrelated discovered hooks. Canonical entries, non-empty extra directories, and explicit `enabled: false` are preserved. - - -## CLI reference - -```bash -# List all hooks (add --eligible, --verbose, or --json) -openclaw hooks list - -# Show detailed info about a hook -openclaw hooks info - -# Show eligibility summary -openclaw hooks check - -# Enable/disable -openclaw hooks enable -openclaw hooks disable -``` +The legacy `api.registerHook` API registers internal events. It does not invoke +typed lifecycle names such as `before_tool_call`, `message_received`, or +`session_start`; registering those names emits a warning directing authors to +`api.on(...)`. For new integrations needing typed lifecycle control, use the +[Plugin hooks](/plugins/hooks) reference. ## 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. +Handlers for one event run sequentially: family listeners first, then exact +listeners, in registration order within each group. The dispatcher awaits each +handler, catches and logs thrown errors, and continues to later handlers. +There is no priority option for file hooks. + +This sequencing does not serialize different events. Message notifications, +patch notifications, and automatic reset work can overlap with other events and +agent processing. There is no general handler timeout, cancellation signal, +durable event queue, automatic retry, or exactly-once guarantee. Restart or +process exit can lose in-flight work. + +Keep side effects short and bounded. Await the work that belongs to the handler, +set timeouts on network calls, limit data sizes, and make repeatable operations +idempotent. Do not use `void doHeavyWork(event)` as a general solution: that work +escapes the handler's wait/error boundary and can outlive its session or process. +If work needs a durable job lifecycle, use an automation or service that owns it. + +Filter unrelated events early and avoid logging message bodies, whole config +objects, or credentials. Message and session data can be private. Keep only the +minimum needed for the side effect, protect output files, and set retention. +Long-lived timers, watchers, sockets, and clients belong to a plugin service +with an explicit shutdown lifecycle, not a request/event handler. + +## CLI reference + +See [`openclaw hooks`](/cli/hooks) for every public report and toggle option, +JSON output fields, exit behavior, and install/update aliases. ## Troubleshooting ### Hook not discovered -```bash -# Verify directory structure -ls -la ~/.openclaw/hooks/my-hook/ -# Should show: HOOK.md, handler.ts +Check the report's `workspaceDir` and `managedHooksDir` with +`openclaw hooks list --json`. Confirm you are inspecting the intended host, +profile, and agent. The hook must be a child directory containing `HOOK.md` and +one supported handler file; a metadata file alone is insufficient. -# List all discovered hooks -openclaw hooks list -``` +Check duplicate names and containment warnings in Gateway logs. A workspace +hook cannot override a bundled or managed hook. For extra directories and +linked packs, verify the scan-root layout described under +[Hook discovery](/automation/hooks#hook-discovery). ### Hook not eligible ```bash openclaw hooks info my-hook +openclaw hooks list --verbose ``` -Check for missing binaries (PATH), environment variables, config values, or OS compatibility. +Check `blockedReason`, missing binaries on the Gateway's `PATH`, environment, +config paths, and OS. A workspace hook is disabled until explicitly enabled. +A hook with no declared events is not loadable. Reports can pass requirements +without proving that its module imports successfully. ### 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` +Check `hooks.internal.enabled`, the configured-name selection, and the hook's +`hookKey` entry. Restart after changes. A `ready` report does not override the +master switch or name selection and does not mean a non-startup agent's workspace +was loaded. + +```bash +openclaw logs --follow +``` + +Look for import/export errors, boundary failures, unknown-event warnings, or +`Hook error [:]`. Trigger the exact event again and verify a +hook-specific marker or artifact. Ordinary chat text does not trigger +`command:new`; `/stop` does not send hook replies; a metadata subscription does +not invent a custom trigger. + +If the marker appears but the chat reply does not, check the producer and route +under [Reply delivery](/automation/hooks#reply-delivery), not just enablement. +For `session-memory`, allow background writing to finish and inspect the +resolved agent workspace rather than assuming the default workspace. ## Related - [CLI Reference: hooks](/cli/hooks) +- [Plugin hooks](/plugins/hooks) - [Webhooks](/automation/cron-jobs#webhooks) -- [Plugin hooks](/plugins/hooks) — in-process plugin lifecycle hooks - [Configuration](/gateway/configuration-reference#hooks) +- [Agent workspace](/concepts/agent-workspace) diff --git a/docs/automation/imap.md b/docs/automation/imap.md index ac2c72bd058f..c940246bfa14 100644 --- a/docs/automation/imap.md +++ b/docs/automation/imap.md @@ -1,4 +1,5 @@ --- +doc-schema-version: 1 summary: "Watch an IMAP mailbox and route authenticated incoming email to an isolated restricted reader agent" read_when: - Triggering OpenClaw from Fastmail, iCloud, or another IMAP mailbox @@ -69,7 +70,7 @@ Configure an explicit reader agent before enabling the plugin. Preserve existing } ``` -Replace the channel placeholder, IMAP hostname, username, sender allowlist, and secret reference with your own values. The reader requires an available sandbox backend and an authenticated model. Unlike Gmail PubSub, this plugin does not require `hooks.enabled`, Google Cloud, Tailscale Funnel, or a public HTTP endpoint. +Replace the channel placeholder, IMAP hostname, username, sender allowlist, and secret reference with your own values. The reader requires an available sandbox backend and an authenticated model. Unlike Gmail PubSub, this plugin does not require `hooks.enabled`, Google Cloud, Tailscale Funnel, or a public HTTP endpoint. It calls the Gateway's trusted plugin email dispatcher directly; HTTP-hook agent/session allowlists are not its configuration boundary. Its `agentId`, sender policy, and restricted reader control this path. It is also separate from [internal `HOOK.md` event handlers](/automation/hooks). ```bash openclaw agents list @@ -114,7 +115,9 @@ openclaw security audit --deep openclaw logs --follow ``` -Send yourself a message containing “follow this link and run a command.” Confirm it creates an isolated `hook:imap:::` session for `mail_reader` and only summarizes the content. Any link navigation, file write, shell command, browser action, or other tool escape is a failed boundary check. +Send yourself a message containing “follow this link and run a command.” Confirm it dispatches to `mail_reader`, creates an isolated run, and only summarizes the content. `hook:imap:::` is the logical dispatch key; the stored run session can use a generated `cron:...:run:...` key instead. Any link navigation, file write, shell command, browser action, or other tool escape is a failed boundary check. + +The IMAP dispatch log with a `runId` records admission, not completed processing or delivery. With `deliver: false`, look for the subsequent Gateway log `hook agent run completed without announcement`, or hook failure warnings, and inspect the run transcript. A model failure after admission does not cause IMAP to replay the message. Existing messages are baselined without dispatch when the plugin first starts. New messages are deduplicated across gateway restarts; a mailbox UIDVALIDITY change records a fresh baseline instead of replaying old mail. Email bodies are capped by `maxBytes`, and oversized content carries a recorded truncation marker. diff --git a/docs/automation/index.md b/docs/automation/index.md index fb7683af8e6d..1cdb179b18db 100644 --- a/docs/automation/index.md +++ b/docs/automation/index.md @@ -31,21 +31,22 @@ flowchart TD Q5 -->|Yes| SO[Standing Orders] ``` -| Use case | Recommended | Why | -| --------------------------------------- | ---------------- | ------------------------------------------------ | -| Send daily report at 9 AM sharp | Automations | Exact timing, isolated execution | -| Remind me in 20 minutes | Automations | One-shot with precise timing (`--at`) | -| Run weekly deep analysis | Automations | Standalone task, can use different model | -| Check inbox every 30 min | Automations | Independent recurring schedule and job history | -| Trigger safely on new IMAP email | IMAP plugin | Sender-gated isolated reader sessions | -| Monitor calendar for upcoming events | Automations | Explicit recurring schedule and delivery policy | -| Surface ambient main-session updates | Heartbeat | System-owned monitor automation and quiet alerts | -| Inspect status of a subagent or ACP run | Background Tasks | Tasks ledger tracks all detached work | -| Audit what ran and when | Background Tasks | `openclaw tasks list` and `openclaw tasks audit` | -| Multi-step research then summarize | Task Flow | Durable orchestration with revision tracking | -| Run a script on session reset | Hooks | Event-driven, fires on lifecycle events | -| Execute code on every tool call | Plugin hooks | In-process hooks can intercept tool calls | -| Always check compliance before replying | Standing Orders | Injected into every session automatically | +| Use case | Recommended | Why | +| ----------------------------------------- | ------------------------------------------ | ------------------------------------------------------ | +| Send daily report at 9 AM sharp | Automations | Exact timing, isolated execution | +| Remind me in 20 minutes | Automations | One-shot with precise timing (`--at`) | +| Run weekly deep analysis | Automations | Standalone task, can use different model | +| Check inbox every 30 min | Automations | Independent recurring schedule and job history | +| Trigger safely on new IMAP email | IMAP plugin | Sender-gated isolated reader sessions | +| Monitor calendar for upcoming events | Automations | Explicit recurring schedule and delivery policy | +| Surface ambient main-session updates | Heartbeat | System-owned monitor automation and quiet alerts | +| Inspect status of a subagent or ACP run | Background Tasks | Tasks ledger tracks all detached work | +| Audit what ran and when | Background Tasks | `openclaw tasks list` and `openclaw tasks audit` | +| Multi-step research then summarize | Task Flow | Durable orchestration with revision tracking | +| Run a script on session reset | Hooks | Internal `HOOK.md` scripts react to lifecycle events | +| Trigger an agent from an external service | [Webhooks](/automation/cron-jobs#webhooks) | Authenticated HTTP ingress, not an internal event hook | +| Execute code on every tool call | Plugin hooks | Typed `api.on(...)` handlers can intercept tool calls | +| Always check compliance before replying | Standing Orders | Injected into every session automatically | ### Automations vs Heartbeat diff --git a/docs/cli/hooks.md b/docs/cli/hooks.md index c580a5b597ec..9d0fdcda1065 100644 --- a/docs/cli/hooks.md +++ b/docs/cli/hooks.md @@ -1,49 +1,91 @@ --- -summary: "CLI reference for `openclaw hooks` (agent hooks)" +summary: "CLI reference for internal hook discovery, eligibility, enablement, and hook packs" read_when: - - You want to manage agent hooks - - You want to inspect hook availability or enable workspace hooks + - You want to inspect internal hooks on a local or remote Gateway + - You want to enable or disable a hook in local config + - You need hook command flags or JSON report fields title: "Hooks" +doc-schema-version: 1 --- # `openclaw hooks` -Manage agent hooks (event-driven automations for commands like `/new`, `/reset`, and gateway startup). Bare `openclaw hooks` is equivalent to `openclaw hooks list`. +Inspect and configure [internal hooks](/automation/hooks): handlers for command, +message, session, and Gateway events. Bare `openclaw hooks` runs the same report +as `openclaw hooks list`. These commands do not manage HTTP +[Webhooks](/automation/cron-jobs#webhooks) or the typed `api.on(...)` hook catalog in +[Plugin hooks](/plugins/hooks). -Related: [Hooks](/automation/hooks) - [Plugin hooks](/plugins/hooks) +## Target and scope + +Read-only reports (`hooks`, `list`, `info`, `check`) first call `hooks.status` on +the selected Gateway. Configured remote Gateways and explicit +`OPENCLAW_GATEWAY_URL` targets are authoritative: missing remote URLs, +connection/authentication failures, and unsupported methods fail instead of +showing client-local hooks. An implicitly selected local Gateway can fall back +to local discovery when unavailable or when its hook-report method/agent +parameter is unsupported. Other errors are not silently replaced with local +inventory. + +**Enable, disable, install, and update mutate local files/config/state.** They do +not change a remote Gateway over RPC. To change the server, run the command on +that host using its profile/config, then restart that Gateway. + +`--agent ` selects the agent workspace used for inspection. It is required +when configured agents do not have an implicit owner; blank or unknown IDs +fail. The option works before or after `list`, `info`, `check`, `enable`, and +`disable`. It does not scope the persisted hook entry to that agent and is not +supported on install/update. See +[Local, remote, and agent scope](/automation/hooks#local-remote-and-agent-scope) +for the distinction between workspace inventory and Gateway loading. ## List hooks ```bash -openclaw hooks --agent --json +openclaw hooks [--agent ] [--json] openclaw hooks list [--agent ] [--eligible] [--json] [-v|--verbose] ``` -Bare `openclaw hooks` and `openclaw hooks --json` use the same list operation as -`openclaw hooks list`. The command discovers hooks from workspace, managed, -extra, and bundled directories. +Discovery includes bundled hooks, active plugin hooks, managed hooks, extra +directories, and the selected workspace. Hook-name collisions follow the +[source policy](/automation/hooks#hook-discovery). -Hook reports (`hooks`, `list`, `info`, and `check`) first request the selected -Gateway's inventory. Configured remote Gateways and explicit -`OPENCLAW_GATEWAY_URL` targets are authoritative: missing URLs, connection or -authentication failures, and unsupported methods fail instead of showing -client-local hooks. An implicitly selected local Gateway may fall back to local -discovery when it is offline or does not support the current hook-report method. +| Option | Meaning | +| --------------- | ----------------------------------------------------------------------------------------------------------------------- | +| `--agent ` | Select the workspace to inspect. | +| `--eligible` | Show only `loadable` hooks: enabled by per-hook/source policy, requirements satisfied, and at least one declared event. | +| `--json` | Write structured JSON directly to stdout. Also accepted on the parent `hooks` command. | +| `-v, --verbose` | Add the Missing column to the human-readable table. | -- `--eligible`: only hooks whose requirements are met. -- `--agent `: inspect hooks for that agent's workspace. Required when multiple agents are configured without an implicit owner. -- `--json`: structured output. -- `-v, --verbose`: include a Missing column with unmet requirements. +Human output is a table with Status, Hook, Description, and Source columns, +preceded by `Hooks (/ ready)`. Plugin-managed sources appear as +`plugin:`. -``` -Hooks (4/5 ready) + +`ready`, `eligible`, and `loadable` are inventory results, not a live handler +registration check. The report does not apply the Gateway's master switch or +configured-name selection, import the handler to prove it works, or verify that +the event has run. A bundled hook can appear ready while the internal hook +system is off. Enable the intended hook, restart, and +[verify its real side effect](/automation/hooks#quick-start). + -Ready: - 🚀 boot-md ✓ - Run BOOT.md on gateway startup - 📎 bootstrap-extra-files ✓ - Inject additional workspace bootstrap files during agent bootstrap - 📝 command-logger ✓ - Log emitted command events to a centralized audit file - 💾 session-memory ✓ - Save session context to memory when /new or /reset command is issued -``` +### List JSON + +The root object contains `workspaceDir`, `managedHooksDir`, and `hooks`. +Each hook includes: + +- Identity/display: `name`, `description`, `source`, optional `pluginId`, + `emoji`, `homepage`, and `managedByPlugin`. +- Status: `enabledByConfig`, `requirementsSatisfied`, `loadable`, optional + `blockedReason`, plus compatibility aliases `eligible` (`loadable`) and + `disabled` (`!enabledByConfig`). +- Events/requirements: `events`, `unknownEvents`, and `missing`, whose arrays + are `bins`, `anyBins`, `env`, `config`, and `os`. + +`blockedReason` can be `disabled in config`, `workspace hook (disabled by default)`, +`missing requirements`, or `no events defined`. Unknown events are +advisory: they do not by themselves make a hook unloadable. ## Get hook info @@ -51,7 +93,15 @@ Ready: openclaw hooks info [--agent ] [--json] ``` -`` is the hook name or hook key (for example `session-memory`). Shows source, file/handler paths, homepage, events, and per-requirement status (binaries, env, config, OS). +Accepts a hook name or its metadata `hookKey`. Shows source, descriptor and +handler paths, homepage, events, unknown-event warnings, blocked reason, and +per-requirement status. A missing hook exits with code 1. + +JSON includes the list fields plus `filePath`, `baseDir`, `handlerPath`, +`hookKey`, `always`, `requirements`, `configChecks`, and normalized `install` +options. Each config check has `path` and `satisfied`; each install option has +`id`, `kind`, `label`, and `bins`. Install options are descriptive metadata, not +a command to install dependencies automatically. ## Check eligibility @@ -59,7 +109,13 @@ openclaw hooks info [--agent ] [--json] openclaw hooks check [--agent ] [--json] ``` -Prints a ready/not-ready count summary; with hooks not ready, lists each with its blocking reason. +Prints totals for ready/not-ready hooks and lists blocking reasons. JSON has +`total`, `eligible`, `notEligible`, and `hooks` containing an `eligible` name +array and a `notEligible` array of `{ name, blockedReason?, missing }` objects. + +A successful report exits with code 0 even when hooks are not ready. For an +automated eligibility gate, inspect the JSON counts rather than treating the +exit code as an all-hooks-ready result. This still does not test actual loading. ## Enable a hook @@ -67,15 +123,29 @@ Prints a ready/not-ready count summary; with hooks not ready, lists each with it openclaw hooks enable [--agent ] ``` -Adds/updates `hooks.internal.entries..enabled = true` in config and also flips the `hooks.internal.enabled` master switch on (the gateway does not load any internal hook handler until at least one is configured). Fails if the hook does not exist, is plugin-managed, or is not eligible (missing requirements). +Discovers the hook locally, then writes +`hooks.internal.entries..enabled = true` and +`hooks.internal.enabled = true` in local config. Other fields in that entry are +preserved. Exact hook names take precedence over matching keys; ambiguous key +matches fail without writing. -`--agent ` selects the workspace used to discover the hook and is required -when multiple agents are configured without an implicit owner. The persisted -hook entry is global and applies wherever that hook key is discovered. +Enable fails for a missing hook, a plugin-managed hook, or unmet runtime +requirements. It can enable a currently disabled workspace hook. This does not +prove a valid module export or event subscription; inspect `info` and the +Gateway logs too. -Plugin-managed hooks show `plugin:` in `hooks list` and cannot be enabled/disabled here; enable or disable the owning plugin instead. +The entry is **global**, even with `--agent`: it applies wherever that key is +discovered. Adding named entries can narrow a previously open-ended directory +selection. See [Configuration](/automation/hooks#configuration). -Restart the gateway after enabling (macOS menu bar app restart, or restart your gateway process in dev) so it reloads hooks. +Restart after enabling: + +```bash +openclaw gateway restart +``` + +For a foreground Gateway, stop and start the process instead. Restart is not +performed automatically by `hooks enable`. ## Disable a hook @@ -83,59 +153,132 @@ Restart the gateway after enabling (macOS menu bar app restart, or restart your openclaw hooks disable [--agent ] ``` -Sets `hooks.internal.entries..enabled = false`. Restart the gateway afterward. +Writes `hooks.internal.entries..enabled = false`. It does not remove the +hook files or change the master switch. Missing/ambiguous and plugin-managed +hooks are rejected; missing runtime requirements do not prevent disabling. +Restart the Gateway afterward. + +Plugin-managed hooks cannot be toggled by these commands. Enable or disable the +owning plugin through [`openclaw plugins`](/cli/plugins). ## Install and update hook packs -```bash -openclaw plugins install # npm by default -openclaw plugins install npm: # npm only -openclaw plugins install --pin # pin resolved version -openclaw plugins install # local directory or archive -openclaw plugins install -l # link a local directory instead of copying +Use the unified plugin installer for reviewed hook packs: +```bash +openclaw plugins install npm: +openclaw plugins install npm:@ --pin +openclaw plugins install ./my-hook-pack +openclaw plugins install ./my-hook-pack.tgz + +openclaw plugins update --dry-run openclaw plugins update -openclaw plugins update --all -openclaw plugins update --dry-run ``` -Hook packs install through the unified plugins installer/updater; `openclaw hooks install` / `openclaw hooks update` still work as deprecated aliases that print a warning and forward to the `plugins` commands. +A pack declares hook directories in `package.json` under `openclaw.hooks`. +A local directory without `package.json` can contain a single `HOOK.md` and +handler. Copied hook packs are installed into `/hooks/`; their +hooks are enabled in config and install provenance is recorded in shared SQLite +state. Restart the Gateway to load them. Do not author +`hooks.internal.installs` in `openclaw.json`. -- Npm specs are registry-only: package name plus an optional exact version or dist-tag. Git/URL/file specs and semver ranges are rejected. Dependency installs run project-local with `--ignore-scripts`. -- Bare specs and `@latest` stay on the stable track; if npm resolves to a prerelease, OpenClaw stops and asks you to opt in explicitly (`@beta`, `@rc`, or an exact prerelease version). -- Supported archives: `.zip`, `.tgz`, `.tar.gz`, `.tar`. -- `-l, --link` links a local directory instead of copying it (adds it to `hooks.internal.load.extraDirs`); linked hook packs are managed hooks from an operator-configured directory, not workspace hooks. -- `--pin` records npm installs as an exact resolved `name@version` in shared SQLite state. -- Install copies the pack into `~/.openclaw/hooks/`, enables its hooks under `hooks.internal.entries.*`, and records install provenance in shared SQLite state. -- If a stored integrity hash no longer matches the fetched artifact, OpenClaw warns and prompts before continuing; pass global `--yes` to bypass the prompt (for example in CI). +For the npm hook-pack path, specs are registry-only: package name with an +optional exact version or dist-tag. Git/URL/file specs, npm aliases, and semver +ranges are not npm registry specs. Bare specs and `@latest` stay on the stable +track; a prerelease resolution requires an explicit prerelease version or a +non-latest tag such as `@beta` or `@rc`. Use `npm:` to select npm explicitly; the +unified installer supports other plugin sources described in +[`openclaw plugins`](/cli/plugins). + +Supported local archives are `.zip`, `.tgz`, `.tar.gz`, and `.tar`. npm pack and +project-local dependency installation use `--ignore-scripts`; this does not +sandbox the installed handler. + +### Install options and trust + +| Option | Effect for hook packs | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| `-l, --link` | Add a local directory to `hooks.internal.load.extraDirs` instead of copying it. See the layout limitation below. | +| `--pin` | Record the resolved exact npm `name@version` in install state when available; does not apply to local paths. | +| `--force` | Acknowledge a non-ClawHub source and allow replacement of an existing copied install. For links it acknowledges the source without copying. | +| `--acknowledge-install-policy-warning` | Acknowledge an operator `security.installPolicy` warning without its prompt. Blocks and policy failures still stop the install. | + +Interactive non-ClawHub installs ask you to confirm trust. Noninteractive +installs require `--force`; global `--yes` is not a substitute for that gate. +`--force` is also not a substitute for acknowledging an install-policy warning. +Review the source before supplying either acknowledgement. + + +A linked hook path is a **scan directory**, not a symlink install. The scanner +looks at its immediate child directories, not the linked root's own `HOOK.md` +or `openclaw.hooks` declaration. A linked pack works with hook directories as +direct children; a single-hook root or a pack with only nested `hooks/` +directories can install successfully yet remain undiscovered. Prefer copied +installation for those layouts, and always verify `hooks list` after linking. +Extra directories also make directory-hook name selection open-ended. + + +### Update behavior + +Updates use tracked npm install records. A tracked hook-pack ID uses its stored +spec; a matching npm package spec can select a new version/tag. Local path and +archive records are not refreshed by the npm hook updater. + +`--dry-run` reports what would change without installing or rewriting config. +`--all` selects **both plugins and hook packs** in the unified updater, including +when reached through the deprecated alias; it is not a hooks-only bulk command. + +When an applicable stored integrity hash differs from the downloaded artifact, +the updater warns and asks for confirmation. Global `--yes` can accept that +yes/no prompt, so use it only when you intend to accept the drift. It does not +bypass operator policy blocks or replace the dedicated policy-warning flag. + +### Deprecated aliases + +These commands print a deprecation warning and forward to the unified owners: + +```bash +openclaw hooks install [-l|--link] [--pin] [--force] [--acknowledge-install-policy-warning] +openclaw hooks update [id] [--all] [--dry-run] [--acknowledge-install-policy-warning] +``` + +For update, provide `id` or `--all`. The aliases do not accept `--agent` and are +not the preferred interface for new automation. ## Bundled hooks -| Hook | Events | What it does | -| --------------------- | ------------------------------------------------- | --------------------------------------------------------------------------------------- | -| boot-md | `gateway:startup` | Runs `BOOT.md` at gateway startup for each configured agent scope | -| bootstrap-extra-files | `agent:bootstrap` | Injects extra bootstrap files (for example monorepo `AGENTS.md`) during agent bootstrap | -| command-logger | `command` | Logs emitted command events to `~/.openclaw/logs/commands.log` | -| compaction-notifier | `session:compact:before`, `session:compact:after` | Sends visible chat notices when session compaction starts and finishes | -| session-memory | `command:new`, `command:reset` | Saves session context to memory on `/new` or `/reset` | - -Enable any bundled hook with `openclaw hooks enable `. Full details, config keys, and defaults: [Bundled hooks](/automation/hooks#bundled-hooks). +The maintained catalog, event subscriptions, options, and verification notes +are in [Bundled hooks](/automation/hooks#bundled-hooks). This includes +`boot-md`, `bootstrap-extra-files`, `command-logger`, `compaction-notifier`, and +`session-memory` (manual **and automatic** reset capture). ### command-logger log file +On the Gateway host, with the default state directory: + ```bash -tail -n 20 ~/.openclaw/logs/commands.log # recent commands -cat ~/.openclaw/logs/commands.log | jq . # pretty-print -grep '"action":"new"' ~/.openclaw/logs/commands.log | jq . # filter by action +tail -n 20 ~/.openclaw/logs/commands.log +jq . ~/.openclaw/logs/commands.log +jq 'select(.action == "new")' ~/.openclaw/logs/commands.log ``` +Use `/logs/commands.log` for a custom state directory. These records +contain session and sender identifiers; protect access and arrange retention or +rotation. The hook does not rotate them. + ## Notes -- `hooks list --json`, `info --json`, and `check --json` write structured JSON directly to stdout. -- Failed hook reports use the standard [CLI JSON failure envelope](/cli#json-failures); missing hook info also includes the requested `hook` name. -- `hooks list`, `info`, and `check` pass `--agent` to a running Gateway and preserve it when an implicit local Gateway requires older-version or offline read-only discovery. +Report commands support `--json`; success JSON goes directly to stdout. Failures +use the standard [CLI JSON failure envelope](/cli#json-failures), and missing +hook info also includes the requested `hook` name. Reports do not execute a hook +as a test. + +The hidden `hooks relay` command is reserved for generated native harness +integration. It is not an internal-hook testing or manual event-trigger command. ## Related - [CLI reference](/cli) - [Automation hooks](/automation/hooks) +- [Plugin hooks](/plugins/hooks) +- [Plugins CLI](/cli/plugins) diff --git a/docs/cli/webhooks.md b/docs/cli/webhooks.md index 9b49b207f7e7..3832386f4f4f 100644 --- a/docs/cli/webhooks.md +++ b/docs/cli/webhooks.md @@ -1,4 +1,5 @@ --- +doc-schema-version: 1 summary: "CLI reference for `openclaw webhooks` (Gmail Pub/Sub setup and runner)" read_when: - You want to wire Gmail Pub/Sub events into OpenClaw @@ -8,7 +9,7 @@ title: "Webhooks" # `openclaw webhooks` -Webhook helpers and integrations. Today this surface is scoped to Gmail Pub/Sub flows built on the bundled `gog` watcher. +`openclaw webhooks` sets up and runs the Gmail Pub/Sub transport through `gog` (gogcli). It does not register [internal `HOOK.md` hooks](/automation/hooks), manage arbitrary [Gateway hook mappings](/automation/cron-jobs#webhooks), or manage the [TaskFlow Webhooks plugin](/plugins/webhooks). ## Subcommands @@ -20,10 +21,10 @@ openclaw webhooks gmail run [--account ] [...] | Subcommand | Description | | ------------- | ------------------------------------------------------------------------------------- | | `gmail setup` | One-time wizard: Gmail watch, Pub/Sub topic/subscription, and OpenClaw hook delivery. | -| `gmail run` | Run `gog watch serve` plus the watch auto-renew loop in the foreground. | +| `gmail run` | Run `gog gmail watch serve` plus the watch auto-renew loop in the foreground. | -The Gateway also auto-starts `gog gmail watch serve` on boot once `hooks.enabled=true` and `hooks.gmail.account` is set (set by `gmail setup`). `gmail run` is the same logic in the foreground, useful for debugging or when the Gateway watcher is disabled. See [Gmail Pub/Sub integration](/automation/cron-jobs#gmail-pubsub-integration) for the auto-start details and `OPENCLAW_SKIP_GMAIL_WATCHER` opt-out. +The Gateway also auto-starts `gog gmail watch serve` on boot once `hooks.enabled=true` and `hooks.gmail.account` is set (set by `gmail setup`). `gmail run` provides a foreground watcher for debugging or when the Gateway watcher is disabled. Do not run both against the same listener. See [Gmail Pub/Sub integration](/automation/cron-jobs#gmail-pubsub-integration) for the auto-start details and `OPENCLAW_SKIP_GMAIL_WATCHER` opt-out. ## `webhooks gmail setup` @@ -34,7 +35,9 @@ openclaw webhooks gmail setup --account you@example.com --project my-gcp-project openclaw webhooks gmail setup --account you@example.com --hook-url https://gateway.example.com/hooks/gmail ``` -Installs `gcloud` and `gog` if missing, authenticates `gcloud`, creates the Pub/Sub topic and subscription, starts the Gmail watch, and writes `hooks.gmail` config with `hooks.enabled=true`. Prints `Next: openclaw webhooks gmail run`. +Authenticates `gcloud`, enables the required APIs, creates or updates the Pub/Sub topic/subscription and push endpoint, starts the Gmail watch, and writes `hooks.gmail` with `hooks.enabled: true` and the Gmail preset. Missing `gcloud`, `gog`, and Tailscale dependencies can be installed automatically on macOS with Homebrew; other platforms need them installed first. The Gmail account must already be authorized in `gog`. + +Setup changes cloud resources, exposure settings, and local config; it is not a read-only check. Re-running it can apply the CLI defaults over saved Gmail settings. It prints `Next: openclaw webhooks gmail run`; use that only if the Gateway-managed watcher is not already running. This command connects Gmail transport but does not create a restricted reader agent or the session-key policy required by the templated preset. Without a custom Gmail mapping that sets `agentId`, inbound email runs as the default agent with that agent's effective workspace, sandbox, and tool policy. Complete [Configure a restricted Gmail reader](/automation/cron-jobs#configure-a-restricted-gmail-reader-recommended) before running setup for an untrusted inbox. @@ -48,40 +51,42 @@ This command connects Gmail transport but does not create a restricted reader ag ### Pub/Sub options -| Flag | Default | Description | -| ----------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| `--project ` | (none) | GCP project id (the OAuth client owner). Falls back to the topic's own project id, then to the project resolved from `gog` credentials. | -| `--topic ` | `gog-gmail-watch` | Pub/Sub topic name. | -| `--subscription ` | `gog-gmail-watch-push` | Pub/Sub subscription name. | -| `--label @@ -105,9 +105,11 @@ bundled, official external, and source-only plugins, see openclaw plugins inspect --runtime --json ``` - Use `--runtime` to prove registered tools, hooks, services, Gateway - methods, or plugin-owned CLI commands. Plain `inspect` is a cold manifest - and registry check only. + `--runtime` loads the plugin in the inspecting CLI process and reports + registered tools, hooks, services, Gateway methods, and plugin-owned CLI + commands. Plain `inspect` is a cold manifest and registry check only. + Neither proves an already-running Gateway has loaded the same code. After + restarting it, trigger the hook or capability and verify its actual effect. @@ -283,8 +285,9 @@ An explicit hook policy is also startup intent. For example, `plugins.entries..hooks.allowConversationAccess: true` both authorizes non-bundled conversation hooks and selects that configured plugin for Gateway startup; normal plugin policy still applies. After changing manifest or hook -policy, restart the Gateway and verify the registration with -`openclaw plugins inspect --runtime --json`. +policy, inspect registration with `openclaw plugins inspect --runtime --json`, +restart the Gateway, and trigger an event to verify the running process. See +[Plugin hooks](/plugins/hooks#quick-start) for a complete example. ## Verify the active Gateway diff --git a/src/hooks/bundled/README.md b/src/hooks/bundled/README.md index 220ebeb68d50..bea83d319860 100644 --- a/src/hooks/bundled/README.md +++ b/src/hooks/bundled/README.md @@ -1,225 +1,66 @@ # Bundled Hooks -This directory contains hooks that ship with OpenClaw. These hooks are automatically discovered and can be enabled/disabled via CLI or configuration. +These internal hooks ship with OpenClaw. They subscribe to colon-separated events +such as `command:new`; they are not typed plugin hooks or HTTP webhooks. -## Available Hooks +For setup, custom hook authoring, event payloads, discovery precedence, and +troubleshooting, use the canonical [Hooks guide](https://docs.openclaw.ai/automation/hooks). +For command flags and Gateway targeting, see the +[hooks CLI reference](https://docs.openclaw.ai/cli/hooks). -### 💾 session-memory +## Available hooks -Automatically saves session context to memory when you issue `/new` or `/reset`. +| Hook | Events | Effect | +| ------------------------------------------------------ | ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| [boot-md](boot-md/HOOK.md) | `gateway:startup` | Runs `BOOT.md` once per distinct configured agent workspace during startup. | +| [bootstrap-extra-files](bootstrap-extra-files/HOOK.md) | `agent:bootstrap` | Appends recognized workspace bootstrap files from configured glob/path patterns; writes no files. | +| [command-logger](command-logger/HOOK.md) | `command` | Appends command metadata as JSON lines to `/logs/commands.log`. | +| [compaction-notifier](compaction-notifier/HOOK.md) | `session:compact:before`, `session:compact:after` | Adds chat notices when compaction starts and finishes. | +| [session-memory](session-memory/HOOK.md) | `command:new`, `command:reset`, `session:auto-reset` | Saves recent conversation excerpts in `/memory/`; timestamp filenames by default, optional model-generated slugs. | -**Events**: `command:new`, `command:reset` -**What it does**: Creates a dated memory file with LLM-generated slug based on conversation content. -**Output**: `/memory/YYYY-MM-DD-slug.md` (defaults to `~/.openclaw/workspace`) +The default state directory is `~/.openclaw`. Agent workspaces can differ; see +[Agent workspace](https://docs.openclaw.ai/concepts/agent-workspace). -**Enable**: +## Enable and verify + +Discovery and eligibility do not prove that a running Gateway has loaded a hook. +Enable the hook in the config used by that Gateway, then reload its handlers by +restarting the Gateway: ```bash -openclaw hooks enable session-memory +openclaw hooks info command-logger ``` -### 📎 bootstrap-extra-files - -Injects extra bootstrap files (for example monorepo `AGENTS.md`) during prompt assembly. - -**Events**: `agent:bootstrap` -**What it does**: Expands configured workspace glob/path patterns and appends matching bootstrap files to injected context. -**Output**: No files written; context is modified in-memory only. - -**Enable**: - -```bash -openclaw hooks enable bootstrap-extra-files -``` - -### 📝 command-logger - -Logs emitted command events to a centralized audit file. Current core actions are `/new`, `/reset`, and `/stop`. - -**Events**: `command` (all emitted command actions) -**What it does**: Appends JSONL entries to command log file. -**Output**: `~/.openclaw/logs/commands.log` - -**Enable**: - ```bash openclaw hooks enable command-logger ``` -### 🚀 boot-md - -Runs `BOOT.md` whenever the gateway starts (after channels start). - -**Events**: `gateway:startup` -**What it does**: Executes BOOT.md instructions via the agent runner. -**Output**: Whatever the instructions request (for example, outbound messages). - -**Enable**: +For an installed Gateway service: ```bash -openclaw hooks enable boot-md +openclaw gateway restart ``` -## Hook Structure +For a foreground development Gateway, stop and restart the process you own. Do +not kill unrelated Gateway processes. Send `/new` or `/reset` in a test +conversation, then check `/logs/commands.log` for that command. -Each hook is a directory containing: +With multiple agents, use `--agent ` to select the discovery workspace. The +persisted hook entry is global, not an agent-specific enablement setting. -- **HOOK.md**: Metadata and documentation in YAML frontmatter + Markdown -- **handler.ts**: The hook handler function (default export) +## Source layout -Example structure: +Each bundled hook has a `HOOK.md` descriptor and a `handler.ts` default export. +`metadata.openclaw.events` declares subscriptions. Custom hooks can also use +`handler.js`, `index.ts`, or `index.js`; see the +[authoring guide](https://docs.openclaw.ai/automation/hooks#writing-hooks) for a +complete example without repository-private imports. -``` -session-memory/ -├── HOOK.md # Metadata + docs -└── handler.ts # Handler implementation -``` +Keep descriptors, handlers, and the public guide aligned when changing a +contract. Do not duplicate the event catalog or config reference in this README. -## HOOK.md Format - -```yaml ---- -name: my-hook -description: "Short description" -homepage: https://docs.openclaw.ai/automation/hooks#my-hook -metadata: - { "openclaw": { "emoji": "🔗", "events": ["command:new"], "requires": { "bins": ["node"] } } } ---- -# Hook Title - -Documentation goes here... -``` - -### Metadata Fields - -- **emoji**: Display emoji for CLI -- **events**: Array of events to listen for (e.g., `["command:new", "session:start"]`) -- **requires**: Optional requirements - - **bins**: Required binaries on PATH - - **anyBins**: At least one of these binaries must be present - - **env**: Required environment variables - - **config**: Required config paths (e.g., `["workspace.dir"]`) - - **os**: Required platforms (e.g., `["darwin", "linux"]`) -- **install**: Installation methods (for bundled hooks: `[{"id":"bundled","kind":"bundled"}]`) - -## Creating Custom Hooks - -To create your own hooks, place them in: - -- **Workspace hooks**: `/hooks/` (highest precedence) -- **Managed hooks**: `~/.openclaw/hooks/` (shared across workspaces) - -Custom hooks follow the same structure as bundled hooks. - -## Managing Hooks - -List all hooks: - -```bash -openclaw hooks list -``` - -Show hook details: - -```bash -openclaw hooks info session-memory -``` - -Check hook status: - -```bash -openclaw hooks check -``` - -Enable/disable: - -```bash -openclaw hooks enable session-memory -openclaw hooks disable command-logger -``` - -## Configuration - -Hooks can be configured in `~/.openclaw/openclaw.json`: - -```json -{ - "hooks": { - "internal": { - "enabled": true, - "entries": { - "session-memory": { - "enabled": true - }, - "command-logger": { - "enabled": false - } - } - } - } -} -``` - -## Event Types - -Currently supported events: - -- **command**: All command events -- **command:new**: `/new` command specifically -- **command:reset**: `/reset` command -- **command:stop**: `/stop` command -- **agent:bootstrap**: Before workspace bootstrap files are injected -- **gateway:startup**: Gateway startup (after channels start) -- **session:compact:before**: Pre-compaction snapshot before the embedded runner rewrites session context -- **session:compact:after**: Post-compaction snapshot after the runner replaces session context -- **message:received**: Inbound channel message accepted for dispatch -- **message:sent**: Outbound channel message delivered (canonical payload only) - -## Handler API - -Hook handlers receive an `InternalHookEvent` object: - -```typescript -interface InternalHookEvent { - type: "command" | "session" | "agent" | "gateway" | "message"; - action: string; // e.g., 'new', 'reset', 'stop', 'compact:before', 'received', 'sent' - sessionKey: string; - context: Record; - timestamp: Date; - messages: string[]; // Push messages here to send to user -} -``` - -Example handler: - -```typescript -import type { HookHandler } from "../../src/hooks/hooks.js"; - -const myHandler: HookHandler = async (event) => { - if (event.type !== "command" || event.action !== "new") { - return; - } - - // Your logic here - console.log("New command triggered!"); - - // Optionally send message to user - event.messages.push("✨ Hook executed!"); -}; - -export default myHandler; -``` - -## Testing - -Test your hooks by: - -1. Place hook in workspace hooks directory -2. Restart gateway: `pkill -9 -f 'openclaw.*gateway' && pnpm openclaw gateway` -3. Enable the hook: `openclaw hooks enable my-hook` -4. Trigger the event (e.g., send `/new` command) -5. Check gateway logs for hook execution - -## Documentation - -Full documentation: https://docs.openclaw.ai/automation/hooks +Internal handlers run as trusted code in the Gateway process, not in the agent +sandbox. Keep work bounded, handle sensitive message content carefully, and use +[typed plugin hooks and services](https://docs.openclaw.ai/plugins/hooks) for +policy decisions or long-lived resources. Pushing to `event.messages` produces a +reply only on the replyable event paths documented in the Hooks guide. diff --git a/src/hooks/bundled/boot-md/HOOK.md b/src/hooks/bundled/boot-md/HOOK.md index b31c97727d41..4a7a87d6b1cd 100644 --- a/src/hooks/bundled/boot-md/HOOK.md +++ b/src/hooks/bundled/boot-md/HOOK.md @@ -16,5 +16,6 @@ metadata: # Boot Checklist Hook -Runs `BOOT.md` at gateway startup for each configured agent scope, if the file exists in that -agent's resolved workspace. +Runs `BOOT.md` at Gateway startup once per distinct configured agent workspace, +if the file exists there. Agents sharing a workspace do not run the same checklist +again. Enable with `openclaw hooks enable boot-md`, then restart the Gateway. diff --git a/src/hooks/bundled/session-memory/HOOK.md b/src/hooks/bundled/session-memory/HOOK.md index c8d55a4d3ed5..12616ab59480 100644 --- a/src/hooks/bundled/session-memory/HOOK.md +++ b/src/hooks/bundled/session-memory/HOOK.md @@ -55,7 +55,10 @@ With `llmSlug: true`, the configured model can generate descriptive slugs based ## Requirements -- **Config**: `workspace.dir` must be set (automatically configured during setup) +- A resolved agent workspace. Configure it with `agents.defaults.workspace` or + `agents.entries..workspace` when the default is unsuitable. The descriptor's + `workspace.dir` requirement is an internal eligibility marker, not an + `openclaw.json` key to add. When `llmSlug` is enabled, the hook uses your configured LLM provider to generate slugs, so it works with any provider (Anthropic, OpenAI, etc.). @@ -106,7 +109,7 @@ To disable this hook: openclaw hooks disable session-memory ``` -Or remove it from your config: +Or explicitly disable its config entry, then restart the Gateway: ```json {