| 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, animation graphs, 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`, `*.vanmgrph`/`*.vsubgrph` (animation graph KV3 source), and animgraph concepts (anim nodes, state machines, blend spaces, IK, anim parameters). 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 |
Porting a self-contained / foreign viewmodel (a Rust/CS gun+hands mesh on its own skeleton) into an FP viewmodel system, and adding more of them: sourcing sibling models (the rust org on sbox.game — all share one skeleton, so one bone map + one animgraph drive them all), making arm tuning + bone map per-weapon (the second-gun refactor), retargeting the Facepunch arms onto a skeleton with 0 shared bone names, killing the 1-frame arm jitter (read rendered bone objects, not the animation pose), hiding baked hands by an invisible material override, authoring/name-swapping the weapon animgraph (no baked graph; auto-reset params, full-auto fire re-trigger), viewmodel lighting/darkness (tagged-light trap, metal complex vs skin), placement of a model authored for another camera (pivot-target math), muzzle/eject attachments-or-child effects, and the give-a-weapon test/repro + screenshot loop | references/self-contained-viewmodel-arms.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. |
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 +, the internal static , then . A green is ground truth (the in-editor compiler's array is the Roslyn diagnostics for the fresh build). If you can't hotload at all, C# also loads on a (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>). Defers the relaunch one tick so the response flushes. Slower than a hotload and it resets play state, so use it when accumulation bites, not per-edit.
- ⚠️ SAVE HAZARD — this can silently destroy scenes. Pass
save:false when accumulation has already bitten. restart_editor saves every open edit-session scene first by default, and once contexts have accumulated the very components that are failing to resolve (TypeLibrary could not find GunGame.Cinder.MainMenu) are sitting in those scenes as MissingComponents — and s&box DROPS MissingComponents on save. So the pre-restart save serializes each open scene minus its unresolved components, permanently overwriting the good on-disk file. (Observed: a restart_editor with the main-menu scene open stripped GunGame.Cinder.MainMenu + the SoundEmitter/MainMenuStemMixer/TemporaryEffect objects — the menu "completely disappeared"; the file halved in size.) Therefore: the moment you see context-accumulation symptoms (missing-type / struct-array / cross-context errors), any open scene is already degraded — restart with save:false, never the default. Belt-and-suspenders for a long editing session: back up the Assets/scenes/ + Assets/prefabs/ source .scene/.prefab files up front (they're small), and after any restart diff each scene's size/component-list against the backup — a scene that shrank was stripped; restore the source file (and its .scene_c) from the backup, then recompile/refresh. Detecting stripped scenes: python3 -c "import json; d=json.load(open('Assets/scenes/x.scene')); ..." and walk __types, or just compare byte size to a known-good copy.
- 🚨
restart_editor MAY NOT RELAUNCH — it can end your session. Confirmed twice, identically (2026-07-16/17, WSL/Docker, agent in a Linux container). The tool returns ok/, the editor closes, and : port dead, and (so the process is gone, not busy compiling — that's the check that tells the two apart). A human had to relaunch by hand both times. This is a , because accumulation (below) has no other fix: hotload enough → wedges → the only cure strands you. If a human isn't watching, prefer batching your edits over more recompiles —
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(); // writes shaders/x.shader_c
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.