Skip to content

Commit 803b7ab

Browse files
committed
fix: warn on invalid hook transform directories
1 parent a362831 commit 803b7ab

4 files changed

Lines changed: 55 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
3333
- fix(infra): block workspace state-directory env override [AI]. (#75940) Thanks @pgondhi987.
3434
- 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.
3535
- 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.
36+
- 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.
3637
- 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.
3738
- Discord: allow explicitly configured ack reactions in tool-only guild channels while keeping automatic lifecycle/status reactions suppressed. Fixes #74922. Thanks @samvilian and @BlueBirdBack.
3839
- 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.

docs/gateway/configuration-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,7 @@ Validation and safety notes:
590590
- Templates like `{{messages[0].subject}}` read from the payload.
591591
- `transform` can point to a JS/TS module returning a hook action.
592592
- `transform.module` must be a relative path and stays within `hooks.transformsDir` (absolute paths and traversal are rejected).
593+
- 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`.
593594
- `agentId` routes to a specific agent; unknown IDs fall back to default.
594595
- `allowedAgentIds`: restricts explicit routing (`*` or omitted = allow all, `[]` = deny all).
595596
- `defaultSessionKey`: optional fixed session key for hook agent runs without explicit `sessionKey`.

src/commands/doctor-config-flow.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,6 +1385,30 @@ describe("doctor config flow", () => {
13851385
expect(doctorWarnings.some((line) => line.includes("mutable allowlist"))).toBe(false);
13861386
});
13871387

1388+
it("warns when hooks transformsDir points outside the hook transforms root", async () => {
1389+
const doctorWarnings = await collectDoctorWarnings({
1390+
hooks: {
1391+
enabled: true,
1392+
token: "hook-secret",
1393+
transformsDir: "/virtual/.openclaw/workspace/skills/linear-webhook",
1394+
mappings: [
1395+
{
1396+
match: { path: "linear" },
1397+
action: "agent",
1398+
messageTemplate: "Linear event",
1399+
transform: { module: "./openclaw-linear-transform.js" },
1400+
},
1401+
],
1402+
},
1403+
});
1404+
1405+
const warning = doctorWarnings.join("\n");
1406+
expect(warning).toContain("hooks.transformsDir:");
1407+
expect(warning).toContain("/virtual/.openclaw/workspace/skills/linear-webhook");
1408+
expect(warning).toContain("/virtual/.openclaw/hooks/transforms");
1409+
expect(warning).toContain("move custom transforms there or remove hooks.transformsDir");
1410+
});
1411+
13881412
it("does not warn about sender-based group allowlist for googlechat", async () => {
13891413
const doctorWarnings = await collectDoctorWarnings({
13901414
channels: {

src/commands/doctor-config-flow.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import path from "node:path";
12
import { formatCliCommand } from "../cli/command-format.js";
23
import { findLegacyConfigIssues } from "../config/legacy.js";
34
import { CONFIG_PATH } from "../config/paths.js";
@@ -26,6 +27,30 @@ function hasLegacyInternalHookHandlers(raw: unknown): boolean {
2627
return Array.isArray(handlers) && handlers.length > 0;
2728
}
2829

30+
function collectInvalidHookTransformsDirWarnings(
31+
cfg: OpenClawConfig,
32+
configPath: string,
33+
): string[] {
34+
const transformsDir = cfg.hooks?.transformsDir?.trim();
35+
if (!transformsDir) {
36+
return [];
37+
}
38+
const configDir = path.dirname(configPath);
39+
const transformsRoot = path.join(configDir, "hooks", "transforms");
40+
const resolved = path.isAbsolute(transformsDir)
41+
? path.resolve(transformsDir)
42+
: path.resolve(transformsRoot, transformsDir);
43+
const relative = path.relative(transformsRoot, resolved);
44+
const escapesRoot =
45+
relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
46+
if (!escapesRoot) {
47+
return [];
48+
}
49+
return [
50+
`- hooks.transformsDir: ${transformsDir} is outside ${transformsRoot}. Hook transform modules must live under ${transformsRoot}; move custom transforms there or remove hooks.transformsDir.`,
51+
];
52+
}
53+
2954
function collectConfiguredChannelIds(cfg: OpenClawConfig): string[] {
3055
const channels =
3156
cfg.channels && typeof cfg.channels === "object" && !Array.isArray(cfg.channels)
@@ -111,6 +136,10 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
111136
"Legacy config keys detected",
112137
);
113138
}
139+
const hookTransformsDirWarnings = collectInvalidHookTransformsDirWarnings(cfg, snapshot.path);
140+
if (hookTransformsDirWarnings.length > 0) {
141+
note(sanitizeDoctorNote(hookTransformsDirWarnings.join("\n")), "Doctor warnings");
142+
}
114143

115144
const normalized = normalizeCompatibilityConfigValues(candidate);
116145
if (normalized.changes.length > 0) {

0 commit comments

Comments
 (0)