docs: clarify before_install hook scope (#92766)

Signed-off-by: sallyom <somalley@redhat.com>
This commit is contained in:
Sally O'Malley
2026-06-13 16:54:04 -04:00
committed by GitHub
parent 8047350445
commit e6ffcf7362
10 changed files with 45 additions and 29 deletions
+2 -2
View File
@@ -159,7 +159,7 @@ is available, then fall back to `latest`.
<Accordion title="--dangerously-force-unsafe-install">
`--dangerously-force-unsafe-install` is deprecated and is now a no-op. OpenClaw no longer runs built-in install-time dangerous-code blocking for plugin installs.
Use the shared operator-owned `security.installPolicy` surface when host-specific install policy is required. Plugin `before_install` hooks and `security.installPolicy` can still block installs.
Use the shared operator-owned `security.installPolicy` surface when host-specific install policy is required. Plugin `before_install` hooks are plugin-runtime lifecycle hooks and are not the primary policy boundary for CLI installs.
If a plugin you published on ClawHub is hidden or blocked by a registry scan, use the publisher steps in [ClawHub publishing](/clawhub/publishing). `--dangerously-force-unsafe-install` does not ask ClawHub to rescan the plugin or make a blocked release public.
@@ -405,7 +405,7 @@ Updates apply to tracked plugin installs in the managed plugin index and tracked
</Accordion>
<Accordion title="--dangerously-force-unsafe-install on update">
`--dangerously-force-unsafe-install` is also accepted on `plugins update` for compatibility, but it is deprecated and no longer changes plugin update behavior. Operator `security.installPolicy` and plugin `before_install` hooks can still block updates.
`--dangerously-force-unsafe-install` is also accepted on `plugins update` for compatibility, but it is deprecated and no longer changes plugin update behavior. Operator `security.installPolicy` can still block updates; plugin `before_install` hooks only apply in processes where plugin hooks are loaded.
</Accordion>
</AccordionGroup>
+2 -1
View File
@@ -97,7 +97,7 @@ These run inside the agent loop or gateway pipeline:
- **`agent_end`**: inspect the final message list and run metadata after completion.
- **`before_compaction` / `after_compaction`**: observe or annotate compaction cycles.
- **`before_tool_call` / `after_tool_call`**: intercept tool params/results.
- **`before_install`**: inspect install context and optionally block skill or plugin installs after operator install policy runs.
- **`before_install`**: inspect staged skill or plugin install material after operator install policy runs, when plugin hooks are loaded in the current OpenClaw process.
- **`tool_result_persist`**: synchronously transform tool results before they are written to an OpenClaw-owned session transcript.
- **`message_received` / `message_sending` / `message_sent`**: inbound + outbound message hooks.
- **`session_start` / `session_end`**: session lifecycle boundaries.
@@ -109,6 +109,7 @@ Hook decision rules for outbound/tool guards:
- `before_tool_call`: `{ block: false }` is a no-op and does not clear a prior block.
- `before_install`: `{ block: true }` is terminal and stops lower-priority handlers.
- `before_install`: `{ block: false }` is a no-op and does not clear a prior block.
- Use `security.installPolicy`, not `before_install`, for operator-owned install allow/block decisions that must cover CLI install and update paths.
- `message_sending`: `{ cancel: true }` is terminal and stops lower-priority handlers.
- `message_sending`: `{ cancel: false }` is a no-op and does not clear a prior cancel.
+15 -6
View File
@@ -152,7 +152,8 @@ observation-only.
- `gateway_start` / `gateway_stop` - start or stop plugin-owned services with the Gateway
- `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 context and optionally block
- **`before_install`** - inspect staged skill or plugin install material from a loaded
plugin runtime
## Debug runtime hooks
@@ -462,11 +463,19 @@ Decision rules:
## Install hooks
`before_install` runs after the operator-owned `security.installPolicy` check
when one is configured. 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
`{ block: true, blockReason }` to stop the install.
Use `security.installPolicy` for operator-owned allow/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 `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 `{ block: true, blockReason }` to stop the
install in that process.
`block: true` is terminal. `block: false` is treated as no decision.
Handler failures block the install fail-closed.
+4
View File
@@ -431,6 +431,10 @@ semantics.
### Hook decision semantics
`before_install` is a plugin-runtime lifecycle hook, not the operator install
policy surface. Use `security.installPolicy` when an allow/block decision must
cover CLI and Gateway-backed install or update paths.
- `before_tool_call`: returning `{ block: true }` is terminal. Once any handler sets it, lower-priority handlers are skipped.
- `before_tool_call`: returning `{ block: false }` is treated as no decision (same as omitting `block`), not as an override.
- `before_install`: returning `{ block: true }` is terminal. Once any handler sets it, lower-priority handlers are skipped.
+6 -4
View File
@@ -147,10 +147,12 @@ such as `@beta` stay pinned to the selected package and fail when incompatible.
Configure `security.installPolicy` to run a trusted local policy command before
plugin install or update proceeds. The policy receives metadata plus the staged
source path and can allow or block the install. It runs before plugin
`before_install` hooks. The deprecated `--dangerously-force-unsafe-install`
flag is accepted for compatibility but does not bypass install policy, hooks, or
OpenClaw's built-in plugin dependency denylist.
source path and can allow or block the install. It covers CLI and Gateway-backed
plugin install/update paths. Plugin `before_install` hooks run later only in
OpenClaw processes where plugin hooks are loaded, so use `security.installPolicy`
for operator-owned install decisions. The deprecated
`--dangerously-force-unsafe-install` flag is accepted for compatibility but does
not bypass install policy or OpenClaw's built-in plugin dependency denylist.
See [Skills config](/tools/skills-config#operator-install-policy-securityinstallpolicy)
for the shared `security.installPolicy` exec schema used by both skills and
+2 -2
View File
@@ -153,7 +153,7 @@ export function registerPluginsCli(program: Command) {
.option("--pin", "Record npm installs as exact resolved <name>@<version>", false)
.option(
"--dangerously-force-unsafe-install",
"Deprecated no-op; install policy and plugin hooks may still block",
"Deprecated no-op; security.installPolicy may still block",
false,
)
.option(
@@ -184,7 +184,7 @@ export function registerPluginsCli(program: Command) {
.option("--dry-run", "Show what would change without writing", false)
.option(
"--dangerously-force-unsafe-install",
"Deprecated no-op; install policy and plugin hooks may still block",
"Deprecated no-op; security.installPolicy may still block",
false,
)
.action(async (id: string | undefined, opts: PluginUpdateOptions) => {
+2 -2
View File
@@ -78,8 +78,8 @@ describe("plugins cli update", () => {
expect(helpText).toContain("--dangerously-force-unsafe-install");
expect(helpText).toContain("Deprecated no-op");
expect(helpText).toContain("install policy and");
expect(helpText).toContain("plugin hooks may still block");
expect(helpText).toContain("security.installPolicy");
expect(helpText).toContain("may still block");
});
it("refuses plugin updates in Nix mode before package-manager work", async () => {
+1 -1
View File
@@ -1,4 +1,4 @@
// Builds install policy context for plugin install checks.
// Builds payloads for the plugin-runtime before_install lifecycle hook.
import type {
PluginHookBeforeInstallBuiltinScan,
PluginHookBeforeInstallContext,
+8 -8
View File
@@ -1970,7 +1970,7 @@ describe("installPluginFromArchive", () => {
);
});
it("surfaces plugin scanner findings from before_install", async () => {
it("surfaces plugin lifecycle findings from before_install", async () => {
const handler = vi.fn().mockReturnValue({
findings: [
{
@@ -2052,10 +2052,10 @@ describe("installPluginFromArchive", () => {
expect(requests[1]?.request.requestedSpecifier).toBe(pluginDir);
});
it("blocks plugin install when before_install rejects dangerous-looking source", async () => {
it("blocks plugin install when before_install rejects the staged source", async () => {
const handler = vi.fn().mockReturnValue({
block: true,
blockReason: "Blocked by enterprise policy",
blockReason: "Blocked by plugin lifecycle hook",
});
initializeGlobalHookRunner(createMockPluginRegistry([{ hookName: "before_install", handler }]));
@@ -2078,7 +2078,7 @@ describe("installPluginFromArchive", () => {
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toBe("Blocked by enterprise policy");
expect(result.error).toBe("Blocked by plugin lifecycle hook");
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
}
expect(handler).toHaveBeenCalledTimes(1);
@@ -2098,14 +2098,14 @@ describe("installPluginFromArchive", () => {
extensions: ["index.js"],
});
expect(
warnings.some((w) => w.includes("blocked by plugin hook: Blocked by enterprise policy")),
warnings.some((w) => w.includes("blocked by plugin hook: Blocked by plugin lifecycle hook")),
).toBe(true);
});
it("keeps before_install hook blocks even when dangerous force unsafe install is set", async () => {
const handler = vi.fn().mockReturnValue({
block: true,
blockReason: "Blocked by enterprise policy",
blockReason: "Blocked by plugin lifecycle hook",
});
initializeGlobalHookRunner(createMockPluginRegistry([{ hookName: "before_install", handler }]));
@@ -2132,7 +2132,7 @@ describe("installPluginFromArchive", () => {
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toBe("Blocked by enterprise policy");
expect(result.error).toBe("Blocked by plugin lifecycle hook");
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
}
expect(
@@ -2144,7 +2144,7 @@ describe("installPluginFromArchive", () => {
).toBe(false);
expect(
warnings.some((warning) =>
warning.includes("blocked by plugin hook: Blocked by enterprise policy"),
warning.includes("blocked by plugin hook: Blocked by plugin lifecycle hook"),
),
).toBe(true);
});
+3 -3
View File
@@ -109,7 +109,7 @@ async function withWorkspaceCase(
}
}
describe("installSkill install policy hooks", () => {
describe("installSkill before_install hooks", () => {
beforeEach(() => {
resetGlobalHookRunner();
runCommandWithTimeoutMock.mockClear();
@@ -273,7 +273,7 @@ describe("installSkill install policy hooks", () => {
it("blocks install when before_install rejects the skill", async () => {
const handler = vi.fn().mockReturnValue({
block: true,
blockReason: "Blocked by enterprise policy",
blockReason: "Blocked by plugin lifecycle hook",
});
initializeGlobalHookRunner(createMockPluginRegistry([{ hookName: "before_install", handler }]));
@@ -287,7 +287,7 @@ describe("installSkill install policy hooks", () => {
});
expect(result.ok).toBe(false);
expect(result.message).toBe("Blocked by enterprise policy");
expect(result.message).toBe("Blocked by plugin lifecycle hook");
expect(runCommandWithTimeoutMock).not.toHaveBeenCalled();
});
});