sbox-build-feature

star 14

Use when building, modifying, or polishing any feature in an s&box game project through the Claude Bridge — gameplay systems, UI panels, character abilities, animation, world generation, anything that produces a visible or runtime change. Codifies the screenshot-driven iteration workflow that prevents the "guess-and-check" loop the bridge is most susceptible to.

LouSputthole By LouSputthole schedule Updated 6/9/2026

name: sbox-build-feature description: Use when building, modifying, or polishing any feature in an s&box game project through the Claude Bridge — gameplay systems, UI panels, character abilities, animation, world generation, anything that produces a visible or runtime change. Codifies the screenshot-driven iteration workflow that prevents the "guess-and-check" loop the bridge is most susceptible to.

Building s&box Features Through the Bridge

This skill is the workflow you follow whenever you're about to make non-trivial changes to a player in an s&box project via the bridge. It exists because the bridge gives Claude a lot of power but no eyes — without discipline, sessions devolve into guessing about how things look. These steps prevent that.

Pair this with the sbox-api skill — that's the brain (how to write correct s&box C#: the Unity→s&box translation table, the Ten Rules, and component/UI/networking/physics references). This sbox-build-feature skill is the hands + eyes (drive the editor, screenshot, verify live). Write it right with sbox-api; then build it, run it, and SEE it with the bridge. And the bridge's live reflection (describe_type/search_types/get_method_signature) is the authoritative signature check for your installed SDK.

Hard rule: never declare a visual feature "working" without seeing it

If your change affects anything visual (a model, a position, an animation, a UI panel, a particle, a light), you must see it before saying the work is done. You're a multimodal model — you can read PNGs.

Aim the camera, or you'll screenshot the wrong thing. mcp__sbox__take_screenshot renders from the scene's Main Camera — one fixed angle that usually isn't pointed at what you just changed. Use mcp__sbox__screenshot_from to point the camera at your target object/point, capture, and restore. This is the single highest-leverage habit in the whole workflow.

The Workflow — six steps, in order

1. Confirm the bridge is alive

mcp__sbox__get_bridge_status

If timed out: s&box isn't running, or the editor is mid-compile/relaunch. (The bridge's frame loop runs independently of the dock as of v1.3.0 — the dock no longer needs to be open.) Don't go further until it responds.

2. Brainstorm before code (for non-trivial features)

If the feature is more than a one-line tweak — anything that involves:

  • A new state machine
  • Animation, IK, or camera work
  • A new component or system
  • Anything where you can't predict the visual outcome with confidence

…invoke superpowers:brainstorming first. Don't skip this. The brainstorming skill exists because the cost of designing wrong is much higher than the cost of designing slowly.

3. Research the s&box API before guessing

Before writing code that calls a type or method you haven't verified exists in the current SDK build:

mcp__sbox__describe_type    name="CitizenAnimationHelper"
mcp__sbox__search_types     pattern="*Renderer"
mcp__sbox__get_method_signature  type="GameObject" method="AddComponent"

s&box's API changes between versions. Reflection is the source of truth, not your training data. Look it up.

If you need broader documentation (animation graph, IK setup, rendering pipeline), use WebFetch on https://wiki.facepunch.com/sbox/ or search Discord.

4. Implement with bite-sized edits

  • One change per Edit call. Don't batch unrelated edits.
  • Keep changes scoped to one file at a time when possible.
  • For the bridge addon specifically: never copy claudebridge.sbproj from the repo into a project's Libraries/ — the repo version has Org: sboxskinsgg (for asset library publish) and a project's working copy must stay Org: local. Mixing these causes a compiler-name collision that prevents the project from loading.

5. Hotload and verify compile

mcp__sbox__trigger_hotload

Then tail the log:

LOG="A:/SteamLibrary/steamapps/common/sbox/logs/sbox-dev.log"
tail -30 "$LOG" | grep -iE "Compile of 'local\.<projectname>.*Failed|Error \|"

Compile of 'local.X' Failed lines from earlier hotloads can be stale and survive in the log — what matters is whether the line timestamp is recent and the error message mentions YOUR file. If there's a real error: fix it, re-hotload, re-check.

6. Screenshot and read it yourself

For any visual change, aim the camera at the thing you changed:

mcp__sbox__screenshot_from   target=<object GUID or world point>

screenshot_from moves the Main Camera to frame your target, captures, and restores it. Plain mcp__sbox__take_screenshot renders the Main Camera's current angle — fine if it already frames your subject, useless otherwise. Either way the file saves to <sbox-install>/screenshots/sbox.<timestamp>.png (the path parameter is ignored — known quirk). Use Bash to list newest by mtime, then Read on the PNG. Look at the image. If it doesn't match the design, iterate. Don't declare the feature done based on the code looking right.

