From 31de0335908b2828c36a9ee553fb52b617110ada Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Thu, 14 May 2026 14:22:34 +0800 Subject: [PATCH] fix(hooks): allow dot-prefixed handler paths --- CHANGELOG.md | 1 + src/hooks/loader.test.ts | 22 ++++++++++++++++++++++ src/hooks/loader.ts | 11 ++++++++++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8aca83cbe8..2758fab3aea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Docs: https://docs.openclaw.ai - Telegram: allow trusted local Bot API media files whose filenames start with dots instead of falling back to remote download. - Agents/Codex app-server: remap injected context files under dot-dot-prefixed workspace directories when a run switches to an effective sandbox workspace. - Control UI/i18n: use the installed workspace pi runtime for locale refreshes, update the fallback package pin, and skip scheduled refreshes with invalid provider credentials instead of failing main. +- Hooks: load workspace-relative legacy hook modules from dot-dot-prefixed directories without treating the filename prefix as parent traversal. - Plugins: preserve installed package metadata and persisted registry freshness checks for plugin package paths under dot-dot-prefixed directories. - Agents: allow dot-dot-prefixed filenames such as `..note.txt` through sandbox FS bridge, remote sandbox reads, and apply_patch summaries without mistaking the name for parent traversal. - CLI/migrate: hide per-item source/plugin hints on non-conflicting Codex skill and plugin selection prompts, keeping the hint text reserved for rows that actually need attention. Thanks @sjf. diff --git a/src/hooks/loader.test.ts b/src/hooks/loader.test.ts index 476cb78afbd..18b158b46c6 100644 --- a/src/hooks/loader.test.ts +++ b/src/hooks/loader.test.ts @@ -261,6 +261,28 @@ describe("loader", () => { expect(keys).toContain("command:stop"); }); + it("loads legacy handler modules from dot-prefixed workspace paths", async () => { + await fs.mkdir(path.join(tmpDir, "..hooks"), { recursive: true }); + await writeHandlerModule( + path.join("..hooks", "legacy-handler.js"), + 'export default async function(event) { event.messages.push("dot-prefixed-hook"); }\n', + ); + + const cfg = createEnabledHooksConfig([ + { + event: "command:new", + module: path.join("..hooks", "legacy-handler.js"), + }, + ]); + + const count = await loadInternalHooks(cfg, tmpDir); + expect(count).toBe(1); + + const event = createInternalHookEvent("command", "new", "test-session"); + await triggerInternalHook(event); + expect(event.messages).toEqual(["dot-prefixed-hook"]); + }); + it("preserves plugin-registered hooks when workspace hooks reload", async () => { const pluginHandler = vi.fn(); registerInternalHook("gateway:startup", pluginHandler); diff --git a/src/hooks/loader.ts b/src/hooks/loader.ts index 2b4af989a0f..0511fecff12 100644 --- a/src/hooks/loader.ts +++ b/src/hooks/loader.ts @@ -34,6 +34,15 @@ function safeLogValue(value: string): string { return sanitizeForLog(value); } +function isNonEmptyRelativePathInsideRoot(relativePath: string): boolean { + return ( + relativePath !== "" && + relativePath !== ".." && + !relativePath.startsWith(`..${path.sep}`) && + !path.isAbsolute(relativePath) + ); +} + function maybeWarnTrustedHookSource(source: string): void { if (source === "openclaw-workspace") { log.warn( @@ -211,7 +220,7 @@ export async function loadInternalHooks( continue; } const rel = path.relative(baseDirReal, modulePathSafe); - if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) { + if (!isNonEmptyRelativePathInsideRoot(rel)) { log.error(`Handler module path must stay within workspaceDir: ${safeLogValue(rawModule)}`); continue; }