Hermes plugin writes .planning/.active_plan by truncation instead of replacing the directory entry
Problem
The Hermes plugin writes the active plan pointer with (planning_root / ".active_plan").write_text(plan_id + "\n", encoding="utf-8") in .hermes/plugins/planning-with-files/planning_files.py. write_text 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 Hermes route still uses the older write.
Scope: Hermes users who create named plans through the plugin's /pwf flow, 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 Hermes 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 Python:
- Refuse to write when the existing pointer is not a regular file, is a symlink (
Path.is_symlink()), has more than one link (os.stat(...).st_nlink > 1), or resolves outsideplanning_root. - Write the new content to a temporary file created with
O_CREAT | O_EXCLbeside the pointer andos.replace()it over.active_plan, so the directory entry is replaced instead of the inode being truncated. - Add a test in the plugin's test suite 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 OpenCode plugin has the same pattern, tracked in #260.
Source: OthmanAdi/planning-with-files