Summary
Under a storm of concurrent wt config state marker calls, worktrunk renamed the
shared parent directory of ~180 sibling repos. The parent got renamed to a truncated
form of a child repo's name (a 9-char repo name; the parent ended up named after its
first 7 chars). Every repo under the parent became inaccessible and running agents
broke with NotFound. It was a single rename(2) — inode identity and mtime
preserved, only ctime changed — so nothing was deleted, but it is a data-loss-class
failure.
I never invoked wt switch/merge/remove. The only wt calls in play were
wt config state marker set, emitted automatically by the OpenCode activity-marker
plugin.
Environment
- worktrunk 0.68.0
- Harness: OpenCode with the bundled worktrunk activity-marker plugin
- Layout: ~180 git repos as siblings under a single parent directory (
$PARENT);
one of them has 3 worktrees, also siblings under $PARENT.
Trigger
The plugin shells out to wt on every session event:
// session.status -> wt config state marker set <marker>
// session.idle -> wt config state marker set <marker>
// session.deleted -> wt config state marker clear
With N parallel agents, every status/idle transition spawns a wt process.
Process-launch logs show ~465 wt spawns in ~29s, all contending on worktrunk state
and CWD/worktree resolution.
Timeline (relative)
- T-13s: agent healthy, cwd =
$PARENT/<repo>
- T+0s: burst of
wt spawns begins (~465 over ~29s)
- T+5s:
$PARENT inode ctime changes -> rename($PARENT -> sibling <repo-truncated>);
mtime unchanged, so a pure rename, not copy/delete
- T+29s: harness reports
NotFound: FileSystem.access ($PARENT/<repo>); git worktree ops fail
Suspected root cause
The CWD-recovery path (hardened in #1193, "prevent false matches when multiple repos
share a parent directory") still appears to mis-resolve under concurrency. When a wt
observes a transiently-missing worktree while hundreds of siblings share $PARENT,
recovery/relocate picks the wrong target and renames the parent. Looks TOCTOU,
consistent with the other rename/TOCTOU fixes in the changelog (#2870, #2849, #2808).
Impact
A shipped, automatic agent plugin (wt config state marker) — a command a user would
reasonably assume is read-mostly/telemetry — can rename the parent of every repo in a
workspace, with no explicit mutating subcommand from the user.
Asks
wt config state marker (and any read-mostly/telemetry command) must never
relocate/rename a directory. Gate relocation behind explicit mutating commands.
- Serialize/lock state mutations per repo so the marker storm cannot race CWD-recovery.
- Hard-refuse any recovery/relocate whose target is an ancestor of the repo or contains
other repos.
I can attach the relevant process-launch and OpenCode logs on request.
Summary
Under a storm of concurrent
wt config state markercalls, worktrunk renamed theshared parent directory of ~180 sibling repos. The parent got renamed to a truncated
form of a child repo's name (a 9-char repo name; the parent ended up named after its
first 7 chars). Every repo under the parent became inaccessible and running agents
broke with
NotFound. It was a singlerename(2)— inode identity and mtimepreserved, only ctime changed — so nothing was deleted, but it is a data-loss-class
failure.
I never invoked
wt switch/merge/remove. The onlywtcalls in play werewt config state marker set, emitted automatically by the OpenCode activity-markerplugin.
Environment
$PARENT);one of them has 3 worktrees, also siblings under
$PARENT.Trigger
The plugin shells out to
wton every session event:With N parallel agents, every status/idle transition spawns a
wtprocess.Process-launch logs show ~465
wtspawns in ~29s, all contending on worktrunk stateand CWD/worktree resolution.
Timeline (relative)
$PARENT/<repo>wtspawns begins (~465 over ~29s)$PARENTinode ctime changes ->rename($PARENT -> sibling <repo-truncated>);mtime unchanged, so a pure rename, not copy/delete
NotFound: FileSystem.access ($PARENT/<repo>); git worktree ops failSuspected root cause
The CWD-recovery path (hardened in #1193, "prevent false matches when multiple repos
share a parent directory") still appears to mis-resolve under concurrency. When a
wtobserves a transiently-missing worktree while hundreds of siblings share
$PARENT,recovery/relocate picks the wrong target and renames the parent. Looks TOCTOU,
consistent with the other rename/TOCTOU fixes in the changelog (#2870, #2849, #2808).
Impact
A shipped, automatic agent plugin (
wt config state marker) — a command a user wouldreasonably assume is read-mostly/telemetry — can rename the parent of every repo in a
workspace, with no explicit mutating subcommand from the user.
Asks
wt config state marker(and any read-mostly/telemetry command) must neverrelocate/rename a directory. Gate relocation behind explicit mutating commands.
other repos.
I can attach the relevant process-launch and OpenCode logs on request.