[BUG] Plugin account-upload is last-write-wins with no compare-and-swap, so concurrent sessions silently destroy each other's hook fixes
Cowork on claude.ai, account-level plugin uploads.
POST /api/organizations/{org}/marketplaces/{mkt}/plugins/account-upload?overwrite=true replaces the whole plugin. There is no If-Match, no version precondition, and no rejection when the caller's base is stale. Two sessions working in parallel therefore overwrite each other's work with a 200 and no warning anywhere.
Observed tonight on one account, three times in ninety minutes, on the same plugin:
- session A uploads 0.9.7 with two hook fixes; a peer session uploads 0.9.15 from an older base; A's fixes are gone.
- A re-applies onto 0.9.15 and uploads 0.9.16, verified by downloading the plugin back; a peer uploads again; the fixes are gone a second time and a new gate file has appeared that A never had.
- The only way A discovered either loss was downloading the plugin and grepping its contents. The API's
updated_atmoves on every upload, so it cannot distinguish "my upload landed" from "someone else's landed after mine".
The practical effect is that a user-visible defect stays live while every session believes it fixed it, which is much worse than a failed write.
What would fix it
- A precondition on the upload:
If-Matchon the plugin's current version/etag, 409 on mismatch, so a stale base is refused rather than silently applied. - Failing that, return the resulting content hash in the upload response so a caller can tell its own write apart from a later one.
Skills on this account have exactly this solved by going through a git repo with a real merge; plugins have no equivalent, and the upload API is the only route (there is no JSON write route — PUT/PATCH on the plugin answer 404/405, Allow: DELETE).
Environment
Cowork on claude.ai, Chrome on macOS, Opus 5. Related: #95465, #95408.