mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
docs(hooks): clarify setup and execution contracts (#130734)
This commit is contained in:
committed by
GitHub
parent
a274416822
commit
7eebd6d4a6
+137
-74
@@ -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: "<long-random-hook-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 <long-random-hook-token>' \
|
||||
-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": "<hook-request-run-id>" }
|
||||
```
|
||||
|
||||
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:<uuid>` 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 <token>` (recommended)
|
||||
- `x-openclaw-token: <token>`
|
||||
- `Authorization: Bearer <token>` (recommended).
|
||||
- `x-openclaw-token: <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.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="POST /hooks/wake">
|
||||
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 <long-random-hook-token>' \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"text":"New email received","mode":"now","agentId":"main"}'
|
||||
--data '{"text":"The sample import completed","mode":"now","agentId":"main"}'
|
||||
```
|
||||
|
||||
<ParamField path="text" type="string" required>
|
||||
Event description.
|
||||
</ParamField>
|
||||
<ParamField path="mode" type="string" default="now">
|
||||
`now` or `next-heartbeat`.
|
||||
</ParamField>
|
||||
<ParamField path="agentId" type="string">
|
||||
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.
|
||||
</ParamField>
|
||||
<ParamField path="sessionKey" type="string">
|
||||
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.
|
||||
</ParamField>
|
||||
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.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="POST /hooks/agent">
|
||||
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.
|
||||
|
||||
</Accordion>
|
||||
<Accordion title="Mapped hooks (POST /hooks/<name>)">
|
||||
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: "<key>"` 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: "<key>"` 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.
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
<Warning>
|
||||
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.
|
||||
|
||||
<Warning>
|
||||
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.
|
||||
|
||||
</Warning>
|
||||
|
||||
## 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.
|
||||
|
||||
<Note>
|
||||
**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.
|
||||
</Note>
|
||||
|
||||
### 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 <url>` 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.
|
||||
|
||||
<Warning>
|
||||
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:<message-id>` 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.
|
||||
|
||||
<Steps>
|
||||
<Step title="Select the GCP project">
|
||||
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
|
||||
<Step title="Start the watch">
|
||||
```bash
|
||||
gog gmail watch start \
|
||||
--account openclaw@gmail.com \
|
||||
--account reader@example.com \
|
||||
--label INBOX \
|
||||
--topic projects/<project-id>/topics/gog-gmail-watch
|
||||
```
|
||||
|
||||
+635
-294
File diff suppressed because it is too large
Load Diff
@@ -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:<account>:<uidvalidity>:<uid>` 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:<account>:<uidvalidity>:<uid>` 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.
|
||||
|
||||
|
||||
+16
-15
@@ -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
|
||||
|
||||
|
||||
+210
-67
@@ -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 <id>` 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 <id> --json
|
||||
openclaw hooks [--agent <id>] [--json]
|
||||
openclaw hooks list [--agent <id>] [--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 <id>` | 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 <id>`: 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>/<total> ready)`. Plugin-managed sources appear as
|
||||
`plugin:<id>`.
|
||||
|
||||
```
|
||||
Hooks (4/5 ready)
|
||||
<Note>
|
||||
`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).
|
||||
</Note>
|
||||
|
||||
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 <name> [--agent <id>] [--json]
|
||||
```
|
||||
|
||||
`<name>` 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 <name> [--agent <id>] [--json]
|
||||
openclaw hooks check [--agent <id>] [--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 <name> [--agent <id>]
|
||||
```
|
||||
|
||||
Adds/updates `hooks.internal.entries.<name>.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.<hookKey>.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 <id>` 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:<id>` 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 <name> [--agent <id>]
|
||||
```
|
||||
|
||||
Sets `hooks.internal.entries.<name>.enabled = false`. Restart the gateway afterward.
|
||||
Writes `hooks.internal.entries.<hookKey>.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 <package> # npm by default
|
||||
openclaw plugins install npm:<package> # npm only
|
||||
openclaw plugins install <package> --pin # pin resolved version
|
||||
openclaw plugins install <path> # local directory or archive
|
||||
openclaw plugins install -l <path> # link a local directory instead of copying
|
||||
Use the unified plugin installer for reviewed hook packs:
|
||||
|
||||
```bash
|
||||
openclaw plugins install npm:<package>
|
||||
openclaw plugins install npm:<package>@<version> --pin
|
||||
openclaw plugins install ./my-hook-pack
|
||||
openclaw plugins install ./my-hook-pack.tgz
|
||||
|
||||
openclaw plugins update <id> --dry-run
|
||||
openclaw plugins update <id>
|
||||
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 `<stateDir>/hooks/<id>`; 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/<id>`, 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.
|
||||
|
||||
<Warning>
|
||||
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/<name>`
|
||||
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.
|
||||
</Warning>
|
||||
|
||||
### 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 <path-or-spec> [-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 <hook-name>`. 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 `<stateDir>/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)
|
||||
|
||||
+66
-40
@@ -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 <email>] [...]
|
||||
| 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. |
|
||||
|
||||
<Note>
|
||||
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.
|
||||
</Note>
|
||||
|
||||
## `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.
|
||||
|
||||
<Warning>
|
||||
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 <id>` | (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 <name>` | `gog-gmail-watch` | Pub/Sub topic name. |
|
||||
| `--subscription <name>` | `gog-gmail-watch-push` | Pub/Sub subscription name. |
|
||||
| `--label <label>` | `INBOX` | Gmail label to watch. |
|
||||
| `--push-endpoint <url>` | (none) | Explicit Pub/Sub push endpoint. Overrides Tailscale. |
|
||||
| Flag | Default | Description |
|
||||
| ----------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--project <id>` | (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 <name>` | `gog-gmail-watch` | Pub/Sub topic name. |
|
||||
| `--subscription <name>` | `gog-gmail-watch-push` | Pub/Sub subscription name. |
|
||||
| `--label <label>` | `INBOX` | Gmail label to watch. |
|
||||
| `--push-endpoint <url>` | (none) | Explicit Pub/Sub push endpoint. Skips Tailscale endpoint setup; use `--tailscale off` for externally managed exposure. The URL is used as supplied, including any required push token. |
|
||||
|
||||
### OpenClaw delivery options
|
||||
|
||||
| Flag | Default | Description |
|
||||
| ---------------------- | -------------------------------------------- | ------------------------------------------ |
|
||||
| `--hook-url <url>` | Built from `hooks.path` and the Gateway port | OpenClaw webhook URL. |
|
||||
| `--hook-token <token>` | `hooks.token`, or a generated token | OpenClaw webhook token. |
|
||||
| `--push-token <token>` | Generated token | Push token forwarded to `gog watch serve`. |
|
||||
| Flag | Default | Description |
|
||||
| ---------------------- | --------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `--hook-url <url>` | `hooks.gmail.hookUrl`, then local Gateway URL | OpenClaw webhook URL; generated fallback uses `hooks.path` and the Gateway port. |
|
||||
| `--hook-token <token>` | `hooks.token`, or a generated token | OpenClaw webhook token. |
|
||||
| `--push-token <token>` | `hooks.gmail.pushToken`, or a generated token | Separate token authenticating Pub/Sub to `gog gmail watch serve`. |
|
||||
|
||||
### `gog watch serve` options
|
||||
<a id="gog-watch-serve-options" />
|
||||
|
||||
| Flag | Default | Description |
|
||||
| --------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--bind <host>` | `127.0.0.1` | `gog watch serve` bind host. |
|
||||
| `--port <port>` | `8788` | `gog watch serve` port. |
|
||||
| `--path <path>` | `/gmail-pubsub` | `gog watch serve` path. Forced to `/` when Tailscale is enabled without an explicit target, since Tailscale strips the path before proxying. |
|
||||
| `--include-body` | `true` | Include email body snippets. There is no CLI flag to turn this off; set `hooks.gmail.includeBody: false` in config instead. |
|
||||
| `--max-bytes <n>` | `20000` | Max bytes per body snippet. |
|
||||
| `--renew-minutes <n>` | `720` (12h) | Renew Gmail watch every N minutes. |
|
||||
### `gog gmail watch serve` options
|
||||
|
||||
| Flag | Default | Description |
|
||||
| --------------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `--bind <host>` | `127.0.0.1` | `gog gmail watch serve` bind host. |
|
||||
| `--port <port>` | `8788` | `gog gmail watch serve` port. |
|
||||
| `--path <path>` | `/gmail-pubsub` | `gog gmail watch serve` path. Forced to `/` when Tailscale is enabled without an explicit target, since Tailscale strips the path before proxying. |
|
||||
| `--include-body` | `true` | Include email body snippets. There is no CLI flag to turn this off; set `hooks.gmail.includeBody: false` in config instead. |
|
||||
| `--max-bytes <n>` | `20000` | Max bytes per body snippet. |
|
||||
| `--renew-minutes <n>` | `720` (12h) | Renew Gmail watch every N minutes. |
|
||||
|
||||
### Tailscale exposure
|
||||
|
||||
| Flag | Default | Description |
|
||||
| ------------------------- | -------- | ---------------------------------------------------------------- |
|
||||
| `--tailscale <mode>` | `funnel` | Expose push endpoint via tailscale: `funnel`, `serve`, or `off`. |
|
||||
| `--tailscale-path <path>` | (none) | Path for tailscale serve/funnel. |
|
||||
| `--tailscale-target <t>` | (none) | Tailscale serve/funnel target (port, `host:port`, or URL). |
|
||||
| Flag | Default | Description |
|
||||
| ----------------------------- | -------------------------------------------------- | ---------------------------------------------------------------- |
|
||||
| `--tailscale <mode>` | `funnel` | Expose push endpoint via tailscale: `funnel`, `serve`, or `off`. |
|
||||
| `--tailscale-path <path>` | `hooks.gmail.tailscale.path`, then serve path | Path for tailscale serve/funnel. |
|
||||
| `--tailscale-target <target>` | `hooks.gmail.tailscale.target`, then local watcher | Tailscale serve/funnel target (port, `host:port`, or URL). |
|
||||
|
||||
### Output
|
||||
|
||||
@@ -89,33 +94,54 @@ This command connects Gmail transport but does not create a restricted reader ag
|
||||
| -------- | ------------------------------------------------- |
|
||||
| `--json` | Print a machine-readable summary instead of text. |
|
||||
|
||||
<Warning>Setup output is sensitive: `--json` includes `hookToken` and `pushToken`, and the push endpoint printed in either format can contain its token. Redact output before sharing it.</Warning>
|
||||
|
||||
`--port`, `--max-bytes`, and `--renew-minutes` require positive integers, without unit suffixes. `--include-body` has no negative CLI flag: set `hooks.gmail.includeBody: false` and let `run` inherit it.
|
||||
|
||||
## `webhooks gmail run`
|
||||
|
||||
```bash
|
||||
openclaw webhooks gmail run --account you@example.com
|
||||
```
|
||||
|
||||
Runs `gog watch serve` plus the watch auto-renew loop in the foreground, restarting `gog watch serve` after a 2s delay if it exits unexpectedly.
|
||||
Starts the Gmail watch and runs `gog gmail watch serve` plus periodic watch renewal in the foreground. An unexpected exit of the initial serve process schedules a restart after 2 seconds. Stop with Ctrl-C; investigate repeated exits in the logs.
|
||||
|
||||
`run` accepts the same Pub/Sub, OpenClaw delivery, `gog watch serve`, and Tailscale flags as `setup`, except:
|
||||
`run` accepts the same Pub/Sub, OpenClaw delivery, `gog gmail watch serve`, and Tailscale flags as `setup`, except:
|
||||
|
||||
- `--account` is **optional** on `run`; it falls back to `hooks.gmail.account`.
|
||||
- `run` does **not** accept `--project`, `--push-endpoint`, or `--json`.
|
||||
- Every flag falls back to the matching `hooks.gmail.*` config value (written by `setup`), then to the same built-in default `setup` uses, with one exception: `--tailscale` defaults to `off` on `run` (not `funnel`) when neither the flag nor `hooks.gmail.tailscale.mode` is set.
|
||||
- Unspecified flags inherit the matching `hooks.gmail.*` setting; `--hook-token` inherits `hooks.token`.
|
||||
- Account, full topic path, hook token, and push token must be supplied or configured. `run` does not generate missing tokens, provision Pub/Sub resources, or rewrite config.
|
||||
- Other fields use the setup defaults when no saved setting exists, except `--tailscale`, which defaults to `off` rather than `funnel`.
|
||||
|
||||
| Category | Flags |
|
||||
| ----------------- | -------------------------------------------------------------------------------- |
|
||||
| Pub/Sub | `--account`, `--topic`, `--subscription`, `--label` |
|
||||
| OpenClaw delivery | `--hook-url`, `--hook-token`, `--push-token` |
|
||||
| `gog watch serve` | `--bind`, `--port`, `--path`, `--include-body`, `--max-bytes`, `--renew-minutes` |
|
||||
| Tailscale | `--tailscale`, `--tailscale-path`, `--tailscale-target` |
|
||||
| Category | Flags |
|
||||
| ----------------------- | -------------------------------------------------------------------------------- |
|
||||
| Pub/Sub | `--account`, `--topic`, `--subscription`, `--label` |
|
||||
| OpenClaw delivery | `--hook-url`, `--hook-token`, `--push-token` |
|
||||
| `gog gmail watch serve` | `--bind`, `--port`, `--path`, `--include-body`, `--max-bytes`, `--renew-minutes` |
|
||||
| Tailscale | `--tailscale`, `--tailscale-path`, `--tailscale-target` |
|
||||
|
||||
<Note>
|
||||
For `run`, the `--topic` value is the full Pub/Sub topic path (`projects/.../topics/...`), not just the short topic name.
|
||||
</Note>
|
||||
|
||||
## Verify forwarding
|
||||
|
||||
```bash
|
||||
openclaw config validate
|
||||
openclaw logs --follow
|
||||
```
|
||||
|
||||
Send a test from another account to the watched inbox. The watcher excludes
|
||||
`SPAM`, `TRASH`, `DRAFT`, and `SENT` messages. Check watcher forwarding errors,
|
||||
then the Gateway hook completion/error logs and the reader's run output.
|
||||
A successful push or HTTP admission response does not prove email processing or
|
||||
channel delivery completed. Follow the [reader boundary
|
||||
check](/automation/cron-jobs#verify-the-reader-boundary) before connecting an
|
||||
untrusted inbox.
|
||||
|
||||
## Related
|
||||
|
||||
- [CLI reference](/cli)
|
||||
- [Webhook automation](/automation/webhook)
|
||||
- [Webhook automation](/automation/cron-jobs#webhooks)
|
||||
- [Gmail Pub/Sub integration](/automation/cron-jobs#gmail-pubsub-integration)
|
||||
|
||||
@@ -42,15 +42,17 @@ System prompt is built from OpenClaw's base prompt, skills prompt, bootstrap con
|
||||
|
||||
## Hooks
|
||||
|
||||
OpenClaw has two hook systems:
|
||||
OpenClaw has two in-process hook systems:
|
||||
|
||||
- **Internal hooks** (Gateway hooks): event-driven scripts for commands and lifecycle events.
|
||||
- **Plugin hooks**: extension points inside the agent/tool lifecycle and gateway pipeline.
|
||||
- **Internal hooks**: `HOOK.md` scripts for command and lifecycle events such as `command:new`.
|
||||
- **Plugin hooks**: typed `api.on(...)` handlers inside the agent/tool lifecycle and Gateway pipeline, such as `before_tool_call`.
|
||||
|
||||
[HTTP webhooks](/automation/cron-jobs#webhooks) are separate: they accept external requests that trigger work, rather than subscribing to agent-loop events.
|
||||
|
||||
### Internal hooks (Gateway hooks)
|
||||
|
||||
- **`agent:bootstrap`**: runs while building bootstrap files before the system prompt is finalized. Use it to add or remove bootstrap context files.
|
||||
- **Command hooks**: `/new`, `/reset`, `/stop`, and other command events (see the Hooks doc).
|
||||
- **Command hooks**: core emits `command:new`, `command:reset`, and `command:stop`. Other command names do not automatically become hook events.
|
||||
|
||||
See [Hooks](/automation/hooks) for setup and examples.
|
||||
|
||||
@@ -64,7 +66,7 @@ These run inside the agent loop or gateway pipeline:
|
||||
| `before_prompt_build` | After session load (with `messages`), to inject `prependContext`, `systemPrompt`, `prependSystemContext`, or `appendSystemContext`, or, on supported runtimes with a turn-scoped submitted tool surface, narrow it with `toolsAllow`. An empty `toolsAllow` submits no optional tools; omitted leaves the host-resolved surface unchanged. Unsupported runtimes reject restrictive values instead of ignoring them. |
|
||||
| `before_agent_reply` | After inline actions, before the LLM call. Lets a plugin claim the turn and return a synthetic reply or silence it entirely. |
|
||||
| `agent_end` | After completion, with the final message list and run metadata. |
|
||||
| `before_compaction` / `after_compaction` | Observe or annotate compaction cycles. |
|
||||
| `before_compaction` / `after_compaction` | Observe compaction cycles; these hooks do not rewrite or veto compaction. |
|
||||
| `before_tool_call` / `after_tool_call` | Intercept tool params/results. |
|
||||
| `before_install` | After operator install policy runs, on staged skill/plugin install material, when plugin hooks are loaded in the current process. |
|
||||
| `tool_result_persist` | Synchronously transforms tool results before they are written to an OpenClaw-owned session transcript. |
|
||||
|
||||
@@ -81,7 +81,7 @@ Standard files OpenClaw expects inside the workspace:
|
||||
The `## Tools` section holds local environment notes and conventions. It does not control tool availability; it is only guidance.
|
||||
</Accordion>
|
||||
<Accordion title="BOOT.md - startup checklist">
|
||||
Optional startup checklist run automatically on gateway restart (when [internal hooks](/automation/hooks) are enabled). Keep it short; use the message tool for outbound sends.
|
||||
Optional startup checklist run on Gateway startup when the [boot-md hook](/automation/hooks#boot-md) is enabled. Enabling a different internal hook does not enable `boot-md`. Keep it short; use the message tool for outbound sends.
|
||||
</Accordion>
|
||||
<Accordion title="BOOTSTRAP.md - first-run ritual">
|
||||
One-time first-run ritual. Only created for a brand-new workspace. Delete it after the ritual is complete.
|
||||
|
||||
@@ -226,4 +226,5 @@ For advanced configuration (reserve tokens, identifier preservation, custom cont
|
||||
- [Session](/concepts/session): session management and lifecycle.
|
||||
- [Session pruning](/concepts/session-pruning): trimming tool results.
|
||||
- [Context](/concepts/context): how context is built for agent turns.
|
||||
- [Hooks](/automation/hooks): compaction lifecycle hooks (`before_compaction`, `after_compaction`).
|
||||
- [Hooks](/automation/hooks#event-types): internal compaction events (`session:compact:before`, `session:compact:after`).
|
||||
- [Plugin hooks](/plugins/hooks#hook-catalog): typed compaction hooks (`before_compaction`, `after_compaction`).
|
||||
|
||||
@@ -187,6 +187,8 @@ providers may also include thinking configuration in their cache identity, so
|
||||
changing only the thinking level can increase latency and input-token cost even
|
||||
when the model itself stays the same.
|
||||
|
||||
<a id="model-in-chat" />
|
||||
|
||||
## `/model` in chat
|
||||
|
||||
`/model <model>` changes the current session. Use `-s` for only this session, `-a` to also update the agent's default, or `-g` to also update the shared global default. The long forms are `--session`, `--agent`, and `--global`. Configured-default writes require owner or admin authority.
|
||||
|
||||
@@ -445,6 +445,8 @@ date context. Falls back to the host timezone.
|
||||
- Config writers that mutate these fields (for example `/models set`, `/models set-image`, and fallback add/remove commands) save canonical object form and preserve existing fallback lists when possible.
|
||||
- `maxConcurrent`: max parallel agent runs across sessions (each session still serialized). By default, OpenClaw uses `min(16, max(8, available CPU parallelism))`, based on `os.availableParallelism()` with `os.cpus().length` as a fallback.
|
||||
|
||||
<a id="agentsdefaultsmodelselectionscope" />
|
||||
|
||||
### `agents.defaults.modelSelectionScope`
|
||||
|
||||
Optional scope for chat commands and Gateway session model updates without an explicit scope.
|
||||
@@ -1009,6 +1011,8 @@ scripts/sandbox-browser-setup.sh # optional browser image
|
||||
|
||||
For npm installs without a source checkout, see [Sandboxing § Images and setup](/gateway/sandboxing#images-and-setup) for inline `docker build` commands.
|
||||
|
||||
<a id="agentsentries-per-agent-overrides" />
|
||||
|
||||
### `agents.entries` (per-agent overrides)
|
||||
|
||||
Use `agents.entries.*.tts` to give an agent its own TTS provider, voice, model,
|
||||
|
||||
@@ -544,7 +544,7 @@ See [Plugins](/tools/plugin).
|
||||
}
|
||||
```
|
||||
|
||||
Agent display names, emoji, and avatars belong to each agent's `identity` block under `agents.list`; see [Agent configuration](/gateway/config-agents#agentslist-per-agent-overrides).
|
||||
Agent display names, emoji, and avatars belong to each agent's `identity` block under `agents.entries`; see [Agent configuration](/gateway/config-agents#agentsentries-per-agent-overrides).
|
||||
|
||||
- `seamColor`: operator accent color for native app UI chrome (Talk Mode bubble
|
||||
tint, etc.). The Control UI user accent (`ui.prefs.accent`) takes precedence in
|
||||
@@ -1067,110 +1067,261 @@ Profile changes require a Gateway restart. With the default `gateway.reload.mode
|
||||
|
||||
## Hooks
|
||||
|
||||
`hooks.*` configures generic Gateway HTTP ingress. For setup and a verified first
|
||||
request, see [Webhooks](/automation/cron-jobs#webhooks). This is separate from
|
||||
[internal hooks](/automation/hooks) (`hooks.internal`, `HOOK.md`) and the
|
||||
[TaskFlow Webhooks plugin](/plugins/webhooks) (`plugins.entries.webhooks`).
|
||||
|
||||
```json5
|
||||
{
|
||||
hooks: {
|
||||
enabled: true,
|
||||
token: "shared-secret",
|
||||
token: "<long-random-hook-token>",
|
||||
path: "/hooks",
|
||||
defaultSessionKey: "hook:ingress",
|
||||
allowRequestSessionKey: true,
|
||||
allowedSessionKeyPrefixes: ["hook:", "hook:gmail:"],
|
||||
allowedAgentIds: ["hooks", "main"],
|
||||
presets: ["gmail"],
|
||||
transformsDir: "~/.openclaw/hooks/transforms",
|
||||
mappings: [
|
||||
{
|
||||
match: { path: "gmail" },
|
||||
action: "agent",
|
||||
// Configure this agent under agents.entries with a restricted tool
|
||||
// profile and sandbox before routing untrusted content to it.
|
||||
agentId: "hooks",
|
||||
wakeMode: "now",
|
||||
name: "Gmail",
|
||||
// One dispatch per pushed email; templates see the current message.
|
||||
forEach: "messages",
|
||||
sessionKey: "hook:gmail:{{messages[0].id}}",
|
||||
sessionMode: "persistent",
|
||||
messageTemplate: "From: {{messages[0].from}}\nSubject: {{messages[0].subject}}\n{{messages[0].snippet}}",
|
||||
deliver: true,
|
||||
channel: "last",
|
||||
model: "openai/gpt-5.6-sol",
|
||||
},
|
||||
],
|
||||
allowedAgentIds: ["main"],
|
||||
allowRequestSessionKey: false,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Auth: `Authorization: Bearer <token>` or `x-openclaw-token: <token>`.
|
||||
Query-string hook tokens are rejected.
|
||||
Replace `main` with the intended configured agent. Hook tokens grant ingress
|
||||
access, not an authenticated sender identity; treat payload content as untrusted
|
||||
data and restrict the target agent's tools and workspace separately.
|
||||
|
||||
Validation and safety notes:
|
||||
| Field | Default | Contract |
|
||||
| --------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `enabled` | `false` | Enable the HTTP endpoints. Requires a nonempty `token`. |
|
||||
| `token` | unset | Shared hook secret string. Use a dedicated long random value; SecretRef objects are not supported here. |
|
||||
| `path` | `/hooks` | Dedicated base path; a leading slash is added and trailing slashes removed. `/` is rejected. |
|
||||
| `allowedAgentIds` | unrestricted | Effective agent allowlist, including the default-agent path. Omitted or containing `"*"` allows all; `[]` denies all. |
|
||||
| `defaultSessionKey` | unset | Logical agent-run key when no request/mapping key is supplied; otherwise a fresh `hook:<uuid>` is generated. Does not itself enable persistent sessions. |
|
||||
| `allowRequestSessionKey` | `false` | Allow keys from `/agent`, `/wake`, and payload-derived mapping/transform values. |
|
||||
| `allowedSessionKeyPrefixes` | unrestricted | Case-insensitive prefixes for explicit request/mapping keys and the default/generated key. An empty list or all-blank list imposes no restriction; blank entries are otherwise ignored. See session policy below. |
|
||||
| `presets` | `[]` | Built-in mappings appended after custom mappings. Available preset: `"gmail"`; unknown names add no mappings. |
|
||||
| `mappings` | `[]` | Ordered mapping list; first match wins. See [Mapping details](/gateway/configuration-reference#mapping-details). |
|
||||
| `transformsDir` | `<config-dir>/hooks/transforms` | Transform directory, constrained to that root, including symlink containment. Normally `~/.openclaw/hooks/transforms`. |
|
||||
| `gmail` | unset | Gmail transport and processing defaults; see [Gmail integration](/gateway/configuration-reference#gmail-integration). |
|
||||
| `internal` | separate subsystem | Internal event-hook configuration; see [Hooks](/automation/hooks). It does not enable HTTP ingress. |
|
||||
|
||||
- `hooks.enabled=true` requires a non-empty `hooks.token`.
|
||||
- `hooks.token` should be distinct from active Gateway shared-secret auth (`gateway.auth.token` / `OPENCLAW_GATEWAY_TOKEN` or `gateway.auth.password` / `OPENCLAW_GATEWAY_PASSWORD`); startup logs a non-fatal security warning when it detects reuse.
|
||||
- `openclaw security audit` flags hook/Gateway auth reuse as a critical finding, including Gateway password auth supplied only at audit time (`--auth password --password <password>`). Run `openclaw doctor --fix` to rotate a persisted reused `hooks.token`, then update external hook senders to use the new hook token.
|
||||
- `hooks.path` cannot be `/`; use a dedicated subpath such as `/hooks`.
|
||||
- If `hooks.allowRequestSessionKey=true`, constrain `hooks.allowedSessionKeyPrefixes` (for example `["hook:"]`).
|
||||
- If a mapping or preset uses a templated `sessionKey`, set `hooks.allowedSessionKeyPrefixes` and `hooks.allowRequestSessionKey=true`. Static mapping keys do not require that opt-in.
|
||||
`hooks.token` should be distinct from active Gateway shared-secret auth
|
||||
(`gateway.auth.token` / `OPENCLAW_GATEWAY_TOKEN` or `gateway.auth.password` /
|
||||
`OPENCLAW_GATEWAY_PASSWORD`). Startup logs a non-fatal warning on reuse;
|
||||
`openclaw security audit` reports a critical finding, including password auth
|
||||
supplied at audit time (`--auth password --password <password>`). Use
|
||||
`openclaw doctor --fix` to rotate a persisted reused hook token, then update all
|
||||
external senders.
|
||||
|
||||
**Endpoints:**
|
||||
### Hook HTTP contract
|
||||
|
||||
- `POST /hooks/wake` → `{ text, mode?: "now"|"next-heartbeat", agentId?, sessionKey? }`
|
||||
- `sessionKey` requires `mode: "now"`, is accepted only when `hooks.allowRequestSessionKey=true` (default: `false`), and must match `hooks.allowedSessionKeyPrefixes` when configured.
|
||||
- A supplied `agentId` must name a configured agent.
|
||||
- `POST /hooks/agent` → `{ message, name?, agentId?, sessionKey?, sessionMode?, wakeMode?, deliver?, channel?, to?, accountId?, model?, thinking?, timeoutSeconds? }`
|
||||
- A supplied `agentId` must name a configured agent.
|
||||
- `sessionKey` from request payload is accepted only when `hooks.allowRequestSessionKey=true` (default: `false`).
|
||||
- `sessionMode` is `"isolated"` by default. `"persistent"` reuses the resolved session and requires an explicit request `sessionKey`, `hooks.allowRequestSessionKey=true`, and non-empty `hooks.allowedSessionKeyPrefixes`.
|
||||
- Direct announce delivery requires both a concrete `channel` and `to`; supplying only one fails before the run is scheduled.
|
||||
- `accountId` selects a configured, enabled account for direct announce delivery and requires both `channel` and `to`; invalid selections return `400` before a run starts.
|
||||
- Omit both delivery fields for completion-only hooks, or set `deliver: false` to ignore supplied destination data.
|
||||
- The request waits up to 15 seconds for canonical session/global placement admission, not run completion. `200` means the execution path acquired that admission; the run may still be preparing its model runtime.
|
||||
- Pre-admission failures return `{ ok: false, error, runId }`: `400` for invalid delivery coordinates or account selection, `409` for session admission conflicts, `502` for other preparation failures, and `503` when placement admission does not occur within 15 seconds. Timed-out queued work is canceled and will not start later.
|
||||
- `POST /hooks/<name>` → resolved via `hooks.mappings`
|
||||
- Template-rendered mapping `sessionKey` values are treated as externally supplied and also require `hooks.allowRequestSessionKey=true`.
|
||||
- Mapped `agent` actions use the same admission wait and `200`/`400`/`409`/`502`/`503` outcomes.
|
||||
Paths below assume `hooks.path: "/hooks"`; replace that prefix if configured
|
||||
differently. Send `POST` with a JSON body and
|
||||
`Content-Type: application/json`.
|
||||
|
||||
<Accordion title="Mapping details">
|
||||
Authentication accepts `Authorization: Bearer <token>` or `x-openclaw-token`.
|
||||
A nonempty Bearer token takes precedence. A `token` query parameter is rejected
|
||||
with `400`, even if a valid header is also present. Missing or wrong credentials
|
||||
return `401`. After 20 failed attempts in a 60-second window, further invalid
|
||||
authentication attempts from that client are throttled with `429` and
|
||||
`Retry-After`; valid authentication resets the counter. Loopback is not exempt.
|
||||
Configure trusted proxy attribution correctly before exposing a proxy route.
|
||||
|
||||
- `match.path` matches sub-path after `/hooks` (e.g. `/hooks/gmail` → `gmail`).
|
||||
- `match.source` matches a payload field for generic paths.
|
||||
- Templates like `{{messages[0].subject}}` read from the payload.
|
||||
- `forEach: "<key>"` fans the mapping out over a top-level payload array: one action per element, with templates/transforms seeing a payload whose array holds only that element. The Gmail preset sets `forEach: "messages"` so batched pushes dispatch one run per email.
|
||||
- `transform` can point to a JS/TS module returning a hook action.
|
||||
- `transform.module` must be a relative path and stays within `hooks.transformsDir` (absolute paths and traversal are rejected).
|
||||
- Keep `hooks.transformsDir` under `~/.openclaw/hooks/transforms`; workspace skill directories are rejected. If `openclaw doctor` reports this path as invalid, move the transform module into the hooks transforms directory or remove `hooks.transformsDir`.
|
||||
- Mapping `agentId` routes to a specific agent; unknown mapping IDs retain the legacy fallback to the default agent. Direct `/hooks/wake` and `/hooks/agent` request IDs must name a configured agent.
|
||||
- `allowedAgentIds`: restricts effective agent routing, including the default-agent path when `agentId` is omitted (`*` or omitted = allow all, `[]` = deny all).
|
||||
- `defaultSessionKey`: optional fixed session key for hook agent runs without explicit `sessionKey`.
|
||||
- `allowRequestSessionKey`: allow `/hooks/wake` and `/hooks/agent` callers, plus template-driven mappings, to set `sessionKey` (default: `false`).
|
||||
- `allowedSessionKeyPrefixes`: optional prefix allowlist for explicit `sessionKey` values (request + mapping), e.g. `["hook:"]`. It becomes required when any mapping or preset uses a templated `sessionKey`.
|
||||
- `sessionMode`: mapping session behavior (`"isolated"` by default or `"persistent"`). Persistent mappings must resolve a stable key from `sessionKey` or `hooks.defaultSessionKey`; template-derived keys retain the request-key and prefix checks.
|
||||
- `deliver: true` sends the final reply to a channel; mapped hooks may use `channel: "last"`.
|
||||
- `deliver: false` keeps the mapped run completion-only.
|
||||
- `model` overrides LLM for this hook run (must be allowed if model catalog is set).
|
||||
The normal body limit is **256 KiB**, with a **30-second** body-read timeout.
|
||||
Gmail-path mappings receive a larger derived allowance described below. Generic
|
||||
hooks parse JSON but do not require the JSON content-type header; the TaskFlow
|
||||
plugin does enforce it.
|
||||
|
||||
</Accordion>
|
||||
| Endpoint | Payload and result |
|
||||
| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `POST /hooks/wake` | Required nonempty `text`; optional `mode` (`"now"` default or `"next-heartbeat"`), `agentId`, `sessionKey`. Returns `200 { ok: true, mode }` after enqueueing the system event. `now` also requests a heartbeat; neither result proves the heartbeat ran. |
|
||||
| `POST /hooks/agent` | [Agent payload](/gateway/configuration-reference#hook-agent-payload). Returns `200 { ok: true, runId }` after session/global placement admission, not completion. |
|
||||
| `POST /hooks/<name>` | First matching mapping produces wake/agent actions. No matching mapping returns `404`; no actions returns `204`. Agent fan-out has the [batch response contract](/gateway/configuration-reference#hook-retries-and-fan-out). |
|
||||
|
||||
The direct `/wake` and `/agent` endpoints take precedence over mappings with
|
||||
those names. `/hooks` itself has no action.
|
||||
|
||||
| Status | Meaning |
|
||||
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `400` | Invalid JSON, payload, routing/session policy, or delivery/account selection. Read the `error` before retrying. |
|
||||
| `401` | Hook authentication failed. |
|
||||
| `404` | No hook action or mapping at that path. Disabled hooks fall through to the rest of Gateway routing. |
|
||||
| `405` | Wrong method; `Allow: POST` is returned. |
|
||||
| `408` | Request body timeout. |
|
||||
| `413` | Body exceeds the path's byte limit. |
|
||||
| `429` | Failed-authentication throttling; honor `Retry-After`. |
|
||||
| `409` | Agent admission rejected because the target session changed or cannot accept work. |
|
||||
| `500` | Mapping/transform exception (`hook mapping failed`); inspect Gateway logs. |
|
||||
| `502` | Agent preparation failed before admission. |
|
||||
| `503` | Single-run admission did not occur within 15 seconds; that queued work is canceled. Fan-out pending work is different: it continues in the background. Gateway suspension/restart can also return `503 gateway_unavailable`. |
|
||||
|
||||
Agent admission failures use `{ ok: false, error, runId? }`. Early method/auth/path
|
||||
failures can be plain text; do not assume every error response is JSON. The
|
||||
15-second admission deadline is separate from the body-read timeout and
|
||||
`timeoutSeconds` for the agent turn. HTTP success does not prove a model result
|
||||
or channel delivery. See [hook verification](/automation/cron-jobs#verify-and-troubleshoot-hook-requests).
|
||||
|
||||
### Hook agent payload
|
||||
|
||||
| Field | Default | Contract |
|
||||
| ---------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `message` | required | Nonempty agent input text; external content is safety-wrapped. |
|
||||
| `name` | `"Hook"` | Hook label used in logs/completion events. |
|
||||
| `agentId` | resolved owner | Must name a configured agent when supplied directly. Required when no implicit/retained owner can be resolved. |
|
||||
| `sessionKey` | default/generated key | Subject to caller-key opt-in and prefix policy. |
|
||||
| `sessionMode` | `"isolated"` | `"isolated"` creates a fresh run session; `"persistent"` reuses the resolved session. |
|
||||
| `idempotencyKey` | unset | Optional replay key; headers take precedence. See retries below. |
|
||||
| `wakeMode` | `"now"` | `"now"` or `"next-heartbeat"`; controls waking for completion events, not whether the agent is dispatched immediately. |
|
||||
| `deliver` | `true` | Only `false` opts out. With no direct destination, successful output can become a main-session completion event. `false` logs successful completion without an announcement and ignores destination fields. Non-ok execution results produce a status event. |
|
||||
| `channel` | none for direct delivery | Registered concrete channel id; must be paired with `to`. Direct `/agent` cannot use `"last"`. |
|
||||
| `to` | unset | Nonempty recipient for direct announce delivery, paired with `channel`. |
|
||||
| `accountId` | channel default | Selects a configured, enabled account; requires `channel` and `to`. Unknown, disabled, or invalid selections return `400` before dispatch. |
|
||||
| `model` | agent/model defaults | Model id or alias override, subject to model availability and allowlist policy. |
|
||||
| `thinking` | agent/model defaults | Thinking override for the run. |
|
||||
| `timeoutSeconds` | agent timeout | Positive numeric turn-timeout override; direct payload values are floored to whole seconds. Invalid/nonpositive values are ignored. |
|
||||
|
||||
Omitting all destination fields runs without a direct announce destination.
|
||||
Supplying only part of a destination fails with `400` while delivery is enabled.
|
||||
`deliver: false` disables announcement, not the agent's ability to use messaging
|
||||
tools; constrain those tools in the agent policy when needed.
|
||||
|
||||
### Hook session and agent policy
|
||||
|
||||
Direct request agent ids must exist. Mapping agent ids resolve to a configured
|
||||
agent, with the legacy default-agent fallback for unknown mapping ids. If no
|
||||
owner can be resolved, admission fails rather than inventing an agent. The
|
||||
effective agent must pass `allowedAgentIds`; global session-store ownership is
|
||||
also enforced. Agent-prefixed keys are re-scoped to the selected agent and
|
||||
prefix-checked again.
|
||||
|
||||
Keys resolve from the request/mapping, then `hooks.defaultSessionKey`, then a
|
||||
generated `hook:<uuid>`. A configured default must match the prefix allowlist.
|
||||
Without a default, the allowlist must admit generated `hook:` keys.
|
||||
|
||||
- Direct `/agent` persistent mode requires an explicit request `sessionKey`, `allowRequestSessionKey: true`, and a nonempty prefix allowlist.
|
||||
- Persistent mappings require a stable mapping `sessionKey` or `defaultSessionKey`. Static mapping keys do not require caller-key opt-in, but still obey configured prefixes.
|
||||
- Templated mapping keys require a nonempty prefix allowlist at configuration resolution and `allowRequestSessionKey: true` at dispatch. This includes the built-in Gmail preset unless an earlier mapping overrides it.
|
||||
- `/wake` accepts an explicit key only with `mode: "now"` and the same caller-key/prefix policy. Without one, it uses the selected agent's main session; `defaultSessionKey` is for agent runs, not wakes.
|
||||
|
||||
A logical hook key is not always the stored session key. Isolated runs use fresh
|
||||
automation run sessions even when the hook key is stable. Persistence controls
|
||||
conversation reuse, not tool permissions or sandboxing. Requests sharing a
|
||||
canonical logical key are serialized through completion, even in isolated mode.
|
||||
A fixed `defaultSessionKey` therefore orders those requests but can make a later
|
||||
single request hit the admission timeout while an earlier run is still active.
|
||||
|
||||
### Mapping details
|
||||
|
||||
Custom `mappings` run in array order before `presets`. The first match owns the
|
||||
request, including a transform that returns `null`; later mappings are not tried.
|
||||
Both match predicates must pass when supplied. Omitting them matches any custom
|
||||
hook path.
|
||||
|
||||
| Mapping field | Default | Contract |
|
||||
| ---------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `id` | `mapping-<index>` | Bounded ingress-source attribution for admitted agent actions, not an authenticated principal or invoker. |
|
||||
| `match.path` | any custom path | Subpath after `hooks.path`, with leading/trailing slashes removed (`gmail` matches `/hooks/gmail`). |
|
||||
| `match.source` | any source | Exact match against the payload's string `source` field. |
|
||||
| `action` | `"agent"` | `"agent"` or `"wake"`. |
|
||||
| `wakeMode` | `"now"` | `"now"` or `"next-heartbeat"`; becomes `mode` for wake actions. |
|
||||
| `name` | `"Hook"` at dispatch | Templated agent-run label. |
|
||||
| `agentId` | resolved owner | Static target agent id; subject to effective-agent allowlist. |
|
||||
| `sessionKey` | default/generated key | Static or templated logical key; see session policy. |
|
||||
| `sessionMode` | `"isolated"` | `"isolated"` or `"persistent"` for agent actions. |
|
||||
| `messageTemplate` | empty | Agent input template; the final action must have a nonempty message. |
|
||||
| `textTemplate` | empty | Wake text template; the final action must have nonempty text. Use trusted notification text, not raw untrusted content. |
|
||||
| `forEach` | unset | Top-level payload array key; one action per item, with a 200-item cap. Nested/prototype paths are rejected. |
|
||||
| `deliver` | `true` | Agent announcement policy. Unlike direct `/agent`, mapped delivery may use `"last"` or defer partial targets to the automation delivery resolver. |
|
||||
| `channel` | `"last"` | Registered channel id or `"last"`. Mappings do not expose `accountId`. |
|
||||
| `to` | unset | Templated delivery target. Prefer explicit `channel` and `to`. |
|
||||
| `model` | agent/model defaults | Templated model override. |
|
||||
| `thinking` | agent/model defaults | Templated thinking override. |
|
||||
| `timeoutSeconds` | agent timeout | Positive integer turn timeout. |
|
||||
| `allowUnsafeExternalContent` | `false` | Dangerous: disables agent external-content wrapping for this mapping. Gmail's global unsafe flag can also disable wrapping. |
|
||||
| `transform.module` | unset | Safe relative JS/TS module under `transformsDir`; absolute, traversal, URL/drive forms, and symlink escapes are rejected. |
|
||||
| `transform.export` | `default`, then `transform` | Named function export; an explicitly named export must exist. |
|
||||
|
||||
Templates support `{{payload.field}}` or `{{field}}`, array indexing such as
|
||||
`{{messages[0].subject}}`, `{{headers.x-event-type}}`, `{{query.kind}}`, `{{path}}`,
|
||||
and `{{now}}` (ISO timestamp). Missing/null values become empty strings; objects
|
||||
serialize as JSON. An empty rendered session-key template is rejected.
|
||||
|
||||
Transforms receive `{ payload, headers, url, path }` and may return a partial
|
||||
action override, asynchronously if needed. Action output uses `kind: "agent"` or
|
||||
`"wake"`, with `message` or `text` respectively. Returning `null` skips the action;
|
||||
when no actions remain the response is `204`, before any run, task, execution
|
||||
identity, or audit receipt is created. Transform exceptions return `500`.
|
||||
|
||||
A transform-provided `sessionKey` is externally derived by default. Only trusted
|
||||
code producing a fixed key should mark `sessionKeySource: "static"`; never use
|
||||
that marker to bypass policy for a payload-derived key. Transforms execute as
|
||||
trusted Gateway code, not in the reader agent's sandbox. They are cached until
|
||||
hook configuration reload. Keep modules under the hooks transforms root, not
|
||||
workspace skill directories; move invalid modules there or remove an invalid
|
||||
`transformsDir` if doctor reports it.
|
||||
|
||||
### Hook retries and fan-out
|
||||
|
||||
Agent replay keys resolve in this order: `Idempotency-Key`,
|
||||
`X-OpenClaw-Idempotency-Key`, then payload `idempotencyKey`. Only trimmed nonempty
|
||||
strings of at most 256 characters are used. The same key replays only for the
|
||||
same token, path, and resolved dispatch fields; changing the message or routing
|
||||
can create a new run. Completed admission replay entries expire after 5 minutes
|
||||
and are bounded to 1,000 entries in memory. Restart clears them. Failed admissions
|
||||
remain retryable; a replayed `200` is not a fresh execution or a completion check.
|
||||
|
||||
For `forEach`, templates/transforms see the original payload with the chosen
|
||||
array replaced by `[currentItem]`. Missing, empty, or non-array values produce no
|
||||
actions (`204`). Only the first **200** items are processed; excess items are
|
||||
dropped with a warning, not an HTTP failure. Split larger batches at the sender.
|
||||
|
||||
Fan-out agent dispatch waits up to **8 seconds after mapping/transform work**.
|
||||
Pending admissions continue in the background without the single-run 15-second
|
||||
cancellation deadline. A fully admitted multi-agent batch returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"runId": "<first-hook-request-run-id>",
|
||||
"runIds": ["<hook-request-run-id-1>", "<hook-request-run-id-2>"],
|
||||
"dispatched": 2
|
||||
}
|
||||
```
|
||||
|
||||
A settled single-item batch retains `{ ok: true, runId }`. Partial failures or
|
||||
pending items return non-2xx with `ok: false`, an incomplete-batch `error`, admitted
|
||||
`runIds`, and up to five failure messages in `errors`. A pending-only batch uses
|
||||
`503`. An error can therefore coexist with admitted or still-pending work.
|
||||
|
||||
Agent fan-out derives replay identity from each rendered action even without an
|
||||
explicit idempotency key. Identical retries reconcile pending/admitted items
|
||||
within the cache lifetime; keep transforms deterministic for retries. Wake
|
||||
actions enqueue immediately and have no replay identity, including mixed
|
||||
wake/agent batches. This is not durable exactly-once processing.
|
||||
|
||||
### Gmail integration
|
||||
|
||||
- The built-in Gmail preset uses `sessionKey: "hook:gmail:{{messages[0].id}}"` with `forEach: "messages"`, so each email in a batched push gets its own isolated run and session.
|
||||
- The Gateway sizes the `/hooks/gmail` request-body limit from `hooks.gmail.maxBytes` (default 20 KB per message) times gog's 100-message batch contract; other hook paths keep the shared 256 KiB limit.
|
||||
- This per-message key isolates conversation context, not tools or workspace access. Without a custom mapping that sets `agentId`, the preset uses the default agent.
|
||||
- For untrusted inboxes, route Gmail to a dedicated reader agent and restrict that agent with [per-agent sandbox and tool policy](/tools/multi-agent-sandbox-tools). If the reader must notify the main agent, constrain the handoff with [`tools.agentToAgent`](/gateway/config-tools#tools-agenttoagent). See [Prompt injection](/gateway/security#prompt-injection) for the recommended threat model and model tier.
|
||||
- The setup wizard configures Gmail transport but does not create the reader agent or required session-key policy. Apply the complete [restricted Gmail reader configuration](/automation/cron-jobs#configure-a-restricted-gmail-reader-recommended) before running setup for untrusted mail.
|
||||
- If you keep that per-message routing, set `hooks.allowRequestSessionKey: true` and constrain `hooks.allowedSessionKeyPrefixes` to match the Gmail namespace, for example `["hook:", "hook:gmail:"]`.
|
||||
- If you need `hooks.allowRequestSessionKey: false`, override the preset with a static `sessionKey` instead of the templated default.
|
||||
The Gmail preset routes `/hooks/gmail` through `forEach: "messages"` and
|
||||
`sessionKey: "hook:gmail:{{messages[0].id}}"`, with isolated mode by default. A
|
||||
custom matching mapping runs before the preset. Without a mapping `agentId`, the
|
||||
preset uses the resolved default agent; conversation isolation does not restrict
|
||||
that agent's tools or workspace.
|
||||
|
||||
Apply the [restricted Gmail reader
|
||||
configuration](/automation/cron-jobs#configure-a-restricted-gmail-reader-recommended)
|
||||
before connecting untrusted mail. The setup command configures transport, not the
|
||||
reader or session-key policy. For the templated key, set
|
||||
`allowRequestSessionKey: true` and `allowedSessionKeyPrefixes: ["hook:gmail:"]`
|
||||
with a matching `defaultSessionKey`, or allow the broader `"hook:"` namespace.
|
||||
To keep caller-key overrides disabled, replace the preset with an earlier mapping
|
||||
using a static `sessionKey`. Keep isolated mode unless context reuse is intended.
|
||||
|
||||
```json5
|
||||
{
|
||||
hooks: {
|
||||
gmail: {
|
||||
account: "openclaw@gmail.com",
|
||||
account: "reader@example.com",
|
||||
topic: "projects/<project-id>/topics/gog-gmail-watch",
|
||||
subscription: "gog-gmail-watch-push",
|
||||
pushToken: "shared-push-token",
|
||||
pushToken: "<separate-random-push-token>",
|
||||
hookUrl: "http://127.0.0.1:18789/hooks/gmail",
|
||||
includeBody: true,
|
||||
maxBytes: 20000,
|
||||
@@ -1184,8 +1335,51 @@ Validation and safety notes:
|
||||
}
|
||||
```
|
||||
|
||||
- Gateway auto-starts `gog gmail watch serve` on boot when configured. Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to disable.
|
||||
- Don't run a separate `gog gmail watch serve` alongside the Gateway.
|
||||
This is the transport block, not the complete reader setup. The model is an
|
||||
example and must be available to the reader. Gmail fields:
|
||||
|
||||
| `hooks.gmail` field | Runtime default | Contract |
|
||||
| ---------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `account` | required | Gmail account already authorized in `gog`. |
|
||||
| `label` | `"INBOX"` | Gmail label to watch. OpenClaw excludes `SPAM`, `TRASH`, `DRAFT`, and `SENT` when launching the watcher. |
|
||||
| `topic` | required | Full Pub/Sub topic path. Setup can provision the `gog-gmail-watch` topic. |
|
||||
| `subscription` | `"gog-gmail-watch-push"` | Pub/Sub subscription used by setup. |
|
||||
| `pushToken` | required | Authenticates incoming pushes to the watcher. Separate from `hooks.token`, which authenticates forwarding to OpenClaw. Setup generates one if absent. |
|
||||
| `hookUrl` | local Gateway `/hooks/gmail` | Forwarding URL built from `hooks.path` and Gateway port unless configured. |
|
||||
| `includeBody` | `true` | Include email body snippets. Set `false` in config to omit them. |
|
||||
| `maxBytes` | `20000` | Positive integer per-message body limit passed to the watcher. Also used to derive the Gmail HTTP body allowance. |
|
||||
| `renewEveryMinutes` | `720` | Positive integer watch-renewal interval. |
|
||||
| `serve.bind` | `"127.0.0.1"` | Watcher bind host. |
|
||||
| `serve.port` | `8788` | Positive integer watcher port. |
|
||||
| `serve.path` | `"/gmail-pubsub"` | Watcher path. With Tailscale enabled and no explicit target, it becomes `/` because the exposed prefix is stripped. |
|
||||
| `tailscale.mode` | `"off"` | `"off"`, `"serve"`, or `"funnel"`. Setup defaults to `"funnel"`; runtime without saved config defaults to `"off"`. |
|
||||
| `tailscale.path` | resolved serve path | Exposed Tailscale path, normally `/gmail-pubsub` in setup. |
|
||||
| `tailscale.target` | local watcher | Optional port, `host:port`, or URL target. An explicit target preserves the configured serve path. |
|
||||
| `model` | agent/model defaults | Gmail model default; an explicit mapping model overrides it. A disallowed Gmail default is ignored, while an invalid explicit run override fails preparation. |
|
||||
| `thinking` | agent/model defaults | `"off"`, `"minimal"`, `"low"`, `"medium"`, or `"high"`; explicit mapping thinking takes precedence. |
|
||||
| `allowUnsafeExternalContent` | `false` | Dangerous: disable email safety wrapping for Gmail agent turns. Leave off for untrusted inboxes. |
|
||||
|
||||
Gmail-path mappings use a request-body allowance of
|
||||
`max(256 KiB, min(32 MiB, 100 × (3 × maxBytes + 8192)))`. The multiplier reserves
|
||||
space for escaped content and message metadata; it is not a guarantee that every
|
||||
upstream backlog fits. The upstream history page size counts history records,
|
||||
which can contain multiple messages. Fan-out still processes only the first 200
|
||||
items and logs dropped excess. See [batch limits and
|
||||
retries](/gateway/configuration-reference#hook-retries-and-fan-out).
|
||||
|
||||
When `hooks.enabled: true` and `hooks.gmail.account` is set, the Gateway starts
|
||||
`gog gmail watch serve` if its executable and required transport configuration
|
||||
are available, and renews the watch. Set `OPENCLAW_SKIP_GMAIL_WATCHER=1` to opt out.
|
||||
Do not start a second foreground watcher on the same listener. Setup output can
|
||||
contain tokens; see the [CLI reference](/cli/webhooks).
|
||||
|
||||
A successful push or hook response is transport/admission evidence, not proof of
|
||||
completed email processing or delivery. Verify the restricted reader through
|
||||
[logs and its run output](/automation/cron-jobs#verify-the-reader-boundary). For a
|
||||
reader-to-agent handoff, expose only the required tool and constrain
|
||||
[`tools.agentToAgent`](/gateway/config-tools#tools-agenttoagent); see also
|
||||
[Prompt injection](/gateway/security#prompt-injection) and
|
||||
[per-agent sandbox and tools](/tools/multi-agent-sandbox-tools).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+393
-172
@@ -1,6 +1,7 @@
|
||||
---
|
||||
summary: "Plugin hooks: intercept agent, tool, message, session, and Gateway lifecycle events"
|
||||
title: "Plugin hooks"
|
||||
doc-schema-version: 1
|
||||
read_when:
|
||||
- You are building a plugin that needs before_tool_call, before_agent_reply, message hooks, or lifecycle hooks
|
||||
- You need to block, rewrite, or require approval for tool calls from a plugin
|
||||
@@ -8,52 +9,181 @@ read_when:
|
||||
- You are projecting OpenClaw cron wakes into an external host scheduler
|
||||
---
|
||||
|
||||
Plugin hooks are in-process extension points for OpenClaw plugins: inspect or
|
||||
change agent runs, tool calls, message flow, session lifecycle, subagent
|
||||
routing, installs, or Gateway startup.
|
||||
Plugin hooks let a native OpenClaw plugin observe or change agent runs, tool
|
||||
calls, message delivery, and lifecycle events. Register a typed handler with
|
||||
`api.on("hook_name", handler)` and return the result documented for that hook.
|
||||
|
||||
Use [internal hooks](/automation/hooks) instead for a small operator-installed
|
||||
`HOOK.md` script reacting to command and Gateway events such as `/new`,
|
||||
`/reset`, `/stop`, `agent:bootstrap`, or `gateway:startup`.
|
||||
There are three different hook systems:
|
||||
|
||||
| You want to… | Use |
|
||||
| ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| Change prompts, gate tools, customize replies, or integrate plugin lifecycle | Typed plugin hooks on this page: `api.on("before_tool_call", ...)` |
|
||||
| Run an operator-installed script for `/new`, `/reset`, `/stop`, or bootstrap events | [Internal hooks](/automation/hooks): `HOOK.md` and colon event names such as `command:new` or `agent:bootstrap` |
|
||||
| Trigger an agent from an external service over HTTP | [Webhooks](/automation/cron-jobs#webhooks): Gateway HTTP endpoints |
|
||||
|
||||
Plugins can also register internal hooks with `api.registerHook(...)`. That is
|
||||
not the typed API: registering an underscore name such as `before_tool_call`
|
||||
there produces a warning, and the typed runner never invokes that registration.
|
||||
Use `api.on(...)` for every hook in this page's catalog.
|
||||
|
||||
## Quick start
|
||||
|
||||
Register typed hooks with `api.on(...)` from the plugin entry:
|
||||
This example replies to a user message containing `hook-demo-check` without
|
||||
calling the model.
|
||||
It assumes you already have a working Gateway and can send it a normal chat
|
||||
message. For package metadata, publishing, and install options, see
|
||||
[Building plugins](/plugins/building-plugins) and [Plugin manifest](/plugins/manifest).
|
||||
|
||||
```typescript
|
||||
Create a local `hook-demo` directory with these files:
|
||||
|
||||
```json package.json
|
||||
{
|
||||
"name": "hook-demo",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"openclaw": { "extensions": ["./index.ts"] }
|
||||
}
|
||||
```
|
||||
|
||||
```json openclaw.plugin.json
|
||||
{
|
||||
"id": "hook-demo",
|
||||
"name": "Hook Demo",
|
||||
"activation": { "onStartup": true },
|
||||
"configSchema": { "type": "object", "additionalProperties": false }
|
||||
}
|
||||
```
|
||||
|
||||
```typescript index.ts
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
|
||||
export default definePluginEntry({
|
||||
id: "tool-preflight",
|
||||
name: "Tool Preflight",
|
||||
id: "hook-demo",
|
||||
name: "Hook Demo",
|
||||
description: "Reply to a hook check without a model call.",
|
||||
register(api) {
|
||||
api.on(
|
||||
"before_tool_call",
|
||||
async (event) => {
|
||||
if (event.toolName !== "web_search") {
|
||||
return;
|
||||
"before_agent_reply",
|
||||
(event) => {
|
||||
if (event.cleanedBody.includes("hook-demo-check")) {
|
||||
return { handled: true, reply: { text: "Hook is working." } };
|
||||
}
|
||||
|
||||
return {
|
||||
requireApproval: {
|
||||
title: "Run web search",
|
||||
description: `Allow search query: ${String(event.params.query ?? "")}`,
|
||||
severity: "info",
|
||||
timeoutMs: 60_000,
|
||||
},
|
||||
};
|
||||
},
|
||||
{ priority: 50 },
|
||||
{ eligibleTriggers: ["user"] },
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Handlers that can return decisions or modifications run sequentially in
|
||||
descending `priority`; same-priority handlers keep registration order.
|
||||
Observation-only handlers run in parallel, and fire-and-forget observation
|
||||
dispatches can overlap with later events. Do not use priority to order
|
||||
observation side effects.
|
||||
Review local plugin code before loading it: native plugins run in the Gateway
|
||||
process. Link and enable the directory (`--force` acknowledges installing from
|
||||
a local source):
|
||||
|
||||
```bash
|
||||
openclaw plugins install --link ./hook-demo --force
|
||||
openclaw plugins enable hook-demo
|
||||
```
|
||||
|
||||
Grant this plugin access to conversation hooks in `openclaw.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"hook-demo": {
|
||||
"enabled": true,
|
||||
"hooks": { "allowConversationAccess": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Merge that entry into your existing config, then restart and inspect:
|
||||
|
||||
```bash
|
||||
openclaw gateway restart
|
||||
openclaw plugins inspect hook-demo --runtime --json
|
||||
```
|
||||
|
||||
Send `hook-demo-check` as a normal chat message. Expect `Hook is working.`; other
|
||||
messages continue through the normal agent path. If the hook does not run,
|
||||
see [Troubleshooting](/plugins/hooks#troubleshooting).
|
||||
|
||||
Despite its name, `cleanedBody` is the prepared run prompt and can contain
|
||||
channel context. The example matches a distinctive marker instead of assuming
|
||||
the field is only the sender's raw text.
|
||||
|
||||
### Permissions and scope
|
||||
|
||||
Hook registration does not bypass plugin loading rules. The plugin must be
|
||||
loaded and enabled; `plugins.enabled`, `plugins.allow`, and `plugins.deny` still
|
||||
apply. Restart the Gateway after changing plugin code or hook configuration.
|
||||
|
||||
- Non-bundled plugins need explicit
|
||||
`plugins.entries.<id>.hooks.allowConversationAccess: true` for
|
||||
`before_model_resolve`, `agent_turn_prepare`, `before_prompt_build`,
|
||||
`before_agent_reply`, `llm_input`, `llm_output`, `before_agent_finalize`,
|
||||
`agent_end`, and `before_agent_run`. Bundled plugins are allowed unless this
|
||||
option is explicitly `false`.
|
||||
- `allowPromptInjection: false` blocks `agent_turn_prepare`,
|
||||
`before_prompt_build`, `heartbeat_prompt_contribution`, and durable next-turn
|
||||
injections. It defaults to allowed, but does not grant conversation access.
|
||||
The first two hooks therefore need both permissions.
|
||||
- These are specific registration gates, not a sandbox or a universal filter
|
||||
for every hook that can see message data. Install only plugins you trust.
|
||||
|
||||
A typed handler receives `(event, ctx)`. The event describes the operation;
|
||||
the second argument carries hook-specific context. Fields such as
|
||||
`ctx.agentId`, `ctx.sessionKey`, and `ctx.runId` are optional on many hooks and
|
||||
may be absent for the emitting path. A registration is not automatically
|
||||
scoped to one agent or session: check the context in your handler when needed.
|
||||
|
||||
Read your plugin's resolved settings from `api.pluginConfig` inside the
|
||||
registration closure. Typed hooks do not receive a universal
|
||||
`event.context.pluginConfig` field; that field belongs to the internal
|
||||
`api.registerHook(...)` event contract.
|
||||
|
||||
### Choose a hook
|
||||
|
||||
| Task | Hook |
|
||||
| -------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| Reply without a model call | `before_agent_reply` → `{ handled: true, reply }`; omit `reply` for silence |
|
||||
| Add context or narrow tools for a turn | `before_prompt_build` |
|
||||
| Gate model input on a supported runner | `before_agent_run` → `{ outcome: "block", reason, message? }` |
|
||||
| Block a tool or request approval | `before_tool_call` |
|
||||
| Rewrite the full outgoing reply, including media | `reply_payload_sending` |
|
||||
| Rewrite outgoing text or cancel a send | `message_sending` |
|
||||
| Collect model timing without raw conversation text | `model_call_started` / `model_call_ended` |
|
||||
| Flush state after a turn or at shutdown | `agent_end` / `gateway_stop` |
|
||||
|
||||
The catalog is the registration API, not a promise that every runtime emits
|
||||
every hook. For example, `before_agent_run` is implemented by the embedded and
|
||||
CLI runners; do not rely on it as a Codex or Copilot input gate. Native tool,
|
||||
transcript, and compaction boundaries also differ. See
|
||||
[Codex hook boundaries](/plugins/codex-harness-runtime#hook-boundaries) and
|
||||
[Agent harness plugins](/plugins/sdk-agent-harness).
|
||||
|
||||
## Registration and execution
|
||||
|
||||
Keep `register(api)` synchronous and register handlers there. The handlers
|
||||
themselves may be asynchronous except for the two synchronous persistence hooks.
|
||||
|
||||
Handlers default to priority `0`; higher priorities run first, with registration
|
||||
order breaking ties. Execution depends on the hook kind:
|
||||
|
||||
| Kind | Execution contract |
|
||||
| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Modify | Sequential; results merge according to the hook's contract below. Returning a rewrite does not generally change the event passed to later handlers. |
|
||||
| Claim | Sequential; the first `{ handled: true }` wins and skips remaining handlers. |
|
||||
| Gate | Sequential; a block stops remaining handlers. |
|
||||
| Observe | Handlers run concurrently; return values are ignored. The emitter may await completion or dispatch fire-and-forget. |
|
||||
| Sync modify/gate | Synchronous, in priority order; each handler sees the latest message. Promises are ignored with a warning. |
|
||||
| Evaluate | Skill evaluators run concurrently and produce separate attributed outcomes. |
|
||||
|
||||
Priority does not serialize observation side effects. Fire-and-forget events
|
||||
can overlap later events, and callbacks are not a durable event queue. Return
|
||||
modifications explicitly instead of relying on in-place mutation.
|
||||
|
||||
`api.on(name, handler, opts?)` accepts:
|
||||
|
||||
@@ -62,7 +192,7 @@ observation side effects.
|
||||
| `matcher` | Non-empty list of canonical OpenClaw tool ids handled by `before_tool_call` or `after_tool_call`, such as `exec`, `apply_patch`, or `spawn_agent`. Omit to match all tools. Empty lists, wildcards, blanks, and provider-specific aliases are invalid. |
|
||||
| `priority` | Ordering; higher runs first. |
|
||||
| `registrationId` | Stable identity for one registration inside a plugin. Skill evaluators use it as `evaluatorId`; otherwise the plugin id is used. |
|
||||
| `timeoutMs` | Per-hook await budget. When it expires, OpenClaw stops awaiting that handler and moves on. It does not cancel the handler or its side effects. Omit to use the runner's default per-hook timeout. |
|
||||
| `timeoutMs` | Per-handler asynchronous await budget. Expiry applies the hook's failure policy below; it does not cancel the handler or its side effects. Omit to use the runner's default, if any. |
|
||||
| `eligibleTriggers` | For `before_agent_reply` only, limits host dispatch to one or more of `cron`, `heartbeat`, or `user`. |
|
||||
| `requiresToolAuthority` | For `before_prompt_build` only, runs the handler after the host finalizes the current turn's tool surface and supplies ephemeral `ctx.toolAuthority`. Use this for context retrieval that must follow tool policy. |
|
||||
|
||||
@@ -93,30 +223,39 @@ Operators can set hook budgets without patching plugin code:
|
||||
```
|
||||
|
||||
`hooks.timeouts.<hookName>` overrides `hooks.timeoutMs`, which overrides the
|
||||
plugin-authored `api.on(..., { timeoutMs })` value. Each value must be a
|
||||
positive integer up to 600000 ms. Prefer per-hook overrides for known-slow
|
||||
hooks so one plugin does not get a longer budget everywhere.
|
||||
plugin-authored `api.on(..., { timeoutMs })` value. The two operator config
|
||||
fields accept positive integers up to 600000 ms. Prefer per-hook overrides for
|
||||
known-slow hooks so one plugin does not get a longer budget everywhere.
|
||||
|
||||
A timed-out handler promise continues running because hook callbacks do not
|
||||
receive a timeout-owned cancellation signal. `before_tool_call` receives the
|
||||
receive a timeout-owned cancellation signal. `before_tool_call` may receive the
|
||||
owning tool call's `ctx.abortSignal`, but hook timeout expiry does not abort it.
|
||||
The hook dispatch can release its Gateway admission while that plugin work is
|
||||
still in progress. Plugins that own long-running work must provide their own
|
||||
cancellation and shutdown lifecycle.
|
||||
|
||||
Policy hooks `before_tool_call` and `before_install` use a 15-second default per
|
||||
handler. A timeout fails closed: the tool call or installation is rejected
|
||||
instead of continuing without a policy decision.
|
||||
The standard runner applies these defaults **per handler**:
|
||||
|
||||
`gateway_stop` uses a five-second default per handler. Timed-out handlers are
|
||||
logged and shutdown continues so plugin cleanup cannot consume the Gateway
|
||||
process watchdog.
|
||||
| Hooks | Default timeout | On thrown error or timeout |
|
||||
| -------------------------------------------------------------------------------------------------------------- | ----------------------------------- | ---------------------------------------------------------------- |
|
||||
| `before_agent_run`, `before_tool_call`, `before_install` | 15 seconds | Fail closed: block the run, tool call, or install |
|
||||
| `before_agent_finalize`, `before_prompt_build`, `message_sending`, `reply_payload_sending`, `resolve_exec_env` | 15 seconds | Log and skip the failed handler; retain other successful results |
|
||||
| `agent_end`, `before_compaction`, `after_compaction`, `skill_changed`, `skill_proposal_changed` | 30 seconds | Log and continue |
|
||||
| `channel_pairing_requested` | 2 seconds | Log and continue |
|
||||
| `gateway_stop` | 5 seconds | Log and continue shutdown |
|
||||
| `skill_proposal_evaluate` | 120 seconds | Record an attributed error outcome |
|
||||
| Other asynchronous hooks, including claim hooks | No runner timeout unless configured | Log and continue |
|
||||
| `tool_result_persist`, `before_message_write` | No asynchronous timeout | Synchronous errors are logged; failed results are ignored |
|
||||
|
||||
Outbound modifying hooks `message_sending` and `reply_payload_sending` use a
|
||||
15-second default per handler. If one times out, OpenClaw logs the plugin error
|
||||
and continues with the latest payload so the serialized delivery lane can
|
||||
settle. Set a larger per-hook budget for plugins that intentionally do slower
|
||||
work before delivery.
|
||||
An emitter can impose a tighter overall lifecycle budget, such as the
|
||||
shutdown `session_end` drain below. A timeout only bounds an asynchronous
|
||||
await; it cannot interrupt synchronous JavaScript. For a policy requirement,
|
||||
use a fail-closed gate rather than assuming an observation or delivery hook
|
||||
will reject the operation on failure.
|
||||
|
||||
For claim hooks, continuing means trying the next handler. The caller decides
|
||||
what happens if nobody claims; a failed `inbound_claim` for a bound
|
||||
conversation can produce a binding notice instead of an ordinary agent reply.
|
||||
|
||||
Channel plugins that use `createReplyDispatcher` can likewise declare a larger
|
||||
positive per-stage budget with `beforeDeliverOptions: { timeoutMs }`, or when
|
||||
@@ -124,100 +263,103 @@ appending work with `dispatcher.appendBeforeDeliver(handler, { timeoutMs })`.
|
||||
Without an owner-declared budget, those callbacks use the same 15-second
|
||||
default so a hung callback cannot retain the serialized delivery lane.
|
||||
|
||||
Each hook receives `event.context.pluginConfig`, the resolved config for the
|
||||
plugin that registered that handler. OpenClaw injects it per handler without
|
||||
mutating the shared event object other plugins see.
|
||||
|
||||
## Hook catalog
|
||||
|
||||
Hooks are grouped by the surface they extend. **Bold** names accept a decision
|
||||
result (block, cancel, override, or require approval); the rest are
|
||||
observation-only.
|
||||
Hooks are grouped by the surface they extend. Kinds refer to the execution
|
||||
contracts above; a modifying hook is not an observation hook.
|
||||
|
||||
**Agent turn**
|
||||
|
||||
| Hook | Purpose |
|
||||
| ------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `before_model_resolve` | Override provider or model before session messages load |
|
||||
| `agent_turn_prepare` | Consume queued plugin turn injections and add same-turn context before prompt hooks |
|
||||
| `before_prompt_build` | Add prompt context, narrow the current turn's submitted tools, or perform authorized post-policy enrichment |
|
||||
| **`before_agent_run`** | Inspect the final prompt and session messages before model submission; can block the run |
|
||||
| **`before_agent_reply`** | Short-circuit the model turn with a synthetic reply or silence |
|
||||
| **`before_agent_finalize`** | Inspect the natural final answer and request one more model pass |
|
||||
| `agent_end` | Observe final messages, success state, and run duration |
|
||||
| `heartbeat_prompt_contribution` | Add heartbeat-only context for background monitor and lifecycle plugins |
|
||||
| Hook | Kind | Purpose |
|
||||
| ------------------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `before_model_resolve` | Modify | Override provider or model before session messages load |
|
||||
| `agent_turn_prepare` | Modify | Inspect drained plugin turn injections and add context before prompt hooks |
|
||||
| `before_prompt_build` | Modify | Add prompt context, narrow the current turn's submitted tools, or perform authorized post-policy enrichment |
|
||||
| `before_agent_run` | Gate | Inspect the final prompt and session messages before model submission; can block the run |
|
||||
| `before_agent_reply` | Claim | Short-circuit the model turn with a synthetic reply or silence |
|
||||
| `before_agent_finalize` | Modify | Inspect the natural final answer and request one more model pass |
|
||||
| `agent_end` | Observe | Observe final messages, success state, and run duration |
|
||||
| `heartbeat_prompt_contribution` | Modify | Add heartbeat-only context for background monitor and lifecycle plugins |
|
||||
|
||||
**Conversation observation**
|
||||
|
||||
| Hook | Purpose |
|
||||
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `model_call_started` / `model_call_ended` | Sanitized provider/model call metadata: timing, outcome, bounded request-id hashes. No prompt or response content. |
|
||||
| `llm_input` | Provider input: system prompt, prompt, history |
|
||||
| `llm_output` | Provider output, usage, and the resolved `contextTokenBudget` when available |
|
||||
| Hook | Kind | Purpose |
|
||||
| ----------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| `model_call_started` / `model_call_ended` | Observe | Sanitized provider/model call metadata: timing, outcome, bounded request-id hashes. No prompt or response content. |
|
||||
| `llm_input` | Observe | Provider input: system prompt, prompt, history |
|
||||
| `llm_output` | Observe | Provider output, usage, and the resolved `contextTokenBudget` when available |
|
||||
|
||||
**Tools**
|
||||
|
||||
| Hook | Purpose |
|
||||
| -------------------------- | --------------------------------------------------------- |
|
||||
| **`before_tool_call`** | Rewrite tool params, block execution, or require approval |
|
||||
| `after_tool_call` | Observe tool results, errors, and duration |
|
||||
| `resolve_exec_env` | Contribute plugin-owned environment variables to `exec` |
|
||||
| **`tool_result_persist`** | Rewrite the assistant message produced from a tool result |
|
||||
| **`before_message_write`** | Inspect or block an in-progress message write (rare) |
|
||||
| Hook | Kind | Purpose |
|
||||
| ---------------------- | ------------------ | ---------------------------------------------------------- |
|
||||
| `before_tool_call` | Modify / gate | Rewrite tool params, block execution, or require approval |
|
||||
| `after_tool_call` | Observe | Observe tool results, errors, and duration |
|
||||
| `resolve_exec_env` | Modify | Contribute plugin-owned environment variables to `exec` |
|
||||
| `tool_result_persist` | Sync modify | Rewrite a toolResult message before transcript persistence |
|
||||
| `before_message_write` | Sync modify / gate | Rewrite or block a message before transcript persistence |
|
||||
|
||||
**Messages and delivery**
|
||||
|
||||
| Hook | Purpose |
|
||||
| --------------------------- | -------------------------------------------------------------------------- |
|
||||
| **`inbound_claim`** | Claim an inbound message for the plugin that owns its conversation binding |
|
||||
| `channel_pairing_requested` | Observe newly created DM pairing requests |
|
||||
| `message_received` | Observe inbound content, sender, thread, and metadata |
|
||||
| **`message_sending`** | Rewrite outbound content or cancel delivery |
|
||||
| **`reply_payload_sending`** | Mutate or cancel normalized reply payloads before delivery |
|
||||
| `message_sent` | Observe outbound delivery success or failure |
|
||||
| **`before_dispatch`** | Inspect or rewrite an outbound dispatch before channel handoff |
|
||||
| **`reply_dispatch`** | Participate in the final reply-dispatch pipeline |
|
||||
| Hook | Kind | Purpose |
|
||||
| --------------------------- | ------------- | -------------------------------------------------------------------------- |
|
||||
| `inbound_claim` | Claim | Claim an inbound message for the plugin that owns its conversation binding |
|
||||
| `channel_pairing_requested` | Observe | Observe newly created DM pairing requests |
|
||||
| `message_received` | Observe | Observe inbound content, sender, thread, and metadata |
|
||||
| `message_sending` | Modify / gate | Rewrite outbound content or cancel delivery |
|
||||
| `reply_payload_sending` | Modify / gate | Mutate or cancel normalized reply payloads before delivery |
|
||||
| `message_sent` | Observe | Observe outbound delivery success or failure |
|
||||
| `before_dispatch` | Claim | Handle an inbound message before the normal model dispatch |
|
||||
| `reply_dispatch` | Claim | Own reply generation and dispatch instead of the default model path |
|
||||
|
||||
`inbound_claim` is not a global pre-routing broadcast. OpenClaw invokes it only
|
||||
for the plugin that owns the message's core-managed conversation binding. To
|
||||
suppress an ordinary agent turn before model input without retaining the
|
||||
original prompt in transcript, use `before_agent_run`. To short-circuit an agent
|
||||
turn with a synthetic reply or silence, use `before_agent_reply`.
|
||||
original prompt in transcript, use `before_agent_run` on a supported runner.
|
||||
To short-circuit an agent turn with a synthetic reply or silence, use
|
||||
`before_agent_reply`.
|
||||
|
||||
**Sessions and compaction**
|
||||
|
||||
| Hook | Purpose |
|
||||
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `session_start` / `session_end` | Track session lifecycle boundaries. `reason` is one of `new`, `reset`, `idle`, `daily`, `compaction`, `deleted`, `shutdown`, `restart`, or `unknown`. `shutdown`/`restart` fire from the Gateway shutdown finalizer when the process stops or restarts with active sessions, so plugins (memory, transcript stores) can finalize ghost rows instead of leaving them open across restarts. The finalizer is bounded so a slow plugin cannot block SIGTERM/SIGINT. |
|
||||
| `before_compaction` / `after_compaction` | Observe or annotate compaction cycles |
|
||||
| `before_reset` | Observe session-reset events (`/reset`, programmatic resets) |
|
||||
| Hook | Kind | Purpose |
|
||||
| ---------------------------------------- | ------- | ------------------------------------------------------------ |
|
||||
| `session_start` / `session_end` | Observe | Track session lifecycle boundaries |
|
||||
| `before_compaction` / `after_compaction` | Observe | Observe compaction boundaries; no rewrite or veto result |
|
||||
| `before_reset` | Observe | Observe session-reset events (`/reset`, programmatic resets) |
|
||||
|
||||
`session_end.reason` is one of `new`, `reset`, `idle`, `daily`, `compaction`,
|
||||
`deleted`, `shutdown`, `restart`, or `unknown`. `session_start` has no reason
|
||||
field; it can include `resumedFrom`. Shutdown/restart events come from the
|
||||
Gateway finalizer for active sessions, so plugins can close session state
|
||||
before the process exits.
|
||||
|
||||
Shutdown and restart share one **2-second total `session_end` drain budget**
|
||||
across all active sessions and plugin handlers; the budget is not per handler.
|
||||
Return quickly or keep finalization bounded and persistence crash-consistent.
|
||||
If the budget expires, OpenClaw logs `session-end-drain timed out` and continues
|
||||
shutdown, so unfinished plugin work can be interrupted.
|
||||
If the budget expires, OpenClaw logs `shutdown session-end drain timed out`
|
||||
and continues shutdown, so unfinished plugin work can be interrupted.
|
||||
|
||||
For `sessions.create` calls with `parentSessionKey` and `emitCommandHooks: true`, a distinct child always receives `session_start`. Callers declare whether the parent also receives terminal `session_end` with `succeedsParent`: `true` means successor, `false` means parallel child. Omission preserves the legacy parent-rollover behavior. The `command:new` and `before_reset` hooks still describe the requested `/new` action in both cases.
|
||||
|
||||
**Subagents**
|
||||
|
||||
- `subagent_spawned` / `subagent_ended` - observe subagent launch and completion.
|
||||
- `subagent_delivery_target` - compatibility hook for completion delivery when no core session binding can project a route.
|
||||
- `subagent_progress` - observe portable `started` / `ended` progress for a background child run; includes `runId`, `childSessionKey`, optional requester route, and an outcome on `ended`.
|
||||
- `subagent_delivery_target` - modifying compatibility hook for completion delivery when no core session binding can project a route. The first returned `origin` wins.
|
||||
- `subagent_spawned` includes `resolvedModel` and `resolvedProvider` when OpenClaw has resolved the child session's native model before launch.
|
||||
- `subagent_ended` carries `targetSessionKey` (identity - matches `subagent_spawned.childSessionKey`), `targetKind` (`"subagent"` or `"acp"`), `reason`, optional `outcome` (`"ok"`, `"error"`, `"timeout"`, `"killed"`, `"reset"`, or `"deleted"`), optional `error`, `runId`, `endedAt`, `accountId`, and `sendFarewell`. It does **not** include `agentId` or `childSessionKey`; use `targetSessionKey` to correlate with the matching `subagent_spawned` event.
|
||||
|
||||
**Lifecycle**
|
||||
|
||||
| Hook | Purpose |
|
||||
| -------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `gateway_start` / `gateway_stop` | Start or stop plugin-owned services with the Gateway |
|
||||
| `cron_reconciled` | Reconcile against the complete Gateway cron state after startup or reload |
|
||||
| `cron_changed` | Observe Gateway-owned cron lifecycle changes (added, updated, removed, started, finished, scheduled) |
|
||||
| **`before_install`** | Inspect staged skill or plugin install material from a loaded plugin runtime |
|
||||
| **`skill_proposal_evaluate`** | Evaluate one exact Skill Workshop draft and return attributed findings, metrics, or a decision |
|
||||
| `skill_proposal_changed` | Observe durable Skill Workshop proposal lifecycle events after they commit |
|
||||
| `skill_changed` | Observe committed live-skill create, update, and removal events |
|
||||
| Hook | Kind | Purpose |
|
||||
| -------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `gateway_start` / `gateway_stop` | Observe | Start or stop plugin-owned services with the Gateway |
|
||||
| `cron_reconciled` | Observe | Reconcile against the complete Gateway cron state after startup or reload |
|
||||
| `cron_changed` | Observe | Observe Gateway-owned cron lifecycle changes (added, updated, removed, started, finished, scheduled) |
|
||||
| `before_install` | Modify / gate | Inspect staged skill or plugin install material from a loaded plugin runtime |
|
||||
| `skill_proposal_evaluate` | Evaluate | Evaluate one exact Skill Workshop draft and return attributed findings, metrics, or a decision |
|
||||
| `skill_proposal_changed` | Observe | Observe durable Skill Workshop proposal lifecycle events after they commit |
|
||||
| `skill_changed` | Observe | Observe committed live-skill create, update, and removal events |
|
||||
|
||||
### Skill lifecycle and evaluation
|
||||
|
||||
@@ -257,18 +399,19 @@ consume it itself.
|
||||
|
||||
Stored outcomes identify the evaluator, plugin id, plugin package version,
|
||||
status, and returned result. Timeouts and thrown errors are recorded as
|
||||
attributed error outcomes; they do not fail the whole evaluation. Applying a
|
||||
proposal is blocked only when a completed evaluator returns
|
||||
`decision: "block"`. Apply revalidates the evaluated target tree under the
|
||||
Workshop mutation lock, so any live skill asset drift requires reevaluation.
|
||||
The combined persisted evaluator result is capped at 512 KiB.
|
||||
attributed error outcomes; they do not fail the whole evaluation. Among
|
||||
evaluator outcomes, only a completed `decision: "block"` vetoes apply. Other
|
||||
Workshop validation and ownership checks still apply. Apply revalidates the
|
||||
evaluated target tree under the Workshop mutation lock, so any live skill asset
|
||||
drift requires reevaluation.
|
||||
The complete persisted evaluation envelope is capped at 512 KiB.
|
||||
|
||||
`skill_proposal_changed` fires after the matching proposal row and append-only
|
||||
lifecycle event commit. It carries the event id, sequence, exact proposal
|
||||
revision hash, optional correlation id, and evaluation outcomes.
|
||||
`skill_changed` fires after a live skill create, update, or removal commits and
|
||||
includes before/after artifacts with content, tree, declared, and source
|
||||
versions when available.
|
||||
includes optional before/after artifacts with content and tree hashes, plus
|
||||
declared and source versions when available.
|
||||
|
||||
These hooks are primitives, not an optimization scheduler. A plugin or external
|
||||
controller can observe a durable proposal event, evaluate its exact revision hash,
|
||||
@@ -302,8 +445,9 @@ text. The hook does not include the inbound message body or media.
|
||||
## Debug runtime hooks
|
||||
|
||||
Use `before_model_resolve` to switch provider or model for an agent turn - it
|
||||
runs before model resolution. `llm_output` only runs after a model attempt
|
||||
produces assistant output.
|
||||
runs before model resolution. `llm_output` describes an attempt's output when
|
||||
the runtime emits it; `assistantTexts` can be empty and `lastAssistant` absent,
|
||||
so the event alone does not prove a successful final answer.
|
||||
|
||||
For proof of the effective session model, inspect runtime registrations, then
|
||||
use `openclaw sessions` or the Gateway session/status surfaces. To debug
|
||||
@@ -364,7 +508,15 @@ Guard behavior for typed lifecycle hooks:
|
||||
|
||||
- `block: true` is terminal and skips lower-priority handlers.
|
||||
- `block: false` is treated as no decision.
|
||||
- `params` rewrites the tool parameters for execution.
|
||||
- Return `params` to rewrite host-owned tool parameters. Each handler sees an
|
||||
isolated copy of the original event, not prior returned rewrites. The last
|
||||
returned `params` wins until an approval is requested.
|
||||
- The first `requireApproval` wins, and its plugin id is stamped by the host.
|
||||
It freezes the selected parameter snapshot: later handlers can block but
|
||||
cannot change the approved parameters.
|
||||
- Native tool relays can have narrower contracts. Codex native tools support
|
||||
blocking and observation, but parameter rewrites are rejected; see
|
||||
[Codex hook boundaries](/plugins/codex-harness-runtime#hook-boundaries).
|
||||
- `requireApproval` pauses the agent run and asks the user through plugin
|
||||
approvals. `/approve` can approve both exec and plugin approvals. In Codex
|
||||
app-server report-mode native `PreToolUse` relays, this defers to the
|
||||
@@ -375,6 +527,24 @@ Guard behavior for typed lifecycle hooks:
|
||||
- `onResolution` receives the resolved decision: `allow-once`, `allow-always`,
|
||||
`deny`, `timeout`, or `cancelled`.
|
||||
|
||||
For example, add this inside `register(api)` to ask before a host-owned
|
||||
`exec` call. No conversation-access opt-in is needed for `before_tool_call`:
|
||||
|
||||
```typescript
|
||||
api.on(
|
||||
"before_tool_call",
|
||||
() => ({
|
||||
requireApproval: {
|
||||
title: "Run command",
|
||||
description: "Allow this exec tool call?",
|
||||
severity: "info",
|
||||
timeoutMs: 60_000,
|
||||
},
|
||||
}),
|
||||
{ matcher: ["exec"], priority: 50 },
|
||||
);
|
||||
```
|
||||
|
||||
### Sender-aware policy in one file
|
||||
|
||||
A standalone plugin file can keep deployment-specific policy in code instead
|
||||
@@ -521,8 +691,9 @@ by `before_tool_call`. Omit the matcher to retain match-all behavior.
|
||||
|
||||
### Exec environment hook
|
||||
|
||||
`resolve_exec_env` lets plugins contribute environment variables to `exec`
|
||||
tool invocations before the command runs. It receives:
|
||||
`resolve_exec_env` lets plugins contribute environment variables to OpenClaw
|
||||
`exec` tool invocations before the command runs. It is not a hook for every
|
||||
harness-native shell. It receives:
|
||||
|
||||
- `event.sessionKey`
|
||||
- `event.toolName`, currently always `"exec"`
|
||||
@@ -545,6 +716,17 @@ requests.
|
||||
|
||||
### Tool result persistence
|
||||
|
||||
`tool_result_persist` and `before_message_write` are synchronous hooks. Do not
|
||||
make their handlers `async`: returned promises are ignored with a warning.
|
||||
Each handler receives the message returned by the previous handler.
|
||||
`tool_result_persist` returns `{ message }` to replace a tool result;
|
||||
`before_message_write` can return `{ message }` or `{ block: true }` to prevent
|
||||
that transcript write. Blocking persistence is not a tool-execution veto.
|
||||
|
||||
These hooks operate on OpenClaw-owned transcript writes. They do not rewrite
|
||||
Codex-native tool records; see
|
||||
[Codex transcript boundaries](/plugins/codex-harness-runtime#compaction-and-transcript-mirror).
|
||||
|
||||
Tool results can include structured `details` for UI rendering, diagnostics,
|
||||
media routing, or plugin-owned metadata. Treat `details` as runtime metadata,
|
||||
not prompt content:
|
||||
@@ -565,7 +747,7 @@ Use the phase-specific hooks for new plugins:
|
||||
- `before_model_resolve`: receives only the current prompt and attachment
|
||||
metadata. Return `providerOverride` or `modelOverride`.
|
||||
- `agent_turn_prepare`: receives the current prompt, prepared session
|
||||
messages, and any exactly-once queued injections drained for this session.
|
||||
messages, and queued injections consumed for this session.
|
||||
Return `prependContext` or `appendContext`.
|
||||
- `before_prompt_build`: receives the current prompt and session messages.
|
||||
Return `prependContext`, `appendContext`, `systemPrompt`,
|
||||
@@ -581,11 +763,23 @@ Use the phase-specific hooks for new plugins:
|
||||
- `before_prompt_build` with `{ requiresToolAuthority: true }`: runs in a
|
||||
second, post-policy phase. Use it when prompt enrichment reads data through
|
||||
a tool-backed capability and the same turn must be allowed to call that
|
||||
tool. See [Authorized prompt enrichment](#authorized-prompt-enrichment).
|
||||
tool. See [Authorized prompt enrichment](/plugins/hooks#authorized-prompt-enrichment).
|
||||
- `heartbeat_prompt_contribution`: runs only for heartbeat turns and returns
|
||||
`prependContext` or `appendContext`. Intended for background monitors that
|
||||
need to summarize current state without changing user-initiated turns.
|
||||
|
||||
On the embedded and CLI prompt-preparation paths, ordering is: drain queued
|
||||
injections → `agent_turn_prepare` → heartbeat contribution (if applicable) →
|
||||
ordinary `before_prompt_build` → finalized tool policy → authorized prompt
|
||||
enrichment. `agent_turn_prepare` and queued-injection draining are not currently
|
||||
wired into the Codex or Copilot prompt paths.
|
||||
|
||||
For multiple registrations, the first defined provider/model override and
|
||||
`systemPrompt` win. Context additions concatenate in priority order, and tool
|
||||
restrictions intersect. A nested ordinary `before_prompt_build` dispatch on
|
||||
the same runner is skipped while its outer dispatch is active; other hook
|
||||
families and independent turns remain available.
|
||||
|
||||
### Authorized prompt enrichment
|
||||
|
||||
Register `before_prompt_build` with `requiresToolAuthority: true` when a plugin
|
||||
@@ -637,8 +831,9 @@ installs or updates. Do not publish a package that uses this option while
|
||||
claiming compatibility with an older plugin API; an older host may otherwise
|
||||
treat an unknown option as an ordinary pre-policy hook.
|
||||
|
||||
`before_agent_run` runs after prompt construction and before any model input,
|
||||
including prompt-local image loading and `llm_input` observation. It receives
|
||||
On the embedded and CLI runners, `before_agent_run` runs after prompt
|
||||
construction and before model submission, including `llm_input` observation.
|
||||
On the embedded path it also precedes prompt-local image loading. It receives
|
||||
the current user input as `prompt`, plus loaded session history in `messages`
|
||||
and the active system prompt. Return `{ outcome: "block", reason, message? }`
|
||||
to stop the run before the model reads the prompt. `reason` is internal;
|
||||
@@ -653,11 +848,14 @@ excluded from transcript, history, broadcast, log, and diagnostics payloads.
|
||||
Observability should use sanitized fields such as blocker id, outcome,
|
||||
timestamp, or a safe category.
|
||||
|
||||
Agent-turn hooks including `agent_end` include `event.runId` when OpenClaw can
|
||||
identify the active run; the same value is also on `ctx.runId`. Cron-driven
|
||||
runs also expose `ctx.jobId` (the originating cron job id) on the agent-turn
|
||||
context so hooks can scope metrics, side effects, or state to a specific
|
||||
scheduled job. `ctx.jobId` is not part of the `before_tool_call` tool context.
|
||||
Hooks that expose `event.runId`, such as `agent_end` and
|
||||
`before_agent_finalize`, receive it when OpenClaw can identify the active run;
|
||||
the same value is also on `ctx.runId`. Prompt hooks do not all have an event
|
||||
`runId` field, so use their typed context for correlation. Cron-driven
|
||||
runs can also expose `ctx.jobId` (the originating cron job id) when supplied
|
||||
by the emitter, so hooks can scope metrics, side effects, or state to a specific
|
||||
scheduled job. Do not assume every agent event carries it. `ctx.jobId` is not
|
||||
part of the `before_tool_call` tool context.
|
||||
|
||||
For channel-originated runs, `ctx.channel` and `ctx.messageProvider` identify
|
||||
the provider surface such as `discord` or `telegram`, while `ctx.channelId` is
|
||||
@@ -712,12 +910,12 @@ older plugins. Core does not populate it; new channel-specific sender
|
||||
identities should live under `ctx.channelContext.sender` through module
|
||||
augmentation.
|
||||
|
||||
`agent_end` is an observation hook. Gateway and persistent harness paths run
|
||||
it fire-and-forget after the turn, while short-lived one-shot CLI paths wait
|
||||
`agent_end` is an observation hook. Channel-backed paths generally run
|
||||
it fire-and-forget after the turn, while local one-shot paths can wait
|
||||
for the hook promise before process cleanup so trusted plugins can flush
|
||||
terminal observability or capture state. The hook runner applies a 30 second
|
||||
timeout so a wedged plugin or embedding endpoint cannot leave the hook promise
|
||||
pending forever. A timeout is logged and OpenClaw continues; it does not
|
||||
default per-handler timeout so a wedged plugin or embedding endpoint cannot
|
||||
leave the hook promise pending forever. A timeout is logged and OpenClaw continues; it does not
|
||||
cancel plugin-owned network work unless the plugin also uses its own abort
|
||||
signal.
|
||||
|
||||
@@ -732,18 +930,28 @@ context-window metadata, the hook event and context also include
|
||||
fixed model contracts, and runtime discovery, plus `contextWindowSource` and
|
||||
`contextWindowReferenceTokens` when a lower cap was applied.
|
||||
|
||||
These provider-call hooks are currently emitted by the embedded model-call
|
||||
path. A harness exposing `llm_input` / `llm_output` does not automatically
|
||||
expose the same provider-call telemetry. In external harnesses, LLM events
|
||||
describe adapter-visible input and output, not necessarily the raw provider
|
||||
request or complete native history.
|
||||
|
||||
`before_agent_finalize` runs only when a harness is about to accept a natural
|
||||
final assistant answer. It is not the `/stop` cancellation path and does not
|
||||
run when the user aborts a turn. Return `{ action: "revise", reason }` to ask
|
||||
the harness for one more model pass before finalization, `{ action:
|
||||
"finalize", reason? }` to force finalization, or omit a result to continue.
|
||||
Handlers have a 15s default budget; on timeout, OpenClaw logs the failure and
|
||||
continues with the original final answer.
|
||||
keeps decisions from other handlers. With no revision decision, normal
|
||||
finalization continues. Multiple `revise` reasons are combined; any `finalize`
|
||||
decision overrides revision requests. This hook requires a finalization
|
||||
integration: the embedded runner and native hook relay provide it, but the
|
||||
Copilot harness does not currently dispatch it.
|
||||
Codex native `Stop` hooks are relayed into this hook as OpenClaw
|
||||
`before_agent_finalize` decisions.
|
||||
|
||||
When returning `action: "revise"`, plugins can include `retry` metadata to
|
||||
make the extra model pass bounded and replay-safe:
|
||||
bound repeated revision requests within a run:
|
||||
|
||||
```typescript
|
||||
type BeforeAgentFinalizeRetry = {
|
||||
@@ -754,34 +962,14 @@ type BeforeAgentFinalizeRetry = {
|
||||
```
|
||||
|
||||
`instruction` is appended to the revision reason sent to the harness.
|
||||
`idempotencyKey` lets the host count retries for the same plugin request
|
||||
across equivalent finalize decisions, and `maxAttempts` caps how many extra
|
||||
passes the host will allow before continuing with the natural final answer.
|
||||
`idempotencyKey` lets the host count retries across equivalent finalize
|
||||
decisions within a run; without a key, it hashes the instruction.
|
||||
`maxAttempts` defaults to one extra pass for that key. Use a plugin-specific
|
||||
key to avoid sharing a budget with another plugin. A harness can apply a
|
||||
tighter overall revision limit; the embedded runner allows at most three.
|
||||
|
||||
Non-bundled plugins that need raw conversation hooks (`before_model_resolve`,
|
||||
`agent_turn_prepare`, `before_prompt_build`, `before_agent_reply`, `llm_input`,
|
||||
`llm_output`, `before_agent_finalize`, `agent_end`, or `before_agent_run`) must
|
||||
set:
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"entries": {
|
||||
"my-plugin": {
|
||||
"hooks": {
|
||||
"allowConversationAccess": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`agent_turn_prepare` and `before_prompt_build` also mutate prompt construction,
|
||||
so they require conversation access and remain subject to
|
||||
`plugins.entries.<id>.hooks.allowPromptInjection`. Prompt-mutating hooks and
|
||||
durable next-turn injections can be disabled per plugin by setting that option
|
||||
to `false`.
|
||||
Conversation access and prompt mutation have separate permission gates; see
|
||||
[Permissions and scope](/plugins/hooks#permissions-and-scope) before enabling these hooks.
|
||||
|
||||
### Session extensions and next-turn injections
|
||||
|
||||
@@ -794,11 +982,16 @@ clients render plugin-owned status without learning plugin internals.
|
||||
the `api.session.state` namespace.
|
||||
|
||||
Use `api.session.workflow.enqueueNextTurnInjection(...)` when a plugin needs
|
||||
durable context to reach the next model turn exactly once (the top-level
|
||||
durable context queued for the next prompt build (the top-level
|
||||
`api.enqueueNextTurnInjection(...)` is a deprecated alias with the same
|
||||
behavior). OpenClaw drains queued injections before prompt hooks, drops
|
||||
expired injections, and deduplicates by `idempotencyKey` per plugin. This is
|
||||
the right seam for approval resumes, policy summaries, background monitor
|
||||
behavior). On the embedded and CLI prompt-preparation paths, OpenClaw drains
|
||||
queued injections before prompt hooks. It drops expired entries and entries
|
||||
whose plugin is inactive or has prompt injection disabled. `idempotencyKey`
|
||||
deduplicates unexpired pending entries for the same plugin and session; the
|
||||
key can be reused after consumption. Drained entries are reused across retries
|
||||
within the active run, but consuming an entry is not a receipt that the model
|
||||
saw it: a later failure can prevent submission. This is the right seam for
|
||||
approval resumes, policy summaries, background monitor
|
||||
deltas, and command continuations that should be visible to the model on the
|
||||
next turn but should not become permanent system prompt text.
|
||||
|
||||
@@ -810,8 +1003,22 @@ keeps durable session state while cleanup callbacks let plugins release
|
||||
scheduler jobs, run context, and other out-of-band resources for the old
|
||||
runtime generation.
|
||||
|
||||
Disable cleanup preserves model-locked sessions owned by that plugin's
|
||||
harness. Restart preserves extension state and pending injections, but can
|
||||
clear stale promoted top-level session fields.
|
||||
|
||||
## Message hooks
|
||||
|
||||
For inbound interception, `before_dispatch` receives the incoming message
|
||||
before ordinary model dispatch. Return `{ handled: true, text: "..." }` to
|
||||
send a final reply, or `{ handled: true }` to handle it without text. This is a
|
||||
claim, not an API for rewriting outbound or inbound content.
|
||||
|
||||
`reply_dispatch` is the advanced takeover seam: it receives the finalized
|
||||
message context and a host dispatcher, and a handled result reports
|
||||
`queuedFinal` and delivery `counts`. Use `before_agent_reply` for a simple
|
||||
synthetic reply, and the sending hooks below to transform outgoing payloads.
|
||||
|
||||
Use message hooks for channel-level routing and delivery policy:
|
||||
|
||||
- `message_received`: observe inbound content, sender, `threadId`,
|
||||
@@ -865,8 +1072,8 @@ Decision rules:
|
||||
|
||||
- `message_sending` with `cancel: true` is terminal.
|
||||
- `message_sending` with `cancel: false` is treated as no decision.
|
||||
- Rewritten `content` continues to lower-priority hooks unless a later hook
|
||||
cancels delivery.
|
||||
- Each `message_sending` handler receives the original event content. The last
|
||||
returned `content` wins; a later handler can still cancel delivery.
|
||||
- `reply_payload_sending` runs after payload normalization and before channel
|
||||
delivery, including replies routed back to the originating channel.
|
||||
Handlers run sequentially and each handler sees the latest payload produced
|
||||
@@ -887,11 +1094,12 @@ Use `security.installPolicy` for operator-owned allow/warn/block decisions. That
|
||||
policy runs from OpenClaw config, covers CLI install and update paths, and
|
||||
fails closed when enabled but unavailable.
|
||||
|
||||
`before_install` is a plugin-runtime lifecycle hook. It runs after
|
||||
`security.installPolicy` only in the OpenClaw process where plugin hooks have
|
||||
already been loaded, such as Gateway-backed install flows. It is useful for
|
||||
plugin-owned observations, warnings, and compatibility checks, but it is not
|
||||
the primary enterprise or host security boundary for installs. The
|
||||
`before_install` is a plugin-runtime lifecycle hook. It can run after
|
||||
`security.installPolicy` in a process where plugin hooks have already been
|
||||
loaded, such as Gateway-backed install flows. Trusted official and bundled
|
||||
install paths can skip this hook; they still run the operator install policy.
|
||||
It is useful for plugin-owned observations, warnings, and compatibility checks,
|
||||
but it is not the primary enterprise or host security boundary for installs. The
|
||||
`builtinScan` field remains in the event payload for compatibility, but
|
||||
OpenClaw no longer runs built-in install-time dangerous-code blocking, so it
|
||||
is an empty `ok` result. Return additional findings or
|
||||
@@ -934,9 +1142,9 @@ with a plugin-local readiness promise rather than depending on callback order.
|
||||
|
||||
`cron_changed` fires for Gateway-owned cron lifecycle events with a typed
|
||||
event payload covering `added`, `updated`, `removed`, `started`, `finished`,
|
||||
and `scheduled` reasons. The event carries a `PluginHookGatewayCronJob`
|
||||
and `scheduled` reasons. The event can include a `PluginHookGatewayCronJob`
|
||||
snapshot (including `state.nextRunAtMs`, `state.lastRunStatus`, and
|
||||
`state.lastError` when present) plus a `PluginHookGatewayCronDeliveryStatus`
|
||||
`state.lastError` when present) plus an optional `PluginHookGatewayCronDeliveryStatus`
|
||||
of `not-requested` | `delivered` | `not-delivered` | `unknown`. Removed events
|
||||
are post-commit: they fire only after durable deletion succeeds and still carry
|
||||
the deleted job snapshot so external schedulers can reconcile state.
|
||||
@@ -1101,14 +1309,26 @@ the next Gateway start emits a new authoritative `cron_reconciled` snapshot.
|
||||
`gateway_stop` aborts in-flight host work, waits for the worker to settle, then
|
||||
closes the adapter.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Check |
|
||||
| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Plugin loads but the handler never runs | Use `api.on` for typed names, inspect `openclaw plugins inspect <id> --runtime --json`, and check diagnostics for blocked registrations. Runtime inspection loads the plugin in the inspecting process; restart the Gateway too. |
|
||||
| Conversation hook is blocked | Set `plugins.entries.<id>.hooks.allowConversationAccess: true`; for prompt hooks, also check that `allowPromptInjection` is not `false`. These keys belong under `hooks`, not the plugin's `config`. |
|
||||
| Hook works for one runtime or trigger only | Check the runtime boundary and `eligibleTriggers`. Missing context fields are not proof of a different sender, agent, or authorization state. |
|
||||
| Persistence rewrite has no effect | Return `{ message }` synchronously. An `async` handler's result is ignored. |
|
||||
| A timed-out hook still performs work | Timeout ends the host's await, not plugin work. Pass available abort signals through I/O and bound plugin-owned work yourself. |
|
||||
| One plugin's rewrite disappears | Check the hook's merge rule and priority. `message_sending` uses the last returned content; `reply_payload_sending` passes each updated payload onward. |
|
||||
|
||||
## Upcoming deprecations
|
||||
|
||||
A few hook-adjacent surfaces are deprecated but still supported. Migrate
|
||||
before the next major release:
|
||||
|
||||
- **Plaintext channel envelopes** in `inbound_claim` and `message_received`
|
||||
handlers. Read `BodyForAgent` and the structured user-context blocks
|
||||
instead of parsing flat envelope text. See
|
||||
handlers. Prefer typed fields instead of parsing flat envelope text:
|
||||
`inbound_claim` exposes `event.bodyForAgent`; `message_received` exposes
|
||||
`event.content` and structured metadata, not a `BodyForAgent` field. See
|
||||
[Plaintext channel envelopes → BodyForAgent](/plugins/sdk-migration#removal-timeline).
|
||||
- **`onResolution` in `before_tool_call`** now uses the typed
|
||||
`PluginApprovalResolution` union (`allow-once` / `allow-always` / `deny` /
|
||||
@@ -1130,4 +1350,5 @@ accessors, and the `command-auth` → `command-status` rename - see
|
||||
- [Plugin SDK overview](/plugins/sdk-overview)
|
||||
- [Plugin entry points](/plugins/sdk-entrypoints)
|
||||
- [Internal hooks](/automation/hooks)
|
||||
- [Webhooks](/automation/cron-jobs#webhooks)
|
||||
- [Plugin architecture internals](/plugins/architecture-internals)
|
||||
|
||||
+139
-37
@@ -1,14 +1,22 @@
|
||||
---
|
||||
doc-schema-version: 1
|
||||
summary: "Webhooks plugin: authenticated TaskFlow ingress for trusted external automation"
|
||||
read_when:
|
||||
- You want to trigger or drive TaskFlows from an external system
|
||||
- You want to create or update TaskFlow records from an external system
|
||||
- You are configuring the bundled webhooks plugin
|
||||
title: "Webhooks plugin"
|
||||
---
|
||||
|
||||
The Webhooks plugin adds authenticated HTTP routes so a trusted external
|
||||
system (Zapier, n8n, a CI job, an internal service) can create and drive
|
||||
managed OpenClaw TaskFlows over HTTP, without writing a custom plugin.
|
||||
managed OpenClaw TaskFlow records over HTTP, without writing a custom plugin.
|
||||
`create_flow` creates a tracking record; `run_task` creates or links a child task
|
||||
record. Neither operation starts an agent. The external controller owns the
|
||||
workflow and advances its state.
|
||||
|
||||
To submit an agent turn from an external event, use [Gateway HTTP
|
||||
hooks](/automation/cron-jobs#webhooks). To react to internal agent events, use
|
||||
[internal hooks](/automation/hooks). Those surfaces do not share this plugin's routes or authentication.
|
||||
|
||||
The plugin runs inside the Gateway process. For a remote Gateway, install and
|
||||
configure it on that host, then restart the Gateway. It ships with no routes
|
||||
@@ -28,7 +36,7 @@ Set config under `plugins.entries.webhooks.config`:
|
||||
routes: {
|
||||
zapier: {
|
||||
path: "/plugins/webhooks/zapier",
|
||||
sessionKey: "agent:main:main",
|
||||
sessionKey: "agent:main:hook:automation",
|
||||
secret: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
@@ -67,6 +75,12 @@ on the public request path.
|
||||
|
||||
## Security model
|
||||
|
||||
Each route authenticates with its own `secret`, not `hooks.token` or the Gateway
|
||||
auth token. Use HTTPS outside loopback. The endpoint accepts the action schema
|
||||
below, not arbitrary provider webhook payloads, URL verification challenges, or
|
||||
provider-specific HMAC signatures. Use an existing automation service to validate
|
||||
and translate external events when needed. Treat event content as data.
|
||||
|
||||
Each route acts with the TaskFlow authority of its configured `sessionKey`: it
|
||||
can inspect and mutate any TaskFlow owned by that session. TaskFlow access
|
||||
always goes through `api.runtime.tasks.managedFlows.bindSession(...)`, so a
|
||||
@@ -77,13 +91,15 @@ route can never act outside its bound session. To limit blast radius:
|
||||
- Bind routes to the narrowest session that fits the workflow.
|
||||
- Expose only the specific webhook path you need.
|
||||
|
||||
Request handling order for each path: HTTP method (`POST` only) and
|
||||
`Content-Type: application/json` checks, then fixed-window rate limiting (120
|
||||
requests per 60-second window per path+client-IP key, up to 4,096 tracked
|
||||
keys), then in-flight request limiting (8 concurrent requests per key, up to
|
||||
4,096 tracked keys), then shared-secret authentication, then a 256 KB /
|
||||
15-second JSON body read. Requests that fail an earlier check never reach
|
||||
later ones.
|
||||
Request handling order is: `POST` method, fixed-window rate limit, JSON content
|
||||
type, in-flight limit, shared-secret authentication, bounded JSON body read, then
|
||||
action validation. Earlier failures do not reach later checks.
|
||||
|
||||
| Limit | Scope |
|
||||
| ---------------------------- | ---------------------------------------------------------------------------------------------------------- |
|
||||
| 120 requests per 60 seconds | Path plus client IP resolved using trusted proxy settings; at most 4,096 tracked keys. |
|
||||
| 8 concurrent requests | Path plus socket peer address; at most 4,096 tracked keys. Clients behind one proxy can share this bucket. |
|
||||
| 256 KiB body, 15-second read | Per request. This is a body-read timeout, not an agent-run timeout. |
|
||||
|
||||
## Request format
|
||||
|
||||
@@ -91,33 +107,56 @@ Send `POST` requests with `Content-Type: application/json` and either
|
||||
`Authorization: Bearer <secret>` or `x-openclaw-webhook-secret: <secret>`:
|
||||
|
||||
```bash
|
||||
curl -X POST https://gateway.example.com/plugins/webhooks/zapier \
|
||||
curl --include https://gateway.example.com/plugins/webhooks/zapier \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer YOUR_SHARED_SECRET' \
|
||||
-H 'Authorization: Bearer <route-secret>' \
|
||||
-d '{"action":"create_flow","goal":"Review inbound queue"}'
|
||||
```
|
||||
|
||||
The successful response contains `result.created: true` and `result.flow` with
|
||||
its `flowId`, `revision` (initially `0`), and status (default `queued`). Keep the
|
||||
flow id and read it back using the same route:
|
||||
|
||||
```bash
|
||||
curl --include https://gateway.example.com/plugins/webhooks/zapier \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'Authorization: Bearer <route-secret>' \
|
||||
--data '{"action":"get_flow","flowId":"<returned-flow-id>"}'
|
||||
```
|
||||
|
||||
HTTP `200` confirms a read or record operation, not completed agent work or
|
||||
message delivery. Use `get_task_summary` for linked task counts and
|
||||
[task inspection](/cli/tasks) for actual task status and delivery status. Keep
|
||||
those outcomes separate from the controller's flow status.
|
||||
|
||||
## Supported actions
|
||||
|
||||
| Action | Purpose |
|
||||
| ------------------ | ------------------------------------------------------------------ |
|
||||
| `create_flow` | Create a managed TaskFlow for the route's session. |
|
||||
| `get_flow` | Fetch one TaskFlow by id. |
|
||||
| `list_flows` | List TaskFlows for the route's session. |
|
||||
| `find_latest_flow` | Fetch the most recently updated TaskFlow. |
|
||||
| `resolve_flow` | Resolve a TaskFlow by opaque token. |
|
||||
| `get_task_summary` | Fetch the task summary for a TaskFlow. |
|
||||
| `set_waiting` | Mark a TaskFlow waiting, with optional state/wait data. |
|
||||
| `resume_flow` | Resume a waiting/blocked TaskFlow. |
|
||||
| `finish_flow` | Mark a TaskFlow finished. |
|
||||
| `fail_flow` | Mark a TaskFlow failed. |
|
||||
| `request_cancel` | Request cooperative cancellation. |
|
||||
| `cancel_flow` | Cancel a TaskFlow (may return `202` if children are still active). |
|
||||
| `run_task` | Create a managed child task inside an existing TaskFlow. |
|
||||
| Action | Purpose |
|
||||
| ------------------ | --------------------------------------------------------------------------- |
|
||||
| `create_flow` | Create a managed TaskFlow record for the route's session. |
|
||||
| `get_flow` | Fetch one TaskFlow by id. |
|
||||
| `list_flows` | List the session's TaskFlows, newest created first. |
|
||||
| `find_latest_flow` | Fetch the most recently created TaskFlow. |
|
||||
| `resolve_flow` | Resolve by flow id or the exact bound session key (its latest flow). |
|
||||
| `get_task_summary` | Fetch the task summary for a TaskFlow. |
|
||||
| `set_waiting` | Mark a TaskFlow waiting, with optional state/wait data. |
|
||||
| `resume_flow` | Resume a waiting/blocked TaskFlow. |
|
||||
| `finish_flow` | Mark a TaskFlow finished. |
|
||||
| `fail_flow` | Mark a TaskFlow failed. |
|
||||
| `request_cancel` | Record a cooperative cancellation request; does not cancel children itself. |
|
||||
| `cancel_flow` | Cancel a TaskFlow (may return `202` if children are still active). |
|
||||
| `run_task` | Create or link a managed child task record; does not start a run. |
|
||||
|
||||
Mutating actions (`set_waiting`, `resume_flow`, `finish_flow`, `fail_flow`,
|
||||
`request_cancel`) require `flowId` and `expectedRevision` for optimistic
|
||||
concurrency; a stale revision returns `409 revision_conflict`.
|
||||
concurrency; a stale revision returns `409 revision_conflict`. Read `result.current`
|
||||
or call `get_flow`, reconcile the change, then use its current revision.
|
||||
`cancel_flow` and `run_task` do not take `expectedRevision`. Action schemas reject
|
||||
unknown fields.
|
||||
|
||||
Read actions can return HTTP `200` with `flow: null` or `summary: null` when the
|
||||
flow is absent or outside the route's session. A successful read does not imply
|
||||
that a matching flow exists.
|
||||
|
||||
### `create_flow`
|
||||
|
||||
@@ -130,26 +169,70 @@ concurrency; a stale revision returns `409 revision_conflict`.
|
||||
}
|
||||
```
|
||||
|
||||
`goal` is required. Optional fields are `controllerId`, `status` (`queued`,
|
||||
`running`, `waiting`, `blocked`), `notifyPolicy` (`done_only`, `state_changes`,
|
||||
`silent`), `currentStep`, `stateJson`, and `waitJson`. The request's `controllerId`
|
||||
overrides the route default; it is not a separate authorization boundary.
|
||||
|
||||
Creation has no general idempotency key: retrying `create_flow` after an uncertain
|
||||
connection result can create a second flow. Reconcile with `list_flows` before
|
||||
repeating creation. Child success alone does not mark a managed flow finished;
|
||||
the controller must advance or finish the flow when appropriate.
|
||||
|
||||
### `run_task`
|
||||
|
||||
Allowed `runtime` values: `subagent`, `acp`. `startedAt`, `lastEventAt`, and
|
||||
Required fields are `flowId`, `runtime`, and `task`. Allowed `runtime` values are
|
||||
`subagent` and `acp`; `status` defaults to `queued` and can be `running`. `startedAt`, `lastEventAt`, and
|
||||
`progressSummary` are only valid when `status` is `"running"`; sending them
|
||||
with any other status returns `400 invalid_request`.
|
||||
|
||||
The following links an **already existing**, currently owned backing run. Use
|
||||
the managed flow id returned by `create_flow`, and the `childSessionKey` and
|
||||
`runId` from that existing run. Inventing a child key or run id does not start
|
||||
work or grant authority.
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "run_task",
|
||||
"flowId": "flow_123",
|
||||
"flowId": "<flow-id>",
|
||||
"runtime": "acp",
|
||||
"childSessionKey": "agent:main:acp:worker",
|
||||
"runId": "run_123",
|
||||
"childSessionKey": "<existing-child-session-key>",
|
||||
"runId": "<existing-run-id>",
|
||||
"task": "Inspect the next message batch"
|
||||
}
|
||||
```
|
||||
|
||||
`childSessionKey` identifies the backing run but does not grant authority over it. For automatic
|
||||
lifecycle tracking and cancellation, include the exact `runId`; the backing task must be owned by
|
||||
the route's configured session. Foreign, stale, or replaced runs are rejected at use time.
|
||||
`childSessionKey` requires the exact `runId` and current backing ownership by the
|
||||
route's configured session. Foreign, stale, or replaced runs are rejected at use
|
||||
time. Omit `childSessionKey` to create an unbacked tracking record; supplying an
|
||||
invalid backing reference is rejected. Reuse by
|
||||
`runId` is scoped to the runtime, owner, child, and flow; it is not a universal
|
||||
request-replay guarantee.
|
||||
|
||||
Optional metadata includes `sourceId`, `parentTaskId`, `agentId`, `label`,
|
||||
`preferMetadata`, and `notifyPolicy` (`done_only`, `state_changes`, `silent`).
|
||||
`startedAt` and `lastEventAt` are nonnegative integer timestamps in milliseconds.
|
||||
|
||||
### Waiting and completion
|
||||
|
||||
`set_waiting` accepts `currentStep`, `stateJson`, `waitJson`, `blockedTaskId`, and
|
||||
`blockedSummary`. A nonempty blocked field selects `blocked`; otherwise the flow
|
||||
becomes `waiting`. `resume_flow` accepts `status` (`queued` default or `running`),
|
||||
`currentStep`, and `stateJson`, and clears waiting/blocked state.
|
||||
|
||||
`finish_flow` marks the flow `succeeded` and accepts `stateJson`; `fail_flow` marks
|
||||
it `failed` and also accepts `blockedTaskId` and `blockedSummary`. Optional string
|
||||
fields accept `null` to clear them; `stateJson` and `waitJson` accept any JSON
|
||||
value, including retained JSON `null`. Use the current `expectedRevision` for
|
||||
each transition.
|
||||
|
||||
### Cancellation
|
||||
|
||||
`request_cancel` records cancellation intent. `cancel_flow` attempts cancellation
|
||||
of linked work. If children remain active, the response is HTTP `202` with
|
||||
`ok: true`, `code: "cancel_pending"`, and `result.cancelled: false`. Check
|
||||
`get_flow` and `get_task_summary` afterward; `202` does not mean cancellation
|
||||
finished.
|
||||
|
||||
## Response shape
|
||||
|
||||
@@ -171,14 +254,33 @@ the route's configured session. Foreign, stale, or replaced runs are rejected at
|
||||
}
|
||||
```
|
||||
|
||||
Flow and task views never include owner/session metadata, so responses cannot
|
||||
leak the route's bound `sessionKey`. `code` values include `not_found`,
|
||||
Flow and task views omit owner/requester metadata such as `ownerKey`,
|
||||
`requesterSessionKey`, and `requesterOrigin`. Task views can still include
|
||||
`childSessionKey`, `agentId`, and `runId`; treat responses as operational data.
|
||||
`code` values include `not_found`,
|
||||
`not_managed`, `revision_conflict`, `persist_failed`, `cancel_requested`,
|
||||
`cancel_pending`, `terminal`, `invalid_request`, `request_rejected`, and
|
||||
action-specific fallback codes (`mutation_rejected`, `create_rejected`,
|
||||
`task_not_created`, `cancel_rejected`) when a mutation is rejected for a
|
||||
reason not covered by the named codes above.
|
||||
|
||||
### Errors and troubleshooting
|
||||
|
||||
| Response | Next check |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| `401` | Use the route secret. An unresolved SecretRef leaves that route cold; repair it and reload/restart. |
|
||||
| `405` / `415` | Send `POST` with `Content-Type: application/json`. |
|
||||
| `408` / `413` | Send the JSON body within 15 seconds and below 256 KiB. |
|
||||
| `429` | Reduce request rate or concurrent requests, including clients sharing a proxy. |
|
||||
| `400 invalid_request` | Check the action's fields and types; unknown fields are rejected. |
|
||||
| `404 not_found` | The mutation target does not exist in the route's bound session. |
|
||||
| `409` | Inspect `code`: reconcile revisions, managed-flow status, cancellation state, or backing-run ownership. |
|
||||
| `503 persist_failed` | The record could not be persisted; investigate Gateway storage/logs before retrying. |
|
||||
|
||||
Failures before action validation can be plain text, not the JSON envelope
|
||||
above. A Bearer header takes precedence over `x-openclaw-webhook-secret`;
|
||||
query-string and body tokens are not authentication methods for this plugin.
|
||||
|
||||
## Related
|
||||
|
||||
- [Hooks](/automation/hooks) - internal event-driven hooks vs. this HTTP-based TaskFlow bridge
|
||||
|
||||
+10
-7
@@ -95,8 +95,8 @@ bundled, official external, and source-only plugins, see
|
||||
openclaw gateway restart
|
||||
```
|
||||
|
||||
Enable/disable update config and the cold registry. A runtime inspect is
|
||||
still the clearest proof of live runtime surfaces.
|
||||
Enable/disable update config and the cold registry. Inspect registration
|
||||
next, then verify the running Gateway with an actual hook event or tool call.
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -105,9 +105,11 @@ bundled, official external, and source-only plugins, see
|
||||
openclaw plugins inspect <plugin-id> --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.
|
||||
|
||||
</Step>
|
||||
</Steps>
|
||||
@@ -283,8 +285,9 @@ An explicit hook policy is also startup intent. For example,
|
||||
`plugins.entries.<id>.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 <id> --runtime --json`.
|
||||
policy, inspect registration with `openclaw plugins inspect <id> --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
|
||||
|
||||
|
||||
+42
-201
@@ -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 `<state-dir>/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 `<workspace>/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**: `<workspace>/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 `<state-dir>/logs/commands.log` for that command.
|
||||
|
||||
Each hook is a directory containing:
|
||||
With multiple agents, use `--agent <id>` 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**: `<workspace>/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<string, unknown>;
|
||||
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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.<id>.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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user