A package manager for AI assets (and why the lock file is per-user) | sx blog
Sometime in the last two years your repos quietly filled up with a new category of file. Not code, not config exactly: prompts. A .claude/skills/ directory here. A .cursor/rules/ folder there. A CLAUDE.md at the root, an AGENTS.md next to it, a .mcp.json listing the servers your agent is allowed to call. These are the things that make a coding agent useful on your codebase, and they’re sprawling.
The moment one of them is good enough that a teammate wants it, you copy-paste. Now there are two copies. Someone fixes a bug in one, the copies drift, and three months later nobody can tell which version is canonical or which repos even have it. Git is versioning each copy, but only inside its own repo. Nothing connects the copy in one repo to the copy in another, so there’s no shared source of truth, no way to say “everyone on the platform team gets this rule but nobody else,” and no way to know if anyone is actually using the thing you wrote.
A team I talked to a few weeks ago lives exactly this. They run five repos: one application and four microservices, and every fix to a shared skill has to be copied into all five. They’d been doing that for months, tweaking each copy a little as it landed, until the copies had drifted into near-duplicates that say roughly the same thing in slightly different words. One of their engineers called them versions “from different eras.” Someone eventually built a Confluence page just to inventory the duplication. Four of them meet every couple of days to ask who managed to migrate which repo, and the answer is always the same: no time, a deal just closed, something has to ship today.
This isn’t for lack of tooling. Claude Code has plugins and a plugin marketplace: bundle up some skills, commands, and hooks, push them to a git repo, and anyone can install the bundle. It’s genuinely useful. But it’s one client, and a marketplace install is all-or-nothing. It writes Claude Code’s formats and no one else’s, and it has no concept of “this rule for the platform team, that one for everybody.” The cross-team, cross-client version of the problem is still wide open.
So it’s a distribution problem, and distribution problems have a known shape: you build a package manager. We did; it’s called sx. Most of it is the boring, well-understood machinery you’d expect: a manifest, a lock file, a resolver. That part is solved, and sx just borrows the shape npm and Cargo already settled on.
The standard playbook gets you most of the way and then gives a few wrong answers, because AI assets break the package-manager mental model in places code packages never do. The headline one is heretical if you grew up on package-lock.json: the lock file is not committed, and it is different for every developer on the team. The unglamorous one is the integration tax nobody warns you about. A dozen AI clients took one good idea and each shipped an incompatible on-disk format for it. Those two problems get most of the space here. The back half is shorter: how the assets stay installed without anyone running a command, how a vault running on your laptop can serve the web clients like claude.ai, and an honest accounting of where the design leaks.
The boring part, briefly
sx borrows the manifest-and-lock split that npm, Cargo, and uv all landed on independently, because it’s correct.
There’s a manifest, sx.toml, which is the human-authored source of truth. It lists every managed asset, where its bytes live (an HTTP URL, a git ref, a local path), and who should get it. It’s committed to the vault. Here’s a trimmed asset entry:
[[assets]]<br>name = "python-docstrings"<br>version = "1.2.0"<br>type = "rule"
[assets.source-http]<br>url = "https://vault.example.com/assets/python-docstrings/1.2.0/bundle.zip"<br>hashes = { sha256 = "e3b0c442…" }
[[assets.scopes]]<br>kind = "team"<br>team = "platform"
And there’s a lock file, the fully-resolved artifact you actually install from, pinned by content hash. So far, nothing unusual. The interesting word in that manifest is scopes, and it’s what makes the lock file stop behaving like npm’s.
The lock file is resolved against you
In a normal package manager the dependency graph is a property of the project. Everyone who checks out the repo resolves the same graph and gets the same package-lock.json. The lock file is shared precisely because the question it answers, “what are this project’s dependencies?”, has exactly one answer.
The question sx answers is different: “what should this caller, standing in this directory have installed?” The manifest scopes assets to an org, a repo, a path within a repo, a team, a single user, or a bot identity. Resolving that requires knowing who’s asking. So resolution takes a second input that npm never needs, the caller’s identity, and the lock file becomes a per-user projection of the manifest rather than a shared fact.
Identity comes from git. sx shells out to git config...