Contents
LSL — Linden Scripting Language — is the embedded language of Second Life. It’s been around since 2003. It’s event-driven, sandboxed, and full of traps that a general-purpose LLM will happily walk into without any sign of embarrassment. Ask a stock model to write a HUD that talks to an HTTP backend and you’ll get something that looks right, compiles, and then silently drops events because it queued more than 64. Or it’ll misuse llListen. Or it’ll forget that dataserver is asynchronous and read the wrong avatar’s name.
I’ve been writing LSL long enough to know where it hurts. I’m also a software developer who’s been building AI tools for the past couple of years, and I use Claude Code as my daily driver. So the question I kept coming back to was: can I make Claude Code a competent LSL developer? And once it can write the script — can I skip the clipboard entirely and have it deliver the script directly to my avatar in-world?
The answer to both is yes. Here’s how the stack works.
Teaching Claude Code LSL without choking the context window
The first step is building your own LSL reference file. Scrape the LSL Portal on the Second Life wiki — all six sections — and compile it into a single llms.txt. Mine came out at 371 KB. That’s a lot of context, and loading all of it upfront every session would eat a quarter of a 1M window before you’d said anything useful. Most of it would also be irrelevant to the task at hand.
The solution is to shard it by topic and let the agent read on demand. The reference splits cleanly into six parts:
| Topic | Part |
|---|---|
| Types, operators, states, control flow | 01 — Language & types |
| String & list functions | 02 — Strings & lists |
| Math, vector, rotation | 03 — Math, vector, rotation |
| Prim, object, inventory, linksets | 04 — Prim & object |
| Avatar, physics, sensors, HTTP, comms | 05 — Avatar, physics, comms |
| Events, constants, working examples | 06 — Events, constants, patterns |
The agent reads on demand. Writing a touch handler? Open part 06. Doing rotation math? Open part 03. The other 300 KB stays on disk.
The more important file sits on top of the raw reference: a hand-compiled distillation of what trips up experienced LSL developers. Not a documentation dump — a war diary. Sections include:
- Memory limits (Mono: 64 KB, Pre-Mono: 16 KB) and the stack-heap collision pattern
llListencaveats — the 65-handle limit, filter immutability, cleanup on state change- Timer behaviour — persists across states, one per script, period
- HTTP-in URL lifecycle — must be re-requested on
CHANGED_REGION,TELEPORT,REGION_START dataserverasync pattern — always matchquery_idor you’ll read the wrong avatar’s name- A flat anti-patterns table: common bug → cause → fix
- Notes on SLua, because Linden Lab is migrating LSL to Lua and new code should at least be aware
The rule that makes this work: “Always consult this before writing LSL scripts.” It’s the first thing the agent loads before touching LSL. That one instruction, backed by a 600-line knowledge base, makes Claude write better LSL than any amount of fine-tuning on the wiki would. It’s reversible, debuggable, and updatable in 30 seconds. Don’t fine-tune — curate.
Getting the script into Second Life without touching the viewer
Even with a working script, the loop of “save file → open viewer → paste into script editor → save → drop into the right prim” makes iteration painful. Ten minutes of viewer-fiddling for a script that Claude wrote in 45 seconds.
My bot fleet runs as headless avatars in containers. Each bot holds its own logged-in session against the SL grid and exposes an HTTP API — but not a public one. Every request requires HMAC-SHA1 signing: sha1(commandName + args + unixtime + shared-secret). Raw curl gets a 401. That’s deliberate.
The signing key never lives in the dashboard. A separate container — the relay — holds the secret and exposes a single JSON endpoint on the internal container network:
POST /relay
{"bot": "deliverer", "command": "UploadScript", "args": [lsl_source, item_name]}
The relay signs the request, forwards it to the right bot, parses the response, and returns clean JSON. The dashboard talks to the relay. The relay talks to the bots. The dashboard can’t touch the fleet directly.
This matters because the dashboard is on the internet, parses user input, and renders HTML. If it held the signing secret, a single injection would hand an attacker the whole fleet. The relay is on an internal-only network with no public route. It accepts one JSON shape. That’s its entire attack surface.
Getting a script into my avatar’s inventory is two relay calls:
# Upload to the deliverer bot's inventory
up = relay("deliverer", "UploadScript",
[lsl_source, "AgentVibes Bot HUD v3.16"], timeout=60)
item_uuid = up["reply"]
# Give the item to my avatar
send = relay("deliverer", "SendItem", [item_uuid, MY_AVATAR_UUID])
About three seconds later, an inventory offer pops in the viewer. Accept it, drag it into the prim, done.
The gotchas that aren’t in the API docs
Three things have bitten me enough to be in the runbook now:
UploadScript needs an explicit timeout. The default relay timeout is short — most commands return in under a second. UploadScript blocks on the SL asset server, which in the bad case takes tens of seconds. Forget to pass timeout=60 and you get a confusing partial success: the script uploads, the dashboard reports a network failure, and there’s an orphan item in the bot’s inventory with no record of it.
The asset-upload capability expires after ~24 hours idle. Symptom: "Timeout creating blank script item". Fix: restart the deliverer’s container to force a fresh login. That error message has nothing to do with the script being blank — it means the upload capability is stale. Knowing that saves an embarrassing amount of script-rewriting.
Always version the item name. SL inventory has no concept of “update this item.” It just has items with names. Three unversioned iterations of the same HUD all look identical. The convention is AgentVibes Bot HUD v3.16 — a HUD_VERSION constant near the top of the dashboard’s HUD module, bumped on every LSL change. The script name is the version-control system. It’s inelegant. It works.
The full loop, end to end
For re-flashing my HUD from the dashboard, the sequence is one click:
- Browser →
POSTto the dashboard’s install-HUD endpoint - Dashboard → builds the LSL source, baking in the current callback URL and a fresh per-deployment bearer token
- Dashboard → offloads to a thread (UploadScript blocks for tens of seconds; the FastAPI event loop shouldn’t)
- Dashboard → relay:
UploadScript(lsl_source, "AgentVibes Bot HUD v3.16") - Relay → signs, forwards to the deliverer bot
- Deliverer → asset upload to the SL grid → new item UUID
- Dashboard → relay:
SendItem(item_uuid, MY_AVATAR_UUID) - Deliverer → offers the item to my avatar
- Viewer → inventory offer pops, I accept, script lands in inventory
For ad-hoc scripts — a particle emitter, a chair scanner, a one-off proximity greeter — the same two relay calls work directly from Python. No file ever lives on disk in the viewer’s account. The waypoint prim scripts that power my A* pathfinding and digital twin system were built this way: Claude Code wrote the touch handlers and webhook calls, the relay delivered each iteration to my avatar, and I dragged it into the prim and tested it — no copy-paste, no viewer script editor.
One rule on the LSL source itself: don’t echo it to stdout. The HUD has a per-deployment bearer token baked in at build time. If the agent prints the source “to confirm what it wrote,” that token ends up in transcripts, logs, possibly screen-recordings. The pipe is Python string → relay → SL asset server. The token never crosses stdout.
Want to go deeper with Claude Code?
I run 1-on-1 Zoom coaching sessions for developers and non-developers who want to build real things with AI — from your first project to shipping actual tools. Hands-on, project-based, no fluff.
Book a coaching session →
What the pipeline actually produced
The archive that built up over a few months splits into three tiers. About five scripts form the navigation and world-model layer — the things that teach bots where they are. Another five are interactive in-world applications — touch a prim, get a menu, something useful happens. And a long tail of small throwaway tools under 50 lines each, the LSL equivalent of bash one-liners.
Here are the ones worth talking about.
The waypoint prim
The most-edited script in the archive — over 800 lines at v2, handling every edge case that comes up when prims get rezzed, reset, and region-restarted in the wild. Drop it into any small sphere, rez copies around your venue, and each copy registers itself as a node in the walkable graph. On rez the prim glows ice-blue with hover text “Ready — touch to register.” Touch any prim and it broadcasts on a shared channel so every sibling fires its own HTTP request to the dashboard, each with a small jitter delay so the server isn’t slammed simultaneously.
After registration, the prim turns green. Touching a green prim re-registers just that one — useful after you nudge it. The version history tells the design story: v1 had auto-register on rez with a grace timer. v2 removed it. Silent registers from accidental script reloads turned out to be more confusing than helpful, so registration is now strictly user-driven.
This is the script that the waypoints and A* pathfinding post is built on.
The linkset scanner
The problem this script solves: before you can map waypoints intelligently, you need to know what’s actually in the space. Where are the walls? How big is that building? What’s blocking the path between two nodes? Second Life gives bots no built-in answer to any of those questions — the geometry is invisible to scripts unless you go read it yourself.
So this script reads it. Drop it into any copy-mod linked object — a building, a piece of furniture, an entire room set. On first rez it walks every linked prim, reads shape, size, position, rotation, and face colour, and posts the data to the dashboard in chunked HTTP POSTs (LSL’s HTTP body limit is around 2 KB; complex linksets can be tens of KB of JSON). A begin / chunk / commit three-endpoint protocol on the server side lets the object stream cleanly even across a region restart mid-scan.
The dashboard renders each prim as a box in a Three.js viewer — not an exact replica of the build, but a rough spatial model that shows you where things are and how big they are. That’s the digital twin: a browser-based representation of your sim’s geometry that you can orbit around, overlay your waypoint graph on top of, and use to spot gaps in coverage. Which wall is that disconnected cluster sitting behind? Why can’t the bot reach that corner? The viewer answers both in a second.
After the first scan, touching the prim opens a menu: Rescan, Edit Name, Set Region, Open Viewer. “Open Viewer” pops an SL URL dialog that jumps straight to the right region in the 3D viewer. The whole point is that waypoint mapping and spatial debugging become a visual task instead of a coordinate-arithmetic task.
The storybook prim (the biggest one we wrote)
This one was built for language learning. The idea: a Second Life visitor walks up to a prim, touches it, picks a story, and reads it page by page on a MOAP (Media On A Prim) — the SL feature that lets a prim surface display an image or web page. Each page is fetched from the dashboard, optionally narrated aloud by a voice bot standing nearby. It’s a language lesson delivered entirely inside a virtual world, with no browser tab, no app, no external platform.
The prim pulls a story list from the dashboard on reset and displays the cover image of the first story on face 0. Touch → blue menu listing every available story. Pick one and it pages through, fetching each page image in sequence, optionally narrating via the bot voice stack.
Over 1,200 lines — not because anyone overengineered it, but because LSL forces you to inline everything. Every state, every event handler, every page-timing edge case, every “what happens if a second reader touches the prim mid-story” branch lives in the same file. There’s no factoring out a class. The discipline is keeping it readable inside that constraint.
Worth noting: at 1,200 lines it still fits inside the 64 KB Mono memory limit with room to spare. LSL is constrained, but it isn’t that constrained — you can build real things in it.
The holodeck relay
A 160-line script that bridges the dashboard’s Scenes feature to a third-party holodeck system. The dashboard says “rez the tavern scene” → the relay receives an HTTP POST and shouts the command on a channel the holodeck listens on.
The small design win: the relay self-registers with the dashboard every time it’s granted a new URL. SL’s llRequestURL URLs change after every region restart. Before v2 of this script, that meant manually copy-pasting the new URL into the dashboard after every restart. After: the relay calls home on its own, the dashboard updates its name→URL table, and humans never touch URLs again. I’ve since reused that pattern in three other scripts.
The throwaway tool tier
About a third of the archive is scripts under 50 lines that exist to read one thing out of the sim and shout it on local chat where a Python listener can pick it up:
- Touch reporter (30 lines) — drop into any linkset, click any linked prim, get
link N · prim "Name" · face Fin chat. Indispensable for figuring out face indices on mesh. - Position dump (32 lines) — broadcasts the object’s world position on touch, on rez, and on script reset. Used to capture furniture layouts for later replay.
- Color clicker (8 lines) — every touch randomizes prim colour. Useful for one question only: “is this prim’s touch event actually firing?”
- Text clearer (13 lines) — clears hover text and removes itself from inventory. The only way to nuke leftover floating text from a prim whose original script has been deleted.
Each of these ends with llRemoveInventory(llGetScriptName()) — the script does its one thing, then deletes itself from the prim’s contents. No cleanup, no leftover script slot eating memory. When the bar to writing and delivering a new script is three seconds via UploadScript, tiny tools get written instead of half-remembered. That’s the workflow the pipeline enables.
Patterns that show up everywhere
Reading the archive end to end, three patterns recur in almost every script:
The persistent storage trick: config goes in the prim’s Description field. LSL has no persistent key-value store. Notecards are asynchronous (you have to wait for a dataserver event to read them). In-script constants need source edits and a re-upload. The Description field — readable with llGetObjectDesc(), synchronous, free, and survives inventory drops — is the answer. Pipe-separated key-value pairs: project=my-venue|step_delay=1.5. Edit the Description in Build → General, reset the script, new config is live. Every reconfigurable script in the archive uses this pattern.
Self-register over HTTP on every URL grant. Every long-lived listener script POSTs its current URL and identity to the dashboard immediately after URL_REQUEST_GRANTED. The dashboard maintains the name→URL table; no human ever pastes a URL anywhere.
Version-suffix the item name. Bot HUD v3.17, waypoint-node v2.1. Bump the suffix on every shipped behaviour change. SL inventory has no other versioning primitive that survives drops between avatars.
BMAD and the adversarial code review
Writing LSL through Claude Code is fast. Getting it right is where the BMAD method earns its keep.
BMAD is a multi-agent AI development framework — instead of one AI doing everything, different agents play different roles: analyst, architect, developer, QA. For LSL work, the role I reach for most is the adversarial code reviewer. Once a script is written and delivered, I’ll hand the source to the review agent with a specific brief: assume this is going into production in an active sim, find everything that can go wrong. The agent comes back with things like “this llListen handle is never released on state change — you’ll leak handles until the 65-handle limit kills the script” or “you’re reading the Description field in state_entry before the prim is fully initialized — this will return an empty string on first rez.”
These aren’t edge cases. They’re the kind of thing that shows up silently in a live sim three days after you deployed the script, when you can’t easily reproduce it. The adversarial reviewer catches them before they ship.
The workflow for a non-trivial script looks like this:
- Claude Code writes the first draft using the LSL knowledge base
- The draft gets delivered to my avatar via
UploadScript - I test the happy path in-world
- The source goes to the BMAD adversarial reviewer with the brief: “find race conditions, event queue issues, memory leaks, and anything that breaks on region restart”
- Fixes get written, delivered, tested again
For the waypoint prim — the most complex script in the archive — the adversarial review caught a race condition in the broadcast-and-register flow where multiple prims firing simultaneously could cause the dashboard to receive duplicate registrations with slightly different coordinates. The fix was a jitter delay calculated from the prim’s link number. That would have been a maddening bug to debug in a live sim with 200 prims already placed.
I’ve written more about the BMAD method and its elicitation techniques in Why I Love the BMad Method and Deep Dive: All 50 BMad Advanced Elicitation Techniques. The adversarial reviewer is one agent in a larger system — but for LSL specifically, it’s the one that pays for itself fastest.
What I’d do differently
One named deliverer bot, always. I anointed a single bot as the default deliverer because it’s the most reliable. Any time new code asks “which bot?”, the answer is the deliverer. That decision lives in the config, not in the calling code, and it means the code doesn’t inherit all the edge cases of “pick the least-busy avatar” or “pick one that isn’t mid-animation.”
The relay is not bureaucracy. The first version of this had the signing key in the dashboard. After about ten minutes I moved it. The relay pattern adds one container and one HTTP hop. What it buys is a clean threat model: the dashboard’s threat model explicitly assumes it will be compromised someday. The signing key being on a different container with no public route limits the blast radius to “attacker can talk to the relay, which signs the commands the relay accepts, which is a narrow set.” That’s a much smaller problem than “attacker has the fleet.”
What’s open
Two threads I haven’t closed:
SLua. Linden Lab is rolling out Lua as an alternative scripting language. The knowledge base has notes on it; the reference parts don’t cover it yet. Any new LSL I’m writing now is at least SLua-aware.
Notecards. CreateNotecard isn’t supported on the current bot library builds. Non-script payloads — config blobs, narrative text — get delivered as Script items with a comment-only body. A patched build would fix it, but the workaround is one line of LSL and it’s not a priority.
For the daily case — “write me a HUD that pings the dashboard every 10 seconds and renders the bot roster” — the pipeline turns ten minutes of viewer-fiddling into one button press. That’s the thing that made it worth building.
Stack: containerised SL bot fleet behind a signing relay · FastAPI dashboard · Claude Code as the agent runtime.
Related reading
- I Gave Second Life Bots a Brain, a Voice, and a Soul — the full architecture: the bot fleet, Hermes, and the security model
- Teaching Bots to Walk: Waypoints, A* Pathfinding, and Spatial Intelligence in Second Life — the project this pipeline was built for: a waypoint graph, A* pathfinding, and a 3D digital twin of a Second Life sim, with the prim scripts written and delivered via this exact stack
- Notes from Virtlantis: AI Bots, Voice, and the Teacher as Puppet Master — four AI avatars, one dashboard, one teacher
- Agent Vibes: Finally, Your AI Agents Can Talk Back — the open-source TTS layer that powers voice across the bot fleet
Did this genuinely help you?
I also wrote about this on LinkedIn — Orchestrating AI in Second Life. If this post gave you something useful, a like or share goes a long way — it's how this work reaches people who'd actually use it.
Like & share on LinkedIn →Building AI bots for Second Life?
I build intelligent speaking avatars with real in-world behavior — navigation, scripting, voice, custom personalities. If you're running a sim, an event, or an educational space and want AI that actually feels present, get in touch.
Comments
Loading comments…
Leave a comment