diff --git a/CHANGELOG.md b/CHANGELOG.md
index b24935f9299..0aebd1f9d1d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,7 +20,7 @@ Docs: https://docs.openclaw.ai
- Telegram: persist polling updates through restart replay so queued same-topic messages resume in order instead of losing context after a gateway restart. (#82256) Thanks @VACInc.
- Gateway/Gmail: abort in-flight Gmail watcher startup and hot-reload restarts before shutdown so reloads cannot spawn `gog serve` after the Gateway is closing. Thanks @frankekn.
- MCP plugin tools: forward host MCP `tools/call` `AbortSignal` through `createPluginToolsMcpHandlers().callTool` into plugin `tool.execute`, so host cancellation actually cancels in-flight plugin tool calls instead of letting them run to completion. Fixes #82424. (#82443) Thanks @joshavant.
-- Plugins: accept `api.on("deactivate")` as a compatibility alias for `gateway_stop`, so external plugin cleanup handlers run on Gateway shutdown instead of being ignored as unknown hooks.
+- Plugins: accept deprecated `api.on("deactivate")` registrations as a dated compatibility alias for `gateway_stop`, so external plugin cleanup handlers run on Gateway shutdown while authors get migration guidance.
- Media: ignore image MIME and filename hints when bytes sniff as generic containers, so zip/octet-stream payloads mislabeled as images do not become local image media or keep image file extensions when staged.
- Update/doctor: avoid materializing `groupAllowFrom` for channel schemas that reject it, so package-swap doctor repairs do not fail on externalized Slack configs.
- Gateway/media: prevent image filenames from overriding generic non-image byte sniffing, so zip/octet-stream payloads mislabeled as images are offloaded or rejected before they become inline image attachments.
diff --git a/docs/plugins/compatibility.md b/docs/plugins/compatibility.md
index 6218a68357e..b637e2b04d1 100644
--- a/docs/plugins/compatibility.md
+++ b/docs/plugins/compatibility.md
@@ -112,6 +112,8 @@ Current compatibility records include:
- legacy broad SDK imports such as `openclaw/plugin-sdk/compat`
- legacy hook-only plugin shapes and `before_agent_start`
+- legacy `api.on("deactivate", ...)` cleanup hook names while plugins migrate to
+ `gateway_stop`
- legacy `activate(api)` plugin entrypoints while plugins migrate to
`register(api)`
- legacy SDK aliases such as `openclaw/extension-api`,
diff --git a/docs/plugins/hooks.md b/docs/plugins/hooks.md
index 8ba45000740..e77e94fba3a 100644
--- a/docs/plugins/hooks.md
+++ b/docs/plugins/hooks.md
@@ -145,7 +145,7 @@ observation-only.
**Lifecycle**
- `gateway_start` / `gateway_stop` - start or stop plugin-owned services with the Gateway
-- `deactivate` - compatibility alias for `gateway_stop`; prefer `gateway_stop` in new plugins
+- `deactivate` - deprecated compatibility alias for `gateway_stop`; use `gateway_stop` in new plugins
- `cron_changed` - observe gateway-owned cron lifecycle changes (added, updated, removed, started, finished, scheduled)
- **`before_install`** - inspect skill or plugin install scans and optionally block
@@ -438,8 +438,8 @@ before the next major release:
- **`before_agent_start`** remains for compatibility. New plugins should use
`before_model_resolve` and `before_prompt_build` instead of the combined
phase.
-- **`deactivate`** remains as a cleanup compatibility alias. New plugins should
- use `gateway_stop`.
+- **`deactivate`** remains as a deprecated cleanup compatibility alias until
+ after 2026-08-16. New plugins should use `gateway_stop`.
- **`onResolution` in `before_tool_call`** now uses the typed
`PluginApprovalResolution` union (`allow-once` / `allow-always` / `deny` /
`timeout` / `cancelled`) instead of a free-form `string`.
diff --git a/docs/plugins/sdk-migration.md b/docs/plugins/sdk-migration.md
index cc08df12f93..f9d1bc1594d 100644
--- a/docs/plugins/sdk-migration.md
+++ b/docs/plugins/sdk-migration.md
@@ -757,6 +757,29 @@ canonical replacement.
+
+ **Old**: `api.on("deactivate", handler)`.
+
+ **New**: `api.on("gateway_stop", handler)`. The event and context are the
+ same shutdown cleanup contract; only the hook name changes.
+
+ ```typescript
+ // Before
+ api.on("deactivate", async (event, ctx) => {
+ await stopPluginService(ctx);
+ });
+
+ // After
+ api.on("gateway_stop", async (event, ctx) => {
+ await stopPluginService(ctx);
+ });
+ ```
+
+ `deactivate` remains wired as a deprecated compatibility alias until after
+ 2026-08-16.
+
+
+
Four discovery type aliases are now thin wrappers over the
catalog-era types:
diff --git a/src/plugins/compat/registry.test.ts b/src/plugins/compat/registry.test.ts
index c5da9cb02d4..d963edc8b19 100644
--- a/src/plugins/compat/registry.test.ts
+++ b/src/plugins/compat/registry.test.ts
@@ -135,6 +135,11 @@ const knownDeprecatedSurfaceMarkers = [
file: "src/plugin-sdk/compat.ts",
marker: "@deprecated Use `openclaw/plugin-sdk/channel-message`.",
},
+ {
+ code: "legacy-deactivate-hook-alias",
+ file: "src/plugins/hook-types.ts",
+ marker: "@deprecated Use gateway_stop",
+ },
{
code: "channel-route-key-aliases",
file: "src/plugin-sdk/channel-route.ts",
diff --git a/src/plugins/compat/registry.ts b/src/plugins/compat/registry.ts
index 4161a828b9e..48cee0aa639 100644
--- a/src/plugins/compat/registry.ts
+++ b/src/plugins/compat/registry.ts
@@ -23,6 +23,22 @@ export const PLUGIN_COMPAT_RECORDS = [
releaseNote:
"Legacy `before_agent_start` hook compatibility remains wired while plugins migrate to modern hook stages.",
},
+ {
+ code: "legacy-deactivate-hook-alias",
+ status: "deprecated",
+ owner: "sdk",
+ introduced: "2026-05-16",
+ deprecated: "2026-05-16",
+ warningStarts: "2026-05-16",
+ removeAfter: "2026-08-16",
+ replacement: "`gateway_stop` hook",
+ docsPath: "/plugins/hooks#upcoming-deprecations",
+ surfaces: ["api.on(\"deactivate\", ...)", "plugin typed hook registration"],
+ diagnostics: ["plugin runtime compatibility warning"],
+ tests: ["src/plugins/loader.test.ts"],
+ releaseNote:
+ "`api.on(\"deactivate\", ...)` remains wired as a deprecated compatibility alias while plugins migrate to `gateway_stop`.",
+ },
{
code: "hook-only-plugin-shape",
status: "active",
diff --git a/src/plugins/hook-types.ts b/src/plugins/hook-types.ts
index 9a9555c8183..57add9558f1 100644
--- a/src/plugins/hook-types.ts
+++ b/src/plugins/hook-types.ts
@@ -94,6 +94,7 @@ export type PluginHookName =
| "subagent_delivery_target"
| "subagent_spawned"
| "subagent_ended"
+ /** @deprecated Use gateway_stop. */
| "deactivate"
| "gateway_start"
| "gateway_stop"
@@ -1003,11 +1004,13 @@ export type PluginHookHandlerMap = {
ctx: PluginHookSubagentContext,
) => Promise | void;
/**
- * Compatibility alias for gateway_stop.
+ * Deprecated compatibility alias for gateway_stop.
*
* New plugins should register gateway_stop directly; the loader normalizes
* deactivate registrations onto gateway_stop so cleanup handlers still run
* during Gateway shutdown.
+ *
+ * @deprecated Use gateway_stop.
*/
deactivate: (
event: PluginHookGatewayStopEvent,
diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts
index 542ae99d7a0..f201325844d 100644
--- a/src/plugins/loader.test.ts
+++ b/src/plugins/loader.test.ts
@@ -5939,7 +5939,8 @@ module.exports = {
registry.diagnostics.some(
(diag) =>
diag.pluginId === "legacy-deactivate-hook" &&
- diag.message === 'typed hook "deactivate" is a compatibility alias for "gateway_stop"',
+ diag.message ===
+ 'typed hook "deactivate" is deprecated (legacy-deactivate-hook-alias); use "gateway_stop". This compatibility alias will be removed after 2026-08-16.',
),
).toBe(true);
});
diff --git a/src/plugins/registry.ts b/src/plugins/registry.ts
index 84d52bb2e60..1c784af7e1d 100644
--- a/src/plugins/registry.ts
+++ b/src/plugins/registry.ts
@@ -51,6 +51,7 @@ import {
import { buildPluginApi } from "./api-builder.js";
import { normalizeRegisteredChannelPlugin } from "./channel-validation.js";
import { CODEX_APP_SERVER_EXTENSION_RUNTIME_ID } from "./codex-app-server-extension-factory.js";
+import { getPluginCompatRecord } from "./compat/registry.js";
import type { CodexAppServerExtensionFactory } from "./codex-app-server-extension-types.js";
import {
isReservedCommandName,
@@ -187,6 +188,16 @@ export type PluginHttpRouteRegistration = RegistryTypesPluginHttpRouteRegistrati
};
const GATEWAY_METHOD_DISPATCH_CONTRACT = "authenticated-request";
+const LEGACY_DEACTIVATE_HOOK_ALIAS_COMPAT = getPluginCompatRecord("legacy-deactivate-hook-alias");
+
+function formatLegacyDeactivateHookAliasDiagnostic(): string {
+ const removeAfter =
+ LEGACY_DEACTIVATE_HOOK_ALIAS_COMPAT.removeAfter ?? "a future breaking release";
+ return (
+ `typed hook "deactivate" is deprecated (${LEGACY_DEACTIVATE_HOOK_ALIAS_COMPAT.code}); ` +
+ `use "gateway_stop". This compatibility alias will be removed after ${removeAfter}.`
+ );
+}
type PluginOwnedProviderRegistration = {
pluginId: string;
@@ -2304,7 +2315,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
level: "warn",
pluginId: record.id,
source: record.source,
- message: 'typed hook "deactivate" is a compatibility alias for "gateway_stop"',
+ message: formatLegacyDeactivateHookAliasDiagnostic(),
});
}
let effectiveHandler = handler;