Hermes plugin writes .planning/.active_plan by truncation instead of replacing the directory entry

Author: OthmanAdiCreated Sep 17, 2026Updated Sep 17, 2026
Labelsbughelp wanted

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

bash
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 id

The 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 outside planning_root.
  • Write the new content to a temporary file created with O_CREAT | O_EXCL beside the pointer and os.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