Every lifecycle script answers "is this session mine?" by deriving a name rather than checking an identity.
getSessionName(config) returns hermit-{basename(cwd)} (plugins/claude-code-hermit/scripts/lib/tmux.ts:94-97). Two projects whose directory basenames collide produce the same session name.
Impact
hermit-stop derives the session name from config alone (scripts/hermit-stop.ts:127) and --force kills whatever answers, with no ownership check:
const tree = collectTree(paneRootPids(sessionName));
tmux(["kill-session", "-t", sessionName]);
(scripts/hermit-stop.ts:182-184)
So hermit-stop --force run in one project can terminate a different project's live hermit — its claude process and its heartbeat/routine monitor subprocesses included. hermit-watchdog has the same exposure on its restart/kill paths.
A related case: when a project directory is deleted and recreated while its session is still running, the surviving session is bound to the old inode. It answers to the same name but is factually a different project generation, and nothing can tell the two apart.
Proposed fix
Stamp a session identity descriptor at creation and gate destructive operations on it, instead of inferring ownership:
- canonical project root
- the project directory's
dev and ino (distinguishes "same path, recreated directory" from "same directory" — a path string cannot)
- runtime mode (tmux / docker)
- a marker plus schema version
Carry it in the tmux session environment via tmux new-session -e, read back with tmux show-environment -t <session>. Verified the round trip works: a JSON descriptor survives new-session -e → show-environment intact on tmux 3.6b.
Classify into owned-exactly / same-path-replaced-inode / foreign-path / legacy-unmarked / unreadable / missing, and refuse destructive operations on anything not exactly owned.
Blockers to settle first
- No declared tmux version floor.
plugins/claude-code-hermit/docs/always-on-ops.md:9 lists tmux with no minimum. The version that introduced new-session -e needs checking against the tmux changelog before this can be relied on, plus a capability probe and a doctor check.
- Migration window. Sessions running today carry no descriptor, so they classify as
legacy-unmarked and must degrade to current behavior until they restart. The guarantee only becomes real after a full fleet cycle.
Every lifecycle script answers "is this session mine?" by deriving a name rather than checking an identity.
getSessionName(config)returnshermit-{basename(cwd)}(plugins/claude-code-hermit/scripts/lib/tmux.ts:94-97). Two projects whose directory basenames collide produce the same session name.Impact
hermit-stopderives the session name from config alone (scripts/hermit-stop.ts:127) and--forcekills whatever answers, with no ownership check:(
scripts/hermit-stop.ts:182-184)So
hermit-stop --forcerun in one project can terminate a different project's live hermit — its claude process and its heartbeat/routine monitor subprocesses included.hermit-watchdoghas the same exposure on its restart/kill paths.A related case: when a project directory is deleted and recreated while its session is still running, the surviving session is bound to the old inode. It answers to the same name but is factually a different project generation, and nothing can tell the two apart.
Proposed fix
Stamp a session identity descriptor at creation and gate destructive operations on it, instead of inferring ownership:
devandino(distinguishes "same path, recreated directory" from "same directory" — a path string cannot)Carry it in the tmux session environment via
tmux new-session -e, read back withtmux show-environment -t <session>. Verified the round trip works: a JSON descriptor survivesnew-session -e→show-environmentintact on tmux 3.6b.Classify into
owned-exactly/same-path-replaced-inode/foreign-path/legacy-unmarked/unreadable/missing, and refuse destructive operations on anything not exactly owned.Blockers to settle first
plugins/claude-code-hermit/docs/always-on-ops.md:9lists tmux with no minimum. The version that introducednew-session -eneeds checking against the tmux changelog before this can be relied on, plus a capability probe and a doctor check.legacy-unmarkedand must degrade to current behavior until they restart. The guarantee only becomes real after a full fleet cycle.