For diagnosing a compile/runtime failure, you don't need the editor to respond: mcp__sbox__get_compile_errors and mcp__sbox__read_log read sbox-dev.log directly.

For timing-sensitive captures (e.g. a 0.20s animation phase), coordinate with the user: "press the action and tell me 'go' the moment you do" — fire take_screenshot immediately, the round-trip captures roughly the right window.

Seeing & driving the RUNNING game (play mode)

The bridge can verify gameplay, not just the edit scene — but the play-mode tools behave differently from the edit-mode ones:

  • capture_view is the play-mode eyes. It renders a camera's view of the active scene (RenderToBitmap), so in play mode it captures the running game — no args = the live main camera (player POV); pass position/id for a temp camera at any angle. take_screenshot/screenshot_from are edit-only. After it returns, Read the PNG at the path it gives you.
  • capture_view sees through screen-space menus. It renders the world + world-space UI but NOT fullscreen screen-space panels (lobby/title ScreenPanels) — so a fullscreen lobby overlay won't black it out, but it also won't show screen-space HUD. take_screenshot (literal viewport) is the screen-space-UI complement.
  • To start a match / fire game logic / get past an in-game menu, use invoke_button. It calls any parameterless public method on a component (not just [Button]s) — e.g. invoke_button with component="SasquatchedGame" method="StartGame" leaves the lobby. The bridge can't synthesize a UI click; this is how you drive game state.
  • Networked components are proxies in a no-session solo playtest — a host-authoritative component (if (IsProxy) return;) won't run solo. Generate NPC brains etc. with networked:false to iterate solo, or start a host session.
  • Prove runtime behavior with get_runtime_property (an NPC's CurrentState, a health value, etc.) — unambiguous, and it works even under a menu overlay.
  • Play-state is reliable now: stop_play actually stops (symmetric EditorScene.Stop) and is_playing.isPlaying is authoritative.

Multi-agent work: the bridge drives ONE editor

The bridge connects to a single running s&box editor. Multiple agents cannot drive play-mode / screenshots / scene mutation concurrently — they'd fight over the same editor, the same play session, the same Main Camera, and the shared .scene file. The pattern that works for a parallel build:

  • Parallel agents AUTHOR on disjoint files only — each owns a non-overlapping set of .cs/.razor/.scss files and writes them with the normal file tools (Edit/Write), no play-mode, no scene edits, no screenshots. This is exactly why GameObjectSystem<T> self-bootstrapping (see the gotchas table) matters: a runtime system that needs no .scene entry lets several agents add gameplay/atmosphere/UI in parallel without anyone touching the locked scene file.
  • One orchestrator verifies serially — after the authors finish, a single agent (or the human) hotloads, drives play-mode, takes screenshots, and reads them. Don't have two agents call start_play/take_screenshot against the same editor.
  • If two things genuinely must run at once and both need the editor, they need separate s&box instances each with their own bridge — not the default setup.

Common s&box gotchas (so you don't re-discover them)

Gotcha What to do instead
Which math is sandbox-safe is NOT "MathX only" MathX is always safe but is small — it has Clamp/Lerp/LerpInverse/Remap/Floor/FloorToInt/ExponentialDecay etc. but NO Abs/Min/Max/Sin/Cos/Atan2/Sqrt/Pow/PI. Many projects (incl. bigfoot) also whitelist System.Math + System.MathF, so Math.Clamp/Math.Max, MathF.Abs/MathF.Sin/MathF.Atan2/MathF.Sqrt/MathF.PI compile there. Prefer MathX for what it has; fall back to System.Math/MathF for the rest — but verify the project allows it (describe_type "System.MathF" resolves, or just compile-check). For a trig-free oscillation you don't have to risk it at all: use a phase accumulator + triangle wave (var tri = 1f - Abs(phase*2f - 1f); advance phase += Time.Delta*rate, wrap with phase -= MathX.Floor(phase)).
Cloud-only assets (Cloud.Model("foo")) don't persist across project restarts Use local files or core engine assets
s&box doesn't support .mp3 Convert to .wav (ffmpeg is your friend)
Setting [Property] on a saved component overwrites the deserialized value Use field initializers as defaults that can be overridden in the inspector
TimeSince fields default to 0 → cooldowns fire immediately on spawn Initialize to a large value: private TimeSince _sinceX = 100f;
Hotload cache gets stuck after multiple iterations Restart the project or touch+hotload
Cloud.Model(variable) fails The source generator requires string literals — always inline
Citizen "head" bone exists but bone names are case-sensitive TryGetBoneTransform("head") — lowercase
CitizenAnimationHelper.IkRightHand is a writable GameObject — IK works at runtime Set it to a target GO to drive the hand via IK
set_property for Color wants "r, g, b, a" as a string, not a JSON object Format the value as a comma-separated string
take_screenshot renders the Main Camera (one fixed angle) and ignores its path arg Use screenshot_from to aim at your target; read the latest file in <sbox>/screenshots/
Runtime ParticleEffect tools (spawn_particle, add_trail, add_beam) don't render through the bridge Use spawn_vpcf (compiled .vpcf + LegacyParticleSystem)
Play-state flags is_playing.isPlaying is authoritative (gameFlag‖tracked); sessionPlaying is diagnostic-only and can read stale
A placed EnvmapProbe captures nothing until baked Call bake_reflections
Scene-mutating tools refused during play mode (v1.2.0+) Stop play, mutate, restart play
play_animation is overridden by the animgraph on a Citizen Use set_animgraph_param (duck, move_x/move_y, …) for Citizens; play_animation is for raw-sequence models
Code-gen tools (create_npc_brain, create_*_system, create_player_controller) write game-code strings — inspection misses compile errors Always compile-verify the generated component: describe_type <Class> resolves only if it compiled (or scan the log for error CS)
trigger_hotload doesn't reliably recompile externally-edited .cs Entering play (start_play) forces the project recompile; addon changes need a full restart_editor
Generated game code runs in the sandbox Only sandbox-allowed BCL compiles. using System; is needed for things you'd assume are global — e.g. Random.Shared.Float/.Int is System.Random.Shared + a Sandbox extension, so a missing using System; fails with "name 'Random' does not exist". (For math specifically, see the MathX/Math/MathF row above — it's nuanced, not a blanket ban.)
set_property/add_component_with_properties now coerce asset + reference props You can set Model/Material/GameObject/Component props by path/GUID; an unresolvable value returns success:false (no more silent null)
Trigger a Citizen attack/one-shot animation helper.HoldType = HoldTypes.Punch + helper.Target.Set("b_attack", true) (one-shot bool trigger). Reset HoldType after ~0.4s or the pose sticks
Play a sound in code Sound.Play("sounds/<name>.sound", worldPos) — paths resolve case-insensitively, no Assets/ prefix
Hit reaction / knockback CitizenAnimationHelper.ProceduralHitReaction(new DamageInfo{ Attacker=…, Damage=…, Position=…, Origin=… }, scale, force) (no factory — object initializer) poses the target's OWN helper; add to CharacterController.Velocity for a stagger. ⚠️ can throw an internal NRE on a freshly-spawned Citizen (unset DamageInfo.Hitbox/Shape) — wrap it in try/catch; the flinch is cosmetic, you apply damage/knockback separately anyway
Child GameObjects INHERIT parent tags a child (e.g. a flashlight beam) carries the parent's player tag — filter AI targets by an actual component (a health comp), not tag alone
Spawned player-camper has HealthComponent, the dummy has CamperHealth when an AI deals damage, try both health component types so it works on real players, not just test dummies
facepunch.playercontroller dropped PlayerController.AnimationHelper the property is GONE — get AND set throw MissingMethodException at runtime. (The on-disk Libraries/…/PlayerController.cs is a STALE cache that still shows it; the LOADED assembly is truth. describe_type "PlayerController" resolves the bare name to the wrong Sandbox.PlayerController — read the runtime log for the real error.) PlayerController still drives movement via CharacterController but no longer animates the Citizen. Fix: drive a CitizenAnimationHelper yourself each frame (WithVelocity/WithWishVelocity/IsGrounded/MoveStyle/WithLook from CharacterController.Velocity) — the same pattern an AI brain uses
s&box Razor @if, root-class="@(…)", and @Prop-as-element-text bindings do NOT re-render reliably This is the #1 UI trap. Show/hide: drive Panel.Style.Display = DisplayMode.Flex/None imperatively in OnUpdate (NOT @if). Dynamic text: <label @ref="X"/> + private Label X; + set X.Text = … each frame (a @Prop text-content binding stays stale/empty → an empty div renders invisible even when shown). Don't drive a root class="@(...)" toggle either. ✅ What DOES re-render: STATIC markup text, and style-ATTRIBUTE interpolationstyle="width:@(P)%", style="opacity:@(o)", etc. update fine, as does setting Style.* (Opacity/BorderTopWidth/Width) imperatively. Pattern: hold @ref handles, push the latest values in OnUpdate so the panel re-renders on its own tick regardless of who set them.
Absolutely-positioned HUD elements anchor to their nearest positioned ancestor A position:absolute cue with top/left placed INSIDE another position:absolute panel (e.g. a bottom-left .hud) positions relative to THAT box, not the screen — put screen-anchored overlays at <root> level.
set_property/set_runtime_property on a Vector3 silently no-ops Returns set:true but reads back the default; bool/float/string persist fine. Use a source default or a float knob for vector-ish config.
Runtime-SPAWNED GameObjects aren't GUID-addressable via the bridge set_runtime_property/set_enabled resolve only SAVED-scene objects by GUID ("GameObject not found" otherwise). Drive spawned objects via a [Button]/method on a scene component. Also: invoke_button's param is button (method/label name); get_scene_hierarchy ignores maxDepth (grep the saved .scene for GUIDs).
A round-restart that re-runs a "reset match state" wipes [Sync] values you poked in When fast-forwarding via the bridge (set flags → trigger), disable the AI that ends the round + set AutoRestartMatch=false, or the auto-restart's reset clears your flags mid-test.
You need a runtime system but can't add it to the scene (shared/locked .scene, or parallel agents must not touch it) Use GameObjectSystem<T> — the engine constructs exactly one per live Scene automatically, no scene-file edit. Spawn your host GameObject/components in public MySystem(Scene scene) : base(scene). Guard if (scene is null || scene.IsEditor) return; (never run in the edit scene) and make it idempotent (if (scene.GetAllComponents<MyHostComp>().Any()) return;) so a hotload — which re-constructs systems — doesn't spawn a second copy. Ideal for atmosphere/director/ambience systems. Working reference: Libraries/extended.extendednetworking/Code/AdminSystem.cs.
A Razor PanelComponent you can't reference from namespaced C# ("type or namespace not found") Razor .razor files generate their class into the GLOBAL namespace by default. Add @namespace Foo at the top of the .razor (matching the C# namespace Foo; that needs to AddComponent<ThePanel>() or hold a typed ref). Without it the panel type only resolves from global-namespace code.
There is no Light.Brightness Light intensity is the HDR magnitude of LightColor — scale its RGB (channels may exceed 1) for brighter/dimmer; a small-magnitude colour = dim. Scale by hand (new Color(c.r*k, c.g*k, c.b*k, c.a)) rather than trusting a Color×float operator that can differ between SDK builds. Night/atmosphere recipe (all runtime, no scene edit): retune the scene DirectionalLight (LightColor + its SkyColor fill + a low raked WorldRotation), drop AmbientLight.Color, darken SkyBox2D.Tint and set SkyBox2D.SkyIndirectLighting = false (stops the bright sky acting as a giant fill light), then add a GradientFog (Color/StartDistance/EndDistance/FalloffExponent/Height).
Non-positional sound (ambience, heartbeat, UI) plays in 3D / falls off with distance After var h = Sound.Play(evt); set h.ListenLocal = true; h.SpacialBlend = 0f; (fully 2D, same on every client — these are client-local, no networking). To loop a one-shot .sound without a looping asset, re-Sound.Play whenever !h.IsValid() || h.Finished || h.IsStopped, and push h.Volume/h.Pitch every frame so set_property live-tuning takes effect.
Floating 3D text without a WorldPanel go.AddComponent<TextRenderer>() (Sandbox.TextRenderer) renders text as a world quad — set .Text/.Color/.FontSize/.FontWeight/.Scale. ⚠️ Its HorizontalAlignment/VerticalAlignment are a nested enum the bridge's describe_type may not resolve by name — leave alignment at defaults in code (the label still renders, only its anchor differs) and set it in-editor if needed, rather than guessing an enum literal that won't compile.

The bridge map (knowledge graph)

The bridge ships a graphify map of itself at docs/graph/ — every tool maps to its C# IBridgeHandler and to the docs. Before adding or changing a bridge tool, consult docs/graph/graph.json or docs/graph/graph.html to see what connects to what. It CAN GO STALE (check the date in GRAPH_REPORT.md), and maintainers must regenerate it as part of every release (scripts/regen-graph.ps1, or re-run /graphify for the full doc-inclusive graph). See docs/graph/README.md.

Project-level CLAUDE.md

If the project you're working on has its own CLAUDE.md, read it first. It captures project-specific decisions (input bindings, sound files, role assignment, scene layout) that this skill can't know about.

The thing that always works

When you're stuck, in a loop, or about to make your fifth guess at a visual offset:

  1. Take a screenshot of the current state
  2. Read it yourself
  3. Describe to the user exactly what you see vs. what should be there
  4. Propose a specific adjustment (with magnitude) rather than another guess

The screenshot loop closes faster than the guess loop. Use it.

Install via CLI
npx skills add https://github.com/LouSputthole/Sbox-Claude --skill sbox-build-feature
Repository Details
star Stars 14
call_split Forks 5
navigation Branch main
article Path SKILL.md
More from Creator
LouSputthole
LouSputthole Explore all skills →