fix: warn on invalid hook transform directories

This commit is contained in:
Peter Steinberger
2026-05-02 09:48:22 +01:00
parent a3628310e4
commit 803b7ab808
4 changed files with 55 additions and 0 deletions

View File

@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
- fix(infra): block workspace state-directory env override [AI]. (#75940) Thanks @pgondhi987.
- MCP/OpenAI: normalize parameter-free tool schemas whose top-level object `properties` is missing, null, or invalid before sending tools to OpenAI, so MCP tools without params stay usable. Fixes #75362. Thanks @tolkonepiu and @SymbolStar.
- TTS: honor explicit short `[[tts:text]]...[[/tts:text]]` blocks while keeping untagged short auto-TTS suppressed, so tagged voice replies are synthesized instead of being dropped as empty voice-only payloads. Fixes #73758. Thanks @yfge.
- Hooks/doctor: warn when `hooks.transformsDir` points outside the canonical hooks transform directory, so invalid workspace skill paths get a direct recovery hint before the Gateway crash-loops. Fixes #75853. Thanks @midobk.
- Proxy/audio: convert standard `FormData` bodies before proxy-backed undici fetches, so audio transcription and multipart uploads no longer send `[object FormData]` when `HTTP_PROXY` or `HTTPS_PROXY` is configured. Fixes #48554. Thanks @dco5.
- Discord: allow explicitly configured ack reactions in tool-only guild channels while keeping automatic lifecycle/status reactions suppressed. Fixes #74922. Thanks @samvilian and @BlueBirdBack.
- Gateway/diagnostics: include a bounded redacted startup error message in stability bundles, so crash-loop reports identify the failing plugin or contract without exposing secrets. Refs #75797. Thanks @ymebosma.

View File

@@ -590,6 +590,7 @@ Validation and safety notes:
- Templates like `{{messages[0].subject}}` read from the payload.
- `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`.
- `agentId` routes to a specific agent; unknown IDs fall back to default.
- `allowedAgentIds`: restricts explicit routing (`*` or omitted = allow all, `[]` = deny all).
- `defaultSessionKey`: optional fixed session key for hook agent runs without explicit `sessionKey`.

View File

@@ -1385,6 +1385,30 @@ describe("doctor config flow", () => {
expect(doctorWarnings.some((line) => line.includes("mutable allowlist"))).toBe(false);
});
it("warns when hooks transformsDir points outside the hook transforms root", async () => {
const doctorWarnings = await collectDoctorWarnings({
hooks: {
enabled: true,
token: "hook-secret",
transformsDir: "/virtual/.openclaw/workspace/skills/linear-webhook",
mappings: [
{
match: { path: "linear" },
action: "agent",
messageTemplate: "Linear event",
transform: { module: "./openclaw-linear-transform.js" },
},
],
},
});
const warning = doctorWarnings.join("\n");
expect(warning).toContain("hooks.transformsDir:");
expect(warning).toContain("/virtual/.openclaw/workspace/skills/linear-webhook");
expect(warning).toContain("/virtual/.openclaw/hooks/transforms");
expect(warning).toContain("move custom transforms there or remove hooks.transformsDir");
});
it("does not warn about sender-based group allowlist for googlechat", async () => {
const doctorWarnings = await collectDoctorWarnings({
channels: {

View File

@@ -1,3 +1,4 @@
import path from "node:path";
import { formatCliCommand } from "../cli/command-format.js";
import { findLegacyConfigIssues } from "../config/legacy.js";
import { CONFIG_PATH } from "../config/paths.js";
@@ -26,6 +27,30 @@ function hasLegacyInternalHookHandlers(raw: unknown): boolean {
return Array.isArray(handlers) && handlers.length > 0;
}
function collectInvalidHookTransformsDirWarnings(
cfg: OpenClawConfig,
configPath: string,
): string[] {
const transformsDir = cfg.hooks?.transformsDir?.trim();
if (!transformsDir) {
return [];
}
const configDir = path.dirname(configPath);
const transformsRoot = path.join(configDir, "hooks", "transforms");
const resolved = path.isAbsolute(transformsDir)
? path.resolve(transformsDir)
: path.resolve(transformsRoot, transformsDir);
const relative = path.relative(transformsRoot, resolved);
const escapesRoot =
relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
if (!escapesRoot) {
return [];
}
return [
`- hooks.transformsDir: ${transformsDir} is outside ${transformsRoot}. Hook transform modules must live under ${transformsRoot}; move custom transforms there or remove hooks.transformsDir.`,
];
}
function collectConfiguredChannelIds(cfg: OpenClawConfig): string[] {
const channels =
cfg.channels && typeof cfg.channels === "object" && !Array.isArray(cfg.channels)
@@ -111,6 +136,10 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
"Legacy config keys detected",
);
}
const hookTransformsDirWarnings = collectInvalidHookTransformsDirWarnings(cfg, snapshot.path);
if (hookTransformsDirWarnings.length > 0) {
note(sanitizeDoctorNote(hookTransformsDirWarnings.join("\n")), "Doctor warnings");
}
const normalized = normalizeCompatibilityConfigValues(candidate);
if (normalized.changes.length > 0) {