OpenCode plugin writes .planning/.active_plan by truncation instead of replacing the directory entry
Problem
The OpenCode plugin writes the active plan pointer with writeText(path.join(planningRoot, ".active_plan"), ${planId}\n) in .opencode/packages/opencode-planning-with-files/src/core.ts, and writeText is fs.writeFileSync(target, text, { encoding: "utf8" }). writeFileSync opens the existing file and truncates it in place. If .active_plan is a hard link or a symlink, the write lands in whichever file shares that inode or is the link target, instead of replacing the pointer.
The shell initializer removed this bug class in v3.19.0 (PR #249): set-active-plan.sh writes to an exclusive temporary file and renames it over the pointer, and refuses a pointer that is a link or leaves the project. The OpenCode route still uses the older write.
Scope: OpenCode users who create named plans through the plugin's /pwf command, and only when .planning/.active_plan has been replaced by a link. A plain pointer file created by the plugin itself is written correctly. Root mode does not use the pointer.
How to reproduce
mkdir -p .planning && printf 'other-plan\n' > .planning/.active_plan
ln -f .planning/.active_plan elsewhere.txt # hard link
# create a named plan through the OpenCode plugin
cat elsewhere.txt # now contains the new plan idThe same holds for a symlink pointing outside the project: the target file is overwritten.
Suggested fix
Mirror the shell selector in TypeScript:
- Refuse to write when
fs.lstatSync(pointer)is not a regular file, is a symbolic link, reportsnlink > 1, or whenfs.realpathSyncresolves outsideplanningRoot(the plugin already hasrealpathOrNullfor the read side). - Write to a temporary file opened with the
wxflag beside the pointer andfs.renameSyncit over.active_plan, so the directory entry is replaced instead of the inode being truncated. On Windows,renameSyncover an existing file works for regular files. - Add a vitest case with a hard-linked pointer that asserts the linked file is untouched.
Origin
Found by the v3.19.0 release review (PRs #247, #248, #249). Tracked as item 7 of #250 before that issue was split into one issue per item. The Hermes plugin has the same pattern, tracked in #259.
Source: OthmanAdi/planning-with-files