Skip to content

Commit fe62a57

Browse files
committed
feat: check ClawHub trust before plugin installs
1 parent 8fe196e commit fe62a57

23 files changed

Lines changed: 832 additions & 43 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2458,6 +2458,7 @@ Docs: https://docs.openclaw.ai
24582458
- CLI/models: declare fixed Qianfan, Xiaomi, NVIDIA, Cerebras, Mistral, Chutes, Kilo, OpenAI, and OpenCode Go model catalogs in refreshable plugin manifests, keep broad `models list --all` on raw registry and supplement rows without runtime normalization, and avoid duplicate supplement resolution. Thanks @shakkernerd.
24592459
- Gateway/runtime: reuse the current plugin metadata snapshot for provider discovery so repeated model-provider discovery avoids rebuilding plugin manifest metadata. Thanks @shakkernerd.
24602460
- Gateway/startup: pass the plugin metadata snapshot from config validation into plugin bootstrap so startup reuses one manifest product instead of rebuilding plugin metadata. Thanks @shakkernerd.
2461+
- Plugins/ClawHub: check exact-release trust before ClawHub plugin install/update downloads, require confirmation for risky releases, and add `--acknowledge-clawhub-risk` for reviewed automation. Thanks @jesse-merhi.
24612462
- Plugin SDK/testing: move core-only channel contract fixtures under the channel contract test tree and retire the old `test/helpers/channels` bridge directory so plugin tests stay on focused SDK surfaces. Thanks @vincentkoc.
24622463
- Plugin SDK/testing: expose native agent-runtime contract fixtures through `plugin-sdk/agent-runtime-test-contracts`, move sandbox config fixtures into the focused generic fixture subpath, and block extension tests from importing repo-only `test/helpers` bridges. Thanks @vincentkoc.
24632464
- Plugin SDK/testing: expose generic module reload, bundled-path, Node builtin mock, channel pairing/envelope, HTTP server, temp-home, replay-policy, and live STT helpers through focused SDK test subpaths so extension tests no longer depend on repo-only helper bridges. Thanks @vincentkoc.

docs/cli/plugins.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ openclaw plugins install git:github.com/<owner>/<repo> # git repo
8383
openclaw plugins install git:github.com/<owner>/<repo>@<ref>
8484
openclaw plugins install <package> --force # overwrite existing install
8585
openclaw plugins install <package> --pin # pin version
86+
openclaw plugins install <package> --acknowledge-clawhub-risk
8687
openclaw plugins install <package> --dangerously-force-unsafe-install
8788
openclaw plugins install <path> # local path
8889
openclaw plugins install <plugin>@<marketplace> # marketplace
@@ -135,6 +136,12 @@ is available, then fall back to `latest`.
135136

136137
If a plugin you published on ClawHub is blocked by a registry scan, use the publisher steps in [ClawHub](/clawhub/security).
137138

139+
</Accordion>
140+
<Accordion title="--acknowledge-clawhub-risk">
141+
ClawHub installs check the selected release trust record before downloading the package. If ClawHub reports a risky scan status, risky moderation state, download block, or registry reason, OpenClaw shows the trust details and asks for confirmation before continuing.
142+
143+
Use `--acknowledge-clawhub-risk` only after reviewing the ClawHub warning and deciding to continue without an interactive prompt. Pending or stale clean trust records warn but do not require acknowledgement.
144+
138145
</Accordion>
139146
<Accordion title="Hook packs and npm specs">
140147
`plugins install` is also the install surface for hook packs that expose `openclaw.hooks` in `package.json`. Use `openclaw hooks` for filtered hook visibility and per-hook enablement, not package installation.
@@ -324,6 +331,7 @@ openclaw plugins update <id-or-npm-spec>
324331
openclaw plugins update --all
325332
openclaw plugins update <id-or-npm-spec> --dry-run
326333
openclaw plugins update @openclaw/voice-call
334+
openclaw plugins update openclaw-codex-app-server --acknowledge-clawhub-risk
327335
openclaw plugins update openclaw-codex-app-server --dangerously-force-unsafe-install
328336
```
329337

@@ -351,6 +359,9 @@ Updates apply to tracked plugin installs in the managed plugin index and tracked
351359
<Accordion title="--dangerously-force-unsafe-install on update">
352360
`--dangerously-force-unsafe-install` is also available on `plugins update` as a break-glass override for built-in dangerous-code scan false positives during plugin updates. It still does not bypass plugin `before_install` policy blocks or scan-failure blocking, and it only applies to plugin updates, not hook-pack updates.
353361
</Accordion>
362+
<Accordion title="--acknowledge-clawhub-risk on update">
363+
ClawHub-backed plugin updates run the same exact-release trust check as installs before downloading the replacement package. Use `--acknowledge-clawhub-risk` for reviewed automation that should continue when the selected ClawHub release has a risky trust warning.
364+
</Accordion>
354365
</AccordionGroup>
355366

356367
### Inspect

docs/cli/update.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ openclaw update --tag main
2727
openclaw update --dry-run
2828
openclaw update --no-restart
2929
openclaw update --yes
30+
openclaw update --acknowledge-clawhub-risk
3031
openclaw update --json
3132
openclaw --update
3233
```
@@ -44,6 +45,10 @@ openclaw --update
4445
when npm plugin artifact drift is detected during post-update plugin sync.
4546
- `--timeout <seconds>`: per-step timeout (default is 1800s).
4647
- `--yes`: skip confirmation prompts (for example downgrade confirmation).
48+
- `--acknowledge-clawhub-risk`: continue post-update ClawHub plugin sync when
49+
the selected plugin release has a ClawHub trust warning. Without this flag,
50+
interactive runs ask before downloading risky ClawHub plugin releases and
51+
non-interactive runs fail closed.
4752

