Compare commits

...

2 Commits

Author SHA1 Message Date
github-actions[bot]
ffa2a255d9 release: v3.8.3 2026-02-22 06:46:51 +00:00
YeonGyu-Kim
07e8a7c570 feat(write-existing-file-guard): allow writes outside session directory
Remove blocking logic that prevented writes to files outside the
session directory. The guard now only applies to files within the
session directory, allowing free writes to external paths.

- Remove OUTSIDE_SESSION_MESSAGE constant
- Update test to expect outside writes to be allowed
- Add early return for paths outside session directory
- Keep isPathInsideDirectory for session boundary check

TDD cycle:
1. RED: Update test expectation
2. GREEN: Implement early return for outside paths
3. REFACTOR: Clean up unused constants
2026-02-22 15:43:19 +09:00
10 changed files with 24 additions and 33 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode",
"version": "3.8.2",
"version": "3.8.3",
"description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -74,13 +74,13 @@
"typescript": "^5.7.3"
},
"optionalDependencies": {
"oh-my-opencode-darwin-arm64": "3.8.2",
"oh-my-opencode-darwin-x64": "3.8.2",
"oh-my-opencode-linux-arm64": "3.8.2",
"oh-my-opencode-linux-arm64-musl": "3.8.2",
"oh-my-opencode-linux-x64": "3.8.2",
"oh-my-opencode-linux-x64-musl": "3.8.2",
"oh-my-opencode-windows-x64": "3.8.2"
"oh-my-opencode-darwin-arm64": "3.8.3",
"oh-my-opencode-darwin-x64": "3.8.3",
"oh-my-opencode-linux-arm64": "3.8.3",
"oh-my-opencode-linux-arm64-musl": "3.8.3",
"oh-my-opencode-linux-x64": "3.8.3",
"oh-my-opencode-linux-x64-musl": "3.8.3",
"oh-my-opencode-windows-x64": "3.8.3"
},
"trustedDependencies": [
"@ast-grep/cli",

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-darwin-arm64",
"version": "3.8.2",
"version": "3.8.3",
"description": "Platform-specific binary for oh-my-opencode (darwin-arm64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-darwin-x64",
"version": "3.8.2",
"version": "3.8.3",
"description": "Platform-specific binary for oh-my-opencode (darwin-x64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-arm64-musl",
"version": "3.8.2",
"version": "3.8.3",
"description": "Platform-specific binary for oh-my-opencode (linux-arm64-musl)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-arm64",
"version": "3.8.2",
"version": "3.8.3",
"description": "Platform-specific binary for oh-my-opencode (linux-arm64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-x64-musl",
"version": "3.8.2",
"version": "3.8.3",
"description": "Platform-specific binary for oh-my-opencode (linux-x64-musl)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-linux-x64",
"version": "3.8.2",
"version": "3.8.3",
"description": "Platform-specific binary for oh-my-opencode (linux-x64)",
"license": "MIT",
"repository": {

View File

@@ -1,6 +1,6 @@
{
"name": "oh-my-opencode-windows-x64",
"version": "3.8.2",
"version": "3.8.3",
"description": "Platform-specific binary for oh-my-opencode (windows-x64)",
"license": "MIT",
"repository": {

View File

@@ -1,7 +1,7 @@
import type { Hooks, PluginInput } from "@opencode-ai/plugin"
import { existsSync, realpathSync } from "fs"
import { basename, dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path"
import { basename, dirname, isAbsolute, join, normalize, relative, resolve } from "path"
import { log } from "../../shared"
@@ -14,7 +14,7 @@ type GuardArgs = {
const MAX_TRACKED_SESSIONS = 256
export const MAX_TRACKED_PATHS_PER_SESSION = 1024
const OUTSIDE_SESSION_MESSAGE = "Path must be inside session directory."
const BLOCK_MESSAGE = "File already exists. Use edit tool instead."
function asRecord(value: unknown): Record<string, unknown> | undefined {
if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -37,6 +37,8 @@ function isPathInsideDirectory(pathToCheck: string, directory: string): boolean
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath))
}
function toCanonicalPath(absolutePath: string): string {
let canonicalPath = absolutePath
@@ -73,7 +75,6 @@ export function createWriteExistingFileGuardHook(ctx: PluginInput): Hooks {
const readPermissionsBySession = new Map<string, Set<string>>()
const sessionLastAccess = new Map<string, number>()
const canonicalSessionRoot = toCanonicalPath(resolveInputPath(ctx, ctx.directory))
const sisyphusRoot = join(canonicalSessionRoot, ".sisyphus") + sep
const touchSession = (sessionID: string): void => {
sessionLastAccess.set(sessionID, Date.now())
@@ -174,16 +175,7 @@ export function createWriteExistingFileGuardHook(ctx: PluginInput): Hooks {
const isInsideSessionDirectory = isPathInsideDirectory(canonicalPath, canonicalSessionRoot)
if (!isInsideSessionDirectory) {
if (toolName === "read") {
return
}
log("[write-existing-file-guard] Blocking write outside session directory", {
sessionID: input.sessionID,
filePath,
resolvedPath,
})
throw new Error(OUTSIDE_SESSION_MESSAGE)
return
}
if (toolName === "read") {
@@ -206,7 +198,7 @@ export function createWriteExistingFileGuardHook(ctx: PluginInput): Hooks {
return
}
const isSisyphusPath = canonicalPath.startsWith(sisyphusRoot)
const isSisyphusPath = canonicalPath.includes("/.sisyphus/")
if (isSisyphusPath) {
log("[write-existing-file-guard] Allowing .sisyphus/** overwrite", {
sessionID: input.sessionID,

View File

@@ -7,7 +7,6 @@ import { MAX_TRACKED_PATHS_PER_SESSION } from "./hook"
import { createWriteExistingFileGuardHook } from "./index"
const BLOCK_MESSAGE = "File already exists. Use edit tool instead."
const OUTSIDE_SESSION_MESSAGE = "Path must be inside session directory."
type Hook = ReturnType<typeof createWriteExistingFileGuardHook>
@@ -339,7 +338,7 @@ describe("createWriteExistingFileGuardHook", () => {
).resolves.toBeDefined()
})
test("#given existing file outside session directory #when write executes #then blocks", async () => {
test("#given existing file outside session directory #when write executes #then allows", async () => {
const outsideDir = mkdtempSync(join(tmpdir(), "write-existing-file-guard-outside-"))
try {
@@ -349,9 +348,9 @@ describe("createWriteExistingFileGuardHook", () => {
await expect(
invoke({
tool: "write",
outputArgs: { filePath: outsideFile, content: "attempted overwrite" },
outputArgs: { filePath: outsideFile, content: "allowed overwrite" },
})
).rejects.toThrow(OUTSIDE_SESSION_MESSAGE)
).resolves.toBeDefined()
} finally {
rmSync(outsideDir, { recursive: true, force: true })
}