Skip to content

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.

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.

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.

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 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 @tr means 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 PanelShown and PanelHidden edges, and it rate-limits extensions that push regardless.
  • The manifest may suggest a panel size. The host clamps it.

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.

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.

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.