4853
`openclaw update` does not have a `--verbose` flag. Use `--dry-run` to preview
4954
the planned channel/tag/install/restart actions, `--json` for machine-readable

docs/tools/plugin.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,8 +543,10 @@ openclaw plugins install -l <path> # link (no copy) for dev
543543
openclaw plugins install <plugin> --marketplace <source>
544544
openclaw plugins install <plugin> --marketplace https://github.com/<owner>/<repo>
545545
openclaw plugins install <spec> --pin # record exact resolved npm spec
546+
openclaw plugins install <spec> --acknowledge-clawhub-risk
546547
openclaw plugins install <spec> --dangerously-force-unsafe-install
547548
openclaw plugins update <id-or-npm-spec> # update one plugin
549+
openclaw plugins update <id-or-npm-spec> --acknowledge-clawhub-risk
548550
openclaw plugins update <id-or-npm-spec> --dangerously-force-unsafe-install
549551
openclaw plugins update --all # update all
550552
openclaw plugins uninstall <id> # remove config and plugin index records
@@ -603,6 +605,14 @@ beta release exists. Exact versions and explicit tags stay pinned.
603605
`--pin` is npm-only. It is not supported with `--marketplace`, because
604606
marketplace installs persist marketplace source metadata instead of an npm spec.
605607

608+
ClawHub-backed plugin installs and updates check the exact target release's
609+
ClawHub trust record before download. If ClawHub reports risk for that release,
610+
OpenClaw prints the package, version, scan status, moderation state, and reason
611+
codes, then requires confirmation before continuing. Non-interactive automation
612+
must pass `--acknowledge-clawhub-risk` after reviewing that warning. The flag
613+
acknowledges ClawHub registry risk only; it does not bypass plugin
614+
`before_install` hook policy blocks or the built-in dangerous-code scanner.
615+
606616
`--dangerously-force-unsafe-install` is a break-glass override for false
607617
positives from the built-in dangerous-code scanner. It allows plugin installs
608618
and plugin updates to continue past built-in `critical` findings, but it still
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import type { ClawHubRiskAcknowledgementRequest } from "../plugins/clawhub.js";
2+
import { promptYesNo } from "./prompt.js";
3+
4+
export type ClawHubRiskAcknowledgementCliOptions = {
5+
acknowledgeClawHubRisk?: boolean;
6+
};
7+
8+
function canPromptForClawHubRisk(): boolean {
9+
return process.stdin.isTTY && process.stdout.isTTY;
10+
}
11+
12+
export function resolveClawHubRiskAcknowledgementCliOptions(params: {
13+
acknowledgeClawHubRisk?: boolean;
14+
action: "installing" | "updating";
15+
}): ClawHubRiskAcknowledgementCliOptions & {
16+
onClawHubRisk?: (request: ClawHubRiskAcknowledgementRequest) => Promise<boolean>;
17+
} {
18+
return {
19+
acknowledgeClawHubRisk: params.acknowledgeClawHubRisk,
20+
onClawHubRisk:
21+
params.acknowledgeClawHubRisk || !canPromptForClawHubRisk()
22+
? undefined
23+
: async (request) =>
24+
await promptYesNo(
25+
`Continue ${params.action} ClawHub package "${request.packageName}@${request.version}" despite this warning?`,
26+
),
27+
};
28+
}

src/cli/plugins-cli.install.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,33 @@ describe("plugins cli install", () => {
620620
expect(installPluginFromNpmSpec).not.toHaveBeenCalled();
621621
});
622622

623+
it("passes ClawHub risk acknowledgement to explicit ClawHub installs", async () => {
624+
loadConfig.mockReturnValue(createEmptyPluginConfig());
625+
parseClawHubPluginSpec.mockReturnValue({ name: "demo" });
626+
installPluginFromClawHub.mockResolvedValue(
627+
createClawHubInstallResult({
628+
pluginId: "demo",
629+
packageName: "demo",
630+
version: "1.2.3",
631+
channel: "official",
632+
}),
633+
);
634+
enablePluginInConfig.mockReturnValue({ config: createEnabledPluginConfig("demo") });
635+
applyExclusiveSlotSelection.mockReturnValue({
636+
config: createEnabledPluginConfig("demo"),
637+
warnings: [],
638+
});
639+
640+
await runPluginsCommand(["plugins", "install", "clawhub:demo", "--acknowledge-clawhub-risk"]);
641+
642+
expect(installPluginFromClawHub).toHaveBeenCalledWith(
643+
expect.objectContaining({
644+
spec: "clawhub:demo",
645+
acknowledgeClawHubRisk: true,
646+
}),
647+
);
648+
});
649+
623650
it("passes the active profile extensions dir to ClawHub installs", async () => {
624651
const extensionsDir = useProfileExtensionsDir();
625652
const cfg = createEmptyPluginConfig();

src/cli/plugins-cli.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,19 @@ import { applyParentDefaultHelpAction } from "./program/parent-default-help.js";
1818

1919
export type PluginUpdateOptions = {
2020
all?: boolean;
21+
acknowledgeClawhubRisk?: boolean;
2122
dryRun?: boolean;
2223
dangerouslyForceUnsafeInstall?: boolean;
2324
};
2425

26+
type CommanderClawHubRiskOptions = Record<string, unknown> & {
27+
acknowledgeClawhubRisk?: boolean;
28+
};
29+
30+
function normalizeCommanderClawHubRiskOption(opts: CommanderClawHubRiskOptions): boolean {
31+
return opts.acknowledgeClawhubRisk === true || opts.acknowledgeClawHubRisk === true;
32+
}
33+
2534
export type PluginMarketplaceListOptions = {
2635
json?: boolean;
2736
};
@@ -253,14 +262,19 @@ export function registerPluginsCli(program: Command) {
253262
"Bypass built-in dangerous-code install blocking (plugin hooks may still block)",
254263
false,
255264
)
265+
.option(
266+
"--acknowledge-clawhub-risk",
267+
"Acknowledge ClawHub release trust warnings without prompting",
268+
false,
269+
)
256270
.option(
257271
"--marketplace <source>",
258272
"Install a Claude marketplace plugin from a local repo/path or git/GitHub source",
259273
)
260274
.action(
261275
async (
262276
raw: string,
263-
opts: {
277+
opts: CommanderClawHubRiskOptions & {
264278
dangerouslyForceUnsafeInstall?: boolean;
265279
force?: boolean;
266280
link?: boolean;
@@ -272,7 +286,13 @@ export function registerPluginsCli(program: Command) {
272286
"install command",
273287
async () => {
274288
const { runPluginInstallCommand } = await import("./plugins-install-command.js");
275-
await runPluginInstallCommand({ raw, opts });
289+
await runPluginInstallCommand({
290+
raw,
291+
opts: {
292+
...opts,
293+
acknowledgeClawHubRisk: normalizeCommanderClawHubRiskOption(opts),
294+
},
295+
});
276296
},
277297
{ command: "install" },
278298
);
@@ -290,9 +310,20 @@ export function registerPluginsCli(program: Command) {
290310
"Bypass built-in dangerous-code update blocking for plugins (plugin hooks may still block)",
291311
false,
292312
)
313+
.option(
314+
"--acknowledge-clawhub-risk",
315+
"Acknowledge ClawHub release trust warnings without prompting",
316+
false,
317+
)
293318
.action(async (id: string | undefined, opts: PluginUpdateOptions) => {
294319
const { runPluginUpdateCommand } = await import("./plugins-update-command.js");
295-
await runPluginUpdateCommand({ id, opts });
320+
await runPluginUpdateCommand({
321+
id,
322+
opts: {
323+
...opts,
324+
acknowledgeClawHubRisk: normalizeCommanderClawHubRiskOption(opts),
325+
},
326+
});
296327
});
297328

298329
plugins

src/cli/plugins-cli.update.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,35 @@ describe("plugins cli update", () => {
210210
expect(updateParams.dangerouslyForceUnsafeInstall).toBe(true);
211211
});
212212

213+
it("passes ClawHub risk acknowledgement to plugin updates", async () => {
214+
const config = createTrackedPluginConfig({
215+
pluginId: "openclaw-codex-app-server",
216+
spec: "openclaw-codex-app-server@beta",
217+
});
218+
loadConfig.mockReturnValue(config);
219+
setInstalledPluginIndexInstallRecords(config.plugins?.installs ?? {});
220+
updateNpmInstalledPlugins.mockResolvedValue({
221+
config,
222+
changed: false,
223+
outcomes: [],
224+
});
225+
226+
await runPluginsCommand([
227+
"plugins",
228+
"update",
229+
"openclaw-codex-app-server",
230+
"--acknowledge-clawhub-risk",
231+
]);
232+
233+
expect(updateNpmInstalledPlugins).toHaveBeenCalledWith(
234+
expect.objectContaining({
235+
config,
236+
pluginIds: ["openclaw-codex-app-server"],
237+
acknowledgeClawHubRisk: true,
238+
}),
239+
);
240+
});
241+
213242
it("writes updated config when updater reports changes", async () => {
214243
const cfg = {
215244
plugins: {

src/cli/plugins-install-command.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { validateJsonSchemaValue } from "../plugins/schema-validator.js";
3333
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
3434
import { theme } from "../terminal/theme.js";
3535
import { shortenHomePath } from "../utils.js";
36+
import { resolveClawHubRiskAcknowledgementCliOptions } from "./clawhub-risk-acknowledgement.js";
3637
import { formatCliCommand } from "./command-format.js";
3738
import { looksLikeLocalInstallSpec } from "./install-spec.js";
3839
import { resolvePinnedNpmInstallRecordForCli } from "./npm-resolution.js";
@@ -558,6 +559,7 @@ export async function loadConfigForInstall(
558559
export async function runPluginInstallCommand(params: {
559560
raw: string;
560561
opts: InstallSafetyOverrides & {
562+
acknowledgeClawHubRisk?: boolean;
561563
force?: boolean;
562564
link?: boolean;
563565
pin?: boolean;
@@ -941,6 +943,10 @@ export async function runPluginInstallCommand(params: {
941943
if (clawhubSpec) {
942944
const result = await installPluginFromClawHub({
943945
...safetyOverrides,
946+
...resolveClawHubRiskAcknowledgementCliOptions({
947+
acknowledgeClawHubRisk: opts.acknowledgeClawHubRisk,
948+
action: "installing",
949+
}),
944950
mode: installMode,
945951
spec: raw,
946952
extensionsDir,

src/cli/plugins-update-command.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { updateNpmInstalledPlugins } from "../plugins/update.js";
1414
import { defaultRuntime } from "../runtime.js";
1515
import { theme } from "../terminal/theme.js";
16+
import { resolveClawHubRiskAcknowledgementCliOptions } from "./clawhub-risk-acknowledgement.js";
1617
import { commitPluginInstallRecordsWithConfig } from "./plugins-install-record-commit.js";
1718
import { refreshPluginRegistryAfterConfigMutation } from "./plugins-registry-refresh.js";
1819
import { logPluginUpdateOutcomes } from "./plugins-update-outcomes.js";
@@ -24,7 +25,12 @@ import { promptYesNo } from "./prompt.js";
2425

2526
export async function runPluginUpdateCommand(params: {
2627
id?: string;
27-
opts: { all?: boolean; dryRun?: boolean; dangerouslyForceUnsafeInstall?: boolean };
28+
opts: {
29+
all?: boolean;
30+
acknowledgeClawHubRisk?: boolean;
31+
dryRun?: boolean;
32+
dangerouslyForceUnsafeInstall?: boolean;
33+
};
2834
}) {
2935
assertConfigWriteAllowedInCurrentMode();
3036

@@ -62,6 +68,10 @@ export async function runPluginUpdateCommand(params: {
6268
specOverrides: pluginSelection.specOverrides,
6369
dryRun: params.opts.dryRun,
6470
dangerouslyForceUnsafeInstall: params.opts.dangerouslyForceUnsafeInstall,
71+
...resolveClawHubRiskAcknowledgementCliOptions({
72+
acknowledgeClawHubRisk: params.opts.acknowledgeClawHubRisk,
73+
action: "updating",
74+
}),
6575
logger,
6676
onIntegrityDrift: async (drift) => {
6777
const specLabel = drift.resolvedSpec ?? drift.spec;

0 commit comments

Comments
 (0)