[BUG] Account Skills have no read-back and no compare-and-swap, so concurrent sessions silently destroy each other's edits

Status Open
Maintainer reply None cached
Activity 1 comment · opened Aug 26, 2026

Summary

Updating an account Skill is a whole-file replace with no way to read the stored body back and no concurrency control. Two sessions editing the same skill on the same day will silently overwrite one another, with no error on either side and no way to notice except by luck.

This happened three times in two days on my account (25–26 Aug 2026), on two different skills, to sessions that were each individually doing everything correctly.

Steps to reproduce

  1. Session A copies the synced skill body, adds a section, uploads it (via the claude.ai skills endpoints — rename-skill aside, upload-skill, delete-skill the aside copy, since there is no update verb).
  2. Session A verifies: the skill is listed, updated_at is fresh, the synced mirror matches byte for byte. Correct at that moment.
  3. Two hours later, Session B — which took its base before A's upload — adds its own section and uploads.
  4. A's section is gone. No error is surfaced to either session. Nothing on the account records that a version ever existed with A's text in it.

What makes it undetectable rather than merely racy

  • No read endpoint. GET skills/{id}, skills/get-skill, skills/download-skill, skills/{id}/files all 404. The account holds no readable copy of a skill's body, so a session cannot diff its intended write against what is actually stored — not before writing, and not after.
  • No compare-and-swap. upload-skill takes no sha, if-match, base_version or equivalent. There is no way to say "write this only if the stored copy is still the one I edited".
  • No version history. Once overwritten, the previous body is unrecoverable from the platform.
  • The local synced mirror is not a substitute. It lags by minutes (sometimes much longer), and on some surfaces it is a session-start snapshot that never refreshes, so a stale read there is indistinguishable from a lost write.
  • updated_at moving proves a write landed, not whose. Both sessions see a fresh timestamp after their own upload.

Why this matters more for skills than for ordinary files

Skills are the mechanism by which an agent's own operating rules persist. A silently lost skill edit is a corrected behaviour that quietly reverts — the failure resurfaces later as the agent repeating a mistake it had already been taught not to make, with nothing in the record explaining why.

What would fix it, in order of preference

  1. A read endpoint returning the stored SKILL.md (and ideally the bundled references/, scripts/). This alone makes the problem detectable, which is most of the value.
  2. Compare-and-swap on upload: accept an opaque version token from list-skills and reject the write with 409 if the stored version moved. GitHub's sha parameter on contents writes is the exact shape.
  3. Version history, so an overwrite is recoverable rather than terminal.
  4. A true update verb. Today the only route is rename-aside → upload → delete, which widens the window and strands a -prev copy whenever a session dies mid-sequence.

Current workaround, for anyone hitting this

An external mutex (a lock file in a private repo, created without a sha so creation is an atomic test-and-set), plus re-reading the synced mirror immediately before packaging, plus a content check after upload — and when the check fails, rebasing onto the other session's version rather than re-uploading, since re-uploading turns one lost edit into two. It narrows the window to seconds; it cannot close it, because the platform exposes nothing to close it with.

Related

  • #87071 — no in-session way to save/update a skill, and no confirmation that a save happened. Neighbouring but distinct: that one is about the absence of a save path; this one is about the save path that does exist being unsafe under concurrency and unverifiable afterwards.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