From a6e3c6a5eda2c0e65f988de8cfc27c03dede402a Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 18 Apr 2026 14:10:12 +0900 Subject: [PATCH 1/2] test(write-existing-file-guard): cover lazy canonical path init Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../lazy-canonical-path-init.test.ts | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/hooks/write-existing-file-guard/lazy-canonical-path-init.test.ts diff --git a/src/hooks/write-existing-file-guard/lazy-canonical-path-init.test.ts b/src/hooks/write-existing-file-guard/lazy-canonical-path-init.test.ts new file mode 100644 index 000000000..1d3ada213 --- /dev/null +++ b/src/hooks/write-existing-file-guard/lazy-canonical-path-init.test.ts @@ -0,0 +1,65 @@ +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test" +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" + +const realFs = await import("node:fs") + +const existsSyncMock = mock(realFs.existsSync) +const realpathNativeMock = mock(realFs.realpathSync.native) + +mock.module("fs", () => ({ + ...realFs, + existsSync: existsSyncMock, + realpathSync: { + ...realFs.realpathSync, + native: realpathNativeMock, + }, +})) + +const { createWriteExistingFileGuardHook } = await import("./index") + +describe("createWriteExistingFileGuardHook", () => { + let tempDir = "" + + beforeEach(() => { + // given + tempDir = mkdtempSync(join(tmpdir(), "write-existing-file-guard-lazy-")) + mkdirSync(tempDir, { recursive: true }) + existsSyncMock.mockClear() + realpathNativeMock.mockClear() + }) + + afterEach(() => { + rmSync(tempDir, { recursive: true, force: true }) + }) + + test("#given hook factory #when created #then defers fs canonical path calls until first tool invocation", async () => { + // given + const existingFile = join(tempDir, "existing.txt") + writeFileSync(existingFile, "content") + + // when + const hook = createWriteExistingFileGuardHook({ directory: tempDir } as never) + + // then + expect(existsSyncMock).toHaveBeenCalledTimes(0) + expect(realpathNativeMock).toHaveBeenCalledTimes(0) + + // when + await expect( + hook["tool.execute.before"]?.( + { + tool: "write", + sessionID: "ses_lazy", + callID: "call_lazy", + } as never, + { args: { filePath: existingFile, content: "updated" } } as never, + ), + ).rejects.toThrow("File already exists. Use edit tool instead.") + + // then + expect(existsSyncMock).toHaveBeenCalledTimes(2) + expect(realpathNativeMock).toHaveBeenCalledTimes(1) + }) +}) From a4c45e2770781978d594586421b94efdd107ccfb Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 18 Apr 2026 14:11:03 +0900 Subject: [PATCH 2/2] fix(write-existing-file-guard): defer realpath/existsSync to first tool call Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- src/hooks/write-existing-file-guard/hook.ts | 12 ++++++++++-- .../lazy-canonical-path-init.test.ts | 4 ++-- .../tool-execute-before-handler.ts | 5 +++-- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/hooks/write-existing-file-guard/hook.ts b/src/hooks/write-existing-file-guard/hook.ts index bdaf5cad8..ab7bd9aef 100644 --- a/src/hooks/write-existing-file-guard/hook.ts +++ b/src/hooks/write-existing-file-guard/hook.ts @@ -76,7 +76,15 @@ export function isOverwriteEnabled(value: boolean | string | undefined): boolean export function createWriteExistingFileGuardHook(ctx: PluginInput): Hooks { const readPermissionsBySession = new Map>() const sessionLastAccess = new Map() - const canonicalSessionRoot = toCanonicalPath(resolveInputPath(ctx, ctx.directory)) + let canonicalSessionRoot: string | undefined + + function getCanonicalSessionRoot(): string { + if (!canonicalSessionRoot) { + canonicalSessionRoot = toCanonicalPath(resolveInputPath(ctx, ctx.directory)) + } + + return canonicalSessionRoot + } return { "tool.execute.before": async (input, output) => { @@ -86,7 +94,7 @@ export function createWriteExistingFileGuardHook(ctx: PluginInput): Hooks { output, readPermissionsBySession, sessionLastAccess, - canonicalSessionRoot, + getCanonicalSessionRoot, maxTrackedSessions: MAX_TRACKED_SESSIONS, }) }, diff --git a/src/hooks/write-existing-file-guard/lazy-canonical-path-init.test.ts b/src/hooks/write-existing-file-guard/lazy-canonical-path-init.test.ts index 1d3ada213..0f1d4bb88 100644 --- a/src/hooks/write-existing-file-guard/lazy-canonical-path-init.test.ts +++ b/src/hooks/write-existing-file-guard/lazy-canonical-path-init.test.ts @@ -59,7 +59,7 @@ describe("createWriteExistingFileGuardHook", () => { ).rejects.toThrow("File already exists. Use edit tool instead.") // then - expect(existsSyncMock).toHaveBeenCalledTimes(2) - expect(realpathNativeMock).toHaveBeenCalledTimes(1) + expect(existsSyncMock).toHaveBeenCalledTimes(3) + expect(realpathNativeMock).toHaveBeenCalledTimes(2) }) }) diff --git a/src/hooks/write-existing-file-guard/tool-execute-before-handler.ts b/src/hooks/write-existing-file-guard/tool-execute-before-handler.ts index 25eebbda3..848238a8a 100644 --- a/src/hooks/write-existing-file-guard/tool-execute-before-handler.ts +++ b/src/hooks/write-existing-file-guard/tool-execute-before-handler.ts @@ -90,10 +90,10 @@ export async function handleWriteExistingFileGuardToolExecuteBefore(params: { output: { args?: unknown } readPermissionsBySession: Map> sessionLastAccess: Map - canonicalSessionRoot: string + getCanonicalSessionRoot: () => string maxTrackedSessions: number }): Promise { - const { ctx, input, output, readPermissionsBySession, sessionLastAccess, canonicalSessionRoot, maxTrackedSessions } = params + const { ctx, input, output, readPermissionsBySession, sessionLastAccess, getCanonicalSessionRoot, maxTrackedSessions } = params const toolName = input.tool?.toLowerCase() if (toolName !== "write" && toolName !== "read") { return @@ -107,6 +107,7 @@ export async function handleWriteExistingFileGuardToolExecuteBefore(params: { } const resolvedPath = resolveInputPath(ctx, filePath) + const canonicalSessionRoot = getCanonicalSessionRoot() const canonicalPath = toCanonicalPath(resolvedPath) if (!isPathInsideDirectory(canonicalPath, canonicalSessionRoot)) { return