| name | sbox-live |
| description | Use when working in an s&box (Facepunch's Source 2 game engine) project: writing or editing C# components/Razor panels/scenes, debugging editor behavior, or interacting with the live editor through the claude-sbox in-editor MCP server. Trigger on `using Sandbox;`, `: Component`, `[Property]`, `[Sync]`, `[Rpc.Broadcast]`, `PanelComponent`, `*.razor`, `*.scene`, `*.prefab`, `*.sbproj`. Also trigger when editing files under `~/sbox-public/` or any folder named `addons/`. Replaces and supersedes the third-party `sbox` skill. |
sbox-live: live s&box integration skill
This skill is one half of the ClaudeSbox ↔ s&box deep integration. The other half is the in-editor tool addon (ghage/claude-sbox on sbox.game) that hosts an MCP server on port 6790 and exposes editor introspection / control to you.
Two endpoints, one registry. The same ~730-tool Dispatcher is reachable two ways: (1) the addon's own host on :6790 — the established path (sbox-mcp-bridge, claude mcp add … :6790/mcp); (2) the engine's built-in MCP server on :7269 (Facepunch ships one), through which the addon exposes a gateway. Same Dispatcher, same AutoToast/telemetry either way. Discover tools on demand instead of paging the whole list — the way Facepunch's built-in server works: on :6790 use search_tools(query) → call_tool(name, args) (plus list_toolsets/describe_toolset, and set_tool_list_mode listed for a tiny staleness-proof tools/list); on :7269 use sbox_search_tools(query) → sbox_call_tool(name, args) / sbox_call_tools(calls). Reach for list_tools only when you deliberately want the full inventory.
When you're in an s&box context you have ground-truth docs pipelines (live, no snapshots) plus live editor introspection + drive. The full capability map — all 13 buckets with their exact tool surfaces (schema, first-party + community + hosted docs, scene/inspector, UI drive, spotlight, preferences, reflection, compile/hotload + execute_csharp REPL, asset/cloud/event-bus, gizmo/convars/Qt, filesystem reads) — lives in references/mcp-tools.md § Capability map. The essentials you should keep in mind on every prompt:
Docs ground-truth, in layering order (default; deviate only when the user pinned a layer):
- Ground first with
schema_search_members(query) / schema_lookup_type(fqn) — confirms the symbol exists in this build and pins the canonical FQN, per-parameter types, and return type. The local schema (Facepunch.AssemblySchema over the editor's loaded DLLs) is stricter than any CDN snapshot. If connected=false, skip to step 2 (local prose still works).
- Narrative with
docs_search(query) → docs_get(path) for first-party Facepunch prose (lifecycle, RPC semantics, networking visibility).
- Community fallback with
learn_search(query) / learn_search(topic:"…", difficulty:"Beginner") when official docs don't cover it — faceted (difficulty/topic/tags/rating).
- Real-world usage with
codesearch_search(symbol) → codesearch_get_file(hit.source_url) — searches the source of every open-source package on sbox.game for actual call sites (not signatures/prose). Plain REST (public.facepunch.com/sbox/code/search/1/) — no driver/install. Queries leave the machine — skip for private-source identifiers. See references/mcp-tools.md § Code search.
Shortcut: a one-line concept ("RPC visibility rules") → start at 2; verifying a call you're about to write → run 1 alone (schema is ground truth); "how does anyone actually use this?" → 4.
Live editor drive (editor running + bridge connected): start with editor_state (one-call mode/scene/project/capability snapshot) and gate mode-dependent tools on its result; doctor is the session-opener readiness roll-up. Everything user-facing is reachable. The principle: anything the user can do in the editor, you can do — if you can't find a tool, try list_menus/list_shortcuts (almost everything is registered there), then run_console_command as the universal escape hatch. Reach for references/tool-families.md to answer "is there a tool for X?" across ~730 tools, and references/mcp-tools.md for per-subsystem detail.
If sbox_status reports connected=false, the editor isn't running or the bridge can't reach host.docker.internal:6790. Fall back to the schema/docs pipelines (which still work without the editor) and the references below.
How to respond to common asks
- "where is X?" → don't lecture. Resolve via
find_widget or list_docks/list_menus, then call spotlight with a one-sentence message. The user sees the highlight; you confirm in chat with one line. For multi-step answers ("how do I find the Asset Browser AND open a vmat from there"), use spotlight's tour mode (sequence: [...]).
- "do X for me" → if X has a
[Menu] entry, prefer invoke_menu. If [Shortcut], prefer invoke_shortcut. They go through the same code path the user's manual click would, so notifications/undo behave naturally. Reach for low-level scene mutations only when there's no menu/shortcut.
- "how do I X?" →
docs_search for the prose explanation, spotlight for the visual answer (tour mode if multi-step), then offer to do it for them.
- "set my editor to X" →
list_preferences to find the property, set_preference to apply.
- "is there a tool for Y?" → grep
references/tool-families.md first (curated one-liner index over ~730 bridge tools). Reach for list_tools only if the family isn't there.
- session opener → call
doctor once. It returns a structured pass/warn/fail roll-up plus a single next_suggested_action so you don't have to ping/sbox_status/compile_check_build_state/list_unsaved_scenes individually.
- "do four things in a row" → use
dispatcher_batch. Each op runs through the normal dispatcher (own LogCapture window); refer to earlier results via {"$ref": "alias.path"}. Saves agent turns and roundtrips.
- runtime bug you can't reproduce (no WASD/mouse injection) → have the user reproduce and freeze in the bad state, then read component fields with
get_property (ground truth beats guessing). Read a per-tick value like Velocity twice — if it's byte-identical, that component isn't ticking (disabled / inactive / proxy / paused); get_components shows the enabled flags. See references/gotchas.md "Live runtime debugging".
Routing: when to read which file
| If the user is asking about… | Open this reference file |
|---|
| Translating a Unity pattern to s&box | references/unity-translation.md |
| The Ten Rules of s&box (lifecycle, networking, async) | references/ten-rules.md |
| Common gotchas (namespace surprises, signature traps, set_property coercion — bool/Vector3/float/asset-handles now work, runtime-only props no-op in edit mode, bone GET=world vs SET=model space, Components.Get skips disabled, live runtime debugging, screenshot location, codesearch/news REST + privacy (queries leave the machine) and the forum-only Chromium driver lifecycle, auto_* naming, widget_drag rejections, game-code reflection whitelist vs full-trust editor addons, [Event] handler arg-count signature rule) | references/gotchas.md |
| Bodygroups: hiding/showing body parts on models (e.g. citizen) | references/bodygroups.md |
| Making store/marketing art for a package — the sbox.game thumbnails (Square 512², Wide 910×512, Tall 512×910) or a Discord WAYWO showcase (1200×800, 3:2) | references/publish-art.md — hand-write one HTML per canvas (uses /claude-design), screenshot via the browser MCP, crop with PIL. Same pipeline for achievement-icon sets. |
| Find &/or "watch" a tutorial video (a YouTube link, or "find a video on X") | references/watch-video.md. youtube_search(query) to discover (keyless), then youtube_watch(input) (MCP): transcribe + frame-per-caption → a viewing package in the game folder. First time → youtube_install. Then Read watch.md + frames/ from the game folder (use the returned output_dir_game_relative). |
Inspecting/editing an animation graph (.vanmgrph): find what drives an animation, disable a state-machine transition, change a node's sequence, add/connect nodes | Don't read a file: animgraph_source_inspect(path) to map nodes/connections/parameters/state-machines, then the animgraph_edit_* session tools (_load → mutate → _verify → _save) + animgraph_set_node_property / animgraph_set_transition_disabled / animgraph_connect / animgraph_add_node. Operates on the KV3 source — not nodegraph_* (that's ActionGraph/ShaderGraph only). See references/tool-families.md § Animation. |
| Live MCP tools you can call (curated, with usage stories) | references/mcp-tools.md |
| "Is there a tool for X?": discovery index across all ~730 bridge tools | references/tool-families.md |
| Which s&box doc area + which tool families cover a domain (Scene/Code/Editor/Assets/Graphics/UI/Gameplay/Networking/Services), and the non-negotiable engineering rules / quality gates | references/nine-categories.md |
| Driving an open file dialog or spawning a modal file picker | Call pick_file (modal blocking) or file_dialog_* (drive an already-open dialog) |
| Driving a tree widget (asset browser tree, scene hierarchy, etc.) | Call tree_list_items to discover paths, then tree_select_item / _expand_node / _activate_item |
| Driving a tab page widget | Call tab_list_pages then tab_select |
Filling an inspector input that doesn't have a [Property] (custom widget, popup dialog) | Call set_input_text / set_color / set_checkbox / set_slider_value / select_dropdown_option; set_widget_value for universal "I don't know the type"; inspect_widget to discover surface first |
| The exact signature of a method, property, field | Don't read a file: call schema_signature |
| Researching a symbol or API you're about to call (full pipeline) | Layer the queries: schema_search_members / schema_signature → docs_search → learn_search as a fallback → codesearch_search for real call sites. See top-of-file recipe. |
| How to use a system (RPC, Razor, Editor Tools, Hammer) | Don't read a file: call docs_search then docs_get; fall back to learn_search for community walkthroughs |
| "Show me how someone built X" / a beginner walkthrough / a community tutorial | Don't read a file: call learn_search(query) or learn_search(difficulty: "Beginner", topic: "UI"). Faceted; tutorials carry difficulty / topic / tags / rating. Pair with learn_get(path) for the body. |
| Find a tutorial video by topic, or "watch" a YouTube link | Don't read a file (until details needed): youtube_search(query: "<topic>") (keyless discovery → ranked videos), then youtube_watch(input: "<url>") — transcribes (yapsnap) + a frame per caption → a viewing package in the game folder; first use → youtube_install, diagnose with youtube_status. Then Read watch.md + frames/*.jpg from the game folder (the result's output_dir_game_relative). Full detail: references/watch-video.md. |
| "How does anyone actually use this API?" / find real call sites across published code | Don't read a file: call codesearch_search(symbol) (every open-source package's source), then codesearch_get_file(hit.source_url) for full context. Plain REST — no install. Queries leave the machine. See references/mcp-tools.md § Code search. |
| Browse / read a specific open-source package's source tree | Don't read a file: codesearch_list_files(org, package) to enumerate, then codesearch_get_file(org, package, file) for any file. |
| "What's the community saying about X" / find a forum thread / read a bug report or announcement | Don't read a file: forum_search(query) for the site's own index (older/specific threads), or forum_list_categories → forum_browse_category(slug) for recent threads, then forum_read_thread(url) for the posts. Forum is the only Chromium-driver family — first use → codesearch_install_driver (forum_driver_outdated → force:true + restart). Queries leave the machine. See references/mcp-tools.md § Community forum. |
| "What changed in the last update" / when did an API land or change / is this a known issue | Don't read a file: call release_notes(limit?) for recent update posts (each with titled sections), or release_notes(version: "26.05") to filter. Plain REST (/news/platform) — no driver. See references/mcp-tools.md § Release notes. |
| Disambiguating a short type name to a fully-qualified one | Don't read a file: call schema_search_members (local) |
| Discovery: "which types implement X", "every method tagged Y" | Don't read a file: call reflection_find_types_with_attribute / reflection_find_methods_with_attribute / reflection_get_type_hierarchy |
| Synchronizing with editor lifecycle (compile finished? scene saved? hotload happened?) | Don't read a file: call wait_for_<event> (or composites: wait_for_scene_state, wait_for_asset_ready, wait_for_editor_ready) |
| "Is the editor ready? What should I do first?": single-call readiness probe | Don't read a file: call doctor. It rolls up listener / dispatcher / active scene / compile state / playing tabs / unsaved scenes / recent engine errors and returns next_suggested_action. |
| Chaining multiple tool calls in one turn (create → reparent → add component → set property, etc.) | Don't read a file: call dispatcher_batch with operations: [{ key, tool, args }] and {"$ref": "alias.path"} substitutions. Up to 50 ops, sequential. |
Placing a .vmdl upright (the model's "up" axis is wrong) | Set the orientation once with orientation_override_set; subsequent drop_asset_into_scene calls auto-apply it. orientation_override_get/_list to inspect. |
"What does <expression> evaluate to in the editor right now?": live C# REPL | Don't read a file: call execute_csharp(code). System / System.Linq / Sandbox / Editor are auto-imported; pass imports for extras. Useful for "what's the active scene's GameObject count", "does this LINQ query return what I expect", "what's the current value of EditorPreferences.<X>". |
| "Find me a model/material/sound from the s&box library": cloud asset workflow | asset_search(query) → pick an ident → asset_fetch(ident) for full metadata → asset_mount(ident) to install. The default pin_to_project:true appends the ident to .sbproj PackageReferences so it auto-mounts on next session; pass false for one-session-only. asset_unpin(ident) removes the pin without unmounting the running session. |
Anti-hallucination rule
Before you write a method call, attribute, or component lifecycle method that you haven't seen verified in this session, call schema_signature or schema_lookup_type to confirm it exists. Hallucinated APIs is the #1 failure mode in s&box code generation; the live schema fixes it for free.
For attribute-driven discovery (e.g. "which types are [GameResource]s?", "every method tagged [Menu]", "what does [Range] actually store?"), reach for reflection_find_types_with_attribute / reflection_find_methods_with_attribute / reflection_parse_attribute_metadata: these are richer than schema_* because they walk relationships, not just signatures.
If sbox_status reports connected=false and you can't call schema_* / reflection_*, fall back to references/unity-translation.md and references/ten-rules.md for the highest-frequency anti-patterns.
Editing live state
When the user asks you to change a value in the inspector ("set the player speed to 250", "disable the second component"), prefer the MCP tools over editing the .scene JSON by hand:
get_selection to see what's selected.
get_components(id) to enumerate.
set_property(id, component_index, name, value): runs through the editor's undo scope, so the user sees a normal undoable change. Handles every common type — bool (true/false), numbers, Vector3/Color ({x,y,z}/{r,g,b,a}), enum-by-name, and asset handles (pass a content-path string for a Model/Material, e.g. "models/dev/box.vmdl"). Always verify: the response includes previous and current. If they're equal, either the write missed or the property is runtime-only (e.g. Rigidbody.Velocity no-ops in edit mode) — confirm with get_property and, for runtime props, test in Play.
When the user asks you to write code, prefer Read/Edit/Write against the bind-mounted source tree (your cwd is the s&box project root).
Applying C# edits made from outside the editor (the WSL/9p gotcha) — use recompile_project. The editor picks up code edits via a FileSystemWatcher (Compiler.Watch.cs → OnFileChanged → MarkForRecompile), but on a WSL/9p-mounted tree the OS never delivers change events, so the editor keeps compiling a stale in-memory snapshot of your files until a full restart — your edit silently never takes, and compile_get_diagnostics reports errors at impossible line numbers (it's reading the old cached compiler state). Two tools, know the difference:
recompile_project (added to this addon for exactly this) — the one to use after editing .cs/.razor from outside the editor. It marks the project's real compilers dirty (bypassing the change-detection that never fired), rebuilds them reading source fresh from disk (Compiler.SyntaxTree.cs: CollectFromFilesystem, content-hash change detection), and hotloads into the running editor/game — no restart needed. Returns ok/error_count/errors. The final package-reload flush restarts the MCP bridge one tick after the response is sent, so the connection may drop right after — that's expected; the operation completed, just ping and continue. If the editor is in PLAY mode, it stops play FIRST (returns to edit mode) before the package reload — hotloading into a live play scene makes the engine migrate runtime component instances across assembly contexts (old IsolatedAssemblyContext → new), and component-typed [Property] refs (e.g. a SoundEmitter) can't cast across contexts, so the scene throws InvalidCastException/TargetException on deserialize and components "don't load even though they're there". Stopping play first makes the reload re-deserialize the edit scene from disk in a single context (the robust path), and also stops the play session pinning the outgoing context (repeated mid-play hotloads otherwise leak a growing pile of stale IsolatedAssemblyContexts — only a full process restart clears them, via the restart_editor tool documented just below). The response carries stopped_play:true when it did so; press Play afterwards for a clean single-context session. Pass keep_playing:true to opt out and hotload into the live session anyway (the old, error-prone behaviour).
compile_project — a throwaway publish-style validate compile (fresh CompileGroup, reads disk). Good for a yes/no "does it compile" gate, but it does not hotload, so it won't apply your edit to the running session.
If the bridge tools aren't loaded yet (e.g. you just edited the addon itself), drive the real compile inline with execute_csharp via reflection: get Sandbox.Project.Current, call the public Compiler.MarkForRecompile() on its internal Compiler+EditorCompiler, await the internal static Project.CompileAsync(), then await EditorUtility.Projects.WaitForCompiles(). A green Compiler.BuildSuccess is ground truth (the in-editor compiler's .Diagnostics array is the Roslyn diagnostics for the fresh build). If you can't hotload at all, C# also loads on a Play restart (the play tab does a fresh compile).
Hotload assembly-context accumulation — the clean-slate fix is restart_editor. Every hotload (recompile_project, or a normal in-editor save) spawns a new collectible IsolatedAssemblyContext for the package. The engine does unload the old ones (they're removed from LoadContext.Children + PackageLoader.Loaded and Unload()'d), but they stay resident because they're rooted OUTSIDE every managed collection — native GCHandles / interop pins that a managed addon can't null and a forced GC can't collect (verified: scanning 86k+ static fields + 16k static delegates finds zero managed roots). So they pile up across a long session (10s of versions), and eventually Play breaks: TypeLibrary could not find GunGame.Cinder.MainMenu (and friends) on serialize, struct-array hotload-upgrade errors (Source/Destination array is too small), and cross-context cast failures. A hotload can't fix this and neither can GC — only a process restart clears it.
restart_editor (added to this addon for exactly this) — relaunches the editor process on the same project (Editor.EditorUtility.RestartEditor(): closes the window, starts a fresh sbox-dev.exe -project <current>). Saves every open edit-session scene first (pass save:false to skip — do that if the scene is already in the degraded multi-context state, so you don't serialize a broken scene over the good on-disk one), then defers the relaunch one tick so the response flushes. The bridge drops with the old process; the new editor boots the addon and re-listens on :6790 in a few seconds — ping until it answers, then continue (same reconnect pattern as recompile_project). Slower than a hotload and it resets play state, so use it when accumulation bites, not per-edit.
- Detect it early: count the loaded contexts with
execute_csharp — AppDomain.CurrentDomain.GetAssemblies().Where(a => a.GetName().Name == "package.local.<game>").Count(). One is healthy; a steadily-climbing number (5+, 10+) means the accumulation is building and a restart_editor is due before the errors start.
Editing assets the editor must recompile (.shader, .vmat, .vmdl, …): the editor's file-watch frequently does not see edits written from outside the editor (e.g. an agent writing into a WSL/9p-mounted tree), so it keeps the stale compiled *_c and your change silently never takes. Deleting the *_c to force a rebuild makes it WORSE — the engine's on-demand recompile path (Asset.Compile) errors (Invalid Dependency Information) and you get the pink/checkerboard error material. The fix differs by asset type:
.shader: Asset.Compile is broken for shaders on this setup, so the bridge routes shader recompiles straight through the engine compiler, Sandbox.Engine.Shaders.ShaderCompile.Compile(absPath, relPath, {ForceRecompile=true}, token). Call shader_compile_and_check(path) (or asset_recompile(path) — same path for .shader): it reads the source fresh from disk (so it sees your external/WSL edit), writes a fresh .shader_c, and returns success + per-program (VFX_PROGRAM_VS/_PS) results plus the engine's compile error lines (e.g. *** Error! "g_tGround" can only use Default1! — the error text only reaches the engine log, not the compile Results, so the bridge captures it for you). A clean compile + valid .shader_c is what you want.
- other assets (
.vmat/.vmdl/…): asset_recompile(path [, full=true]) runs the normal Asset.Compile pipeline and writes a fresh *_c.
Then Play-restart to pick it up in the running game (a newly written .shader_c does not hot-load mid-session — see the runtime-shader quirks below). Custom runtime Material.Create("name","shaders/x.shader") shaders are especially bitten by the file-watch staleness — always recompile the .shader after editing it.
Fallback (the bridge tools aren't reloaded yet, e.g. you just edited the addon): run the engine compiler inline with execute_csharp via reflection (the static ShaderCompile.Compile is internal, so a direct call won't bind — use MethodInfo.Invoke):
var sct = typeof(Sandbox.Engine.Shaders.ShaderCompile);
var mi = sct.GetMethods((System.Reflection.BindingFlags)0x3E).First(m => m.Name=="Compile" && m.GetParameters().Length==4);
var opt = Activator.CreateInstance(sct.Assembly.GetType("Sandbox.Engine.Shaders.ShaderCompileOptions"));
opt.GetType().GetProperty("ForceRecompile").SetValue(opt, true);
var asset = AssetSystem.FindByPath("shaders/x.shader");
var t = (System.Threading.Tasks.Task)mi.Invoke(null, new object[]{ asset.AbsolutePath, asset.Path, opt, System.Threading.CancellationToken.None });
t.GetAwaiter().GetResult();
Do not use AssetSystem.FindByPath(...).Compile(true) for a .shader — that's the broken on-demand path (Invalid Dependency Information, writes no _c). It still works for non-shader assets. Verify either way by checking the *_c file's mtime/size updated.
Runtime custom-shader quirks (Material.Create("name","shaders/x.shader"))
Hard-won, all true together — design around them:
- Render-attribute binding is partial. Setting per-object params via
renderer.SceneObject.Attributes.Set(name, value) (read in the shader as < Attribute("name"); >) works for textures (Texture2D) and float2, but silently fails for float and float4/colour params — those keep their shader Default. (You can tell because heightmap + UV-mapping bind fine while a colour/scale won't.) Fix: don't push scalars/colours as attributes — bake them as shader Defaults and make a separate .shader variant per look (e.g. snow_deform white-default vs sand_deform sand-default). Defaults always render.
- Recompiling an already-loaded shader doesn't hot-swap it.
Asset.Compile(true) rewrites the *_c on disk, but the editor keeps the previously-loaded shader program in memory; even a Play restart (and sometimes a full editor restart) won't pick up the change for a shader that was already loaded this session. A brand-new .shader filename has nothing cached, so it loads fresh on first use — prefer creating a new variant file over editing-in-place when you need the change to actually take.
Material::Init() + manual ShadingModelStandard::Shade(i, m) is the reliable opaque-lit path (set m.Albedo/Normal/Roughness/Metalness/AmbientOcclusion/Opacity, optionally m.Emission). Read world position in the pixel stage via a custom interpolant (o.vWp = o.vPositionWs.xy in VS) — i.vPositionWs is a VS-only field; i.vTextureCoords.xy is the valid PS UV. g_flTime is available for animation.
- For an emissive look on an existing material, prefer using the material's own authored emissive map (most stock ground/lava
.vmats ship one) + a low-threshold camera Bloom post-process, rather than a custom shader — far less fragile than runtime shader plumbing.
Believe the user about what's on screen
When the user tells you what they observe in the running game — "there is no HUD",
"the font isn't showing", "the digits are cut off", "it works now" — treat it as ground
truth and act on it directly. Do NOT take screenshots to verify or contradict them. The
bridge generally cannot capture the in-game UI reliably (screenshot_scene_to_file
excludes screen-space panels; widget capture of the 3D viewport returns blank), so a
screenshot is more likely to mislead than confirm — and re-checking what the user just told
you wastes their time and erodes trust. Reason about the cause from their description, make
the fix, and ask them to confirm. Use screenshots only when the user hasn't said what they
see and you have no other signal — never to second-guess an explicit statement.
What you should NOT do
- Don't suggest
MonoBehaviour, Awake, Start, Update, [SerializeField]. See references/unity-translation.md for the s&box equivalents.
- Don't use
System.IO.File, System.Console, System.Net.Http.HttpClient, raw sockets: they're sandbox-blocked. Use FileSystem.Data, Log, Http.
- Don't write
Physics.Raycast(...). Use Scene.Trace.Ray(...) builder. (Rule 6.)
- Don't write coroutines (
IEnumerator, yield return). Use async Task + await Task.DelaySeconds(n). (Rule 8.)
- Don't
using a namespace you haven't verified exists. The sandbox blocks unknown namespaces at compile time.
What you absolutely should do
- Mark gameplay classes
sealed. (Rule 1.)
- Use
protected override void On*() for all lifecycle. (Rule 2.)
- Tag inspector fields
[Property]. (Rule 3.)
- Tag networked state
[Sync], guard with if ( IsProxy ) return;. (Rules 4 to 5.)
- Run a
schema_search_members whenever you're tempted to guess. The schema is ground truth.