Build an extension
An extension is an ordinary native binary. vrOS launches it, supervises it, and talks to it over a local pipe — a named pipe on Windows, a Unix domain socket on Linux — using VXP, a small JSON-RPC 2.0 protocol. One JSON object per line, one pipe per extension, and no network anywhere in the transport.
There is a Rust SDK crate that gives you the manifest schema, the wire types, and an async client. It is a convenience, not a requirement: VXP is plain JSON-RPC over a pipe, and any language that can open one and write a line can speak it.
For the shape of a working extension, read the quickstart.
Two tiers, one API
Section titled “Two tiers, one API”| Out-of-process | Bundled | |
|---|---|---|
| Runs as | its own process, spawned and supervised by vrOS | a supervised task inside vrOS |
| Crash isolation | full — the process boundary | panic isolation and restart |
| Language | any | Rust |
| Who ships it | you | vrOS |
The extensions that come with vrOS — VRChat OSC, the soundboard — are bundled, and they are built on the same API you get. If a first-party feature cannot be built on the extension API, the API is wrong. Both tiers go through the same dispatch path, so the semantics are identical, and moving an extension from bundled to its own process is a manifest change and nothing else.
The capability model
Section titled “The capability model”Your manifest, vros-ext.toml, declares what you need. Nothing is granted by
default. An installed extension appears in Settings → Extensions
as disabled, with its requested capabilities listed, and stays stopped until the
user decides. A version bump asking for something new re-asks for that delta only.
The declarable capabilities are bus, overlays, capture, audio, osc,
network, and secrets. Declaring none of them still gives you the handshake,
your own KV store, and your own bus prefix.
What the API covers
Section titled “What the API covers”| Namespace | What it does |
|---|---|
hello |
The mandatory first frame. The reply echoes the granted capability set and the user’s locale. |
kv |
A per-extension key/value store that survives restarts, crashes, and reboots. |
secrets |
Named slots in the OS credential store, never written to disk by vrOS. |
bus |
Publish and subscribe. You may always publish under your own ext.<id>.* prefix; anything else needs a pattern in your manifest. vr.lifecycle is the core topic relayed today. |
panel |
Push properties into your panels and receive their callbacks. |
commands |
Register named commands the user can bind and trigger. |
settings |
Read the values from a form vrOS renders out of your manifest. |
strings |
Locale-resolved lookup for panel text. |
overlays is served today: it gates defining a new panel at runtime, while the
panels your manifest declares need no capability. capture and audio are in
the manifest schema, but their namespaces arrive in later vrOS releases. The
host refuses what it does not yet serve, so asking early gets you a typed error
rather than silence. Bundled extensions that use audio — the soundboard — reach
the engine through host-internal paths rather than a VXP namespace; that gap
closes when the namespace lands.
Secrets are backed by Credential Manager on Windows. On Linux there is no credential backend wired up yet, so the store refuses to persist rather than pretending — plan on asking per session there.
Panels
Section titled “Panels”Panels are .slint source files that ship in your extension folder. The host
compiles them in a fenced interpreter, so no foreign code runs inside vrOS, and a
panel that fails to compile disables itself with a visible badge instead of taking
anything down with it. The host renders them with the same engine it uses for its
own screens, so your panel picks up the user’s theme, scale, and density
automatically.
Two roles are declared in [panels]: dash-tab puts a tab on the Dash rail and
on the desktop app, and overlay gives you a floating panel the user can place,
anchor to a wrist, and get back in the same spot after a restart.
Four rules shape how you write one:
- The only import allowed is
@vros/theme.slint. No other imports, no images, no@tr. - No
@trmeans words arrive as properties. Declare string properties and push the text from your extension. - Push properties only while the panel is shown. The host sends
PanelShownandPanelHiddenedges, and it rate-limits extensions that push regardless. - The manifest may suggest a panel size. The host clamps it.
Lifecycle
Section titled “Lifecycle”vrOS launches your binary with the endpoint in the environment. Send hello
within five seconds of launch, then initialize — anything sent before a valid
hello is refused. Your restart policy is never, on-crash, or always; a
crash restarts you with backoff, and an extension that keeps crashing lands at
disabled-with-reason in the Extensions pane rather than being quietly dropped. A
clean exit with code 0 is not restarted.
Treat KV as your only durable state and rebuild everything else on start. vrOS may restart you at any time, and the point of the store is that a restart reads as a resume.
run_without_vr defaults to true, so you keep running with no headset and no VR
runtime up. Extensions that care subscribe to vr.lifecycle; VR going away hides
your overlays and never tears down your state.
Behave
Section titled “Behave”Idle should mean idle. vrOS holds itself to zero work per frame when nothing has changed and expects the same of extensions: block on your connection and your own sockets instead of polling, and keep work inside the window between shown and hidden. Frames are capped at 1 MiB, and the host answers over-cap or hostile requests with typed errors rather than dropping you.
Where the SDK is
Section titled “Where the SDK is”The SDK crate, the reference hello extension, the theme module, and the authoring contract all exist and are licensed MIT or Apache-2.0 at your option, but they are not published yet — the crate is not on crates.io and the repository is private. This page and the quickstart will link them when that changes. Until then, ask in Discord.