Official language handbook

Write scripts that think before they type.

Learn Roterm Script from its execution model upward: capture terminal output, turn raw text into structured decisions, plan weighted command costs, and build dependable .rot automation.

240 pointsoperation budget
20,000evaluation steps
24 KBsource per script

What Roterm Script is

Roterm Script 2.3 is a small, server-side interpreted language. Your source is saved as a .rot file, tokenized once, parsed into a syntax tree, and walked by a sandboxed evaluator. It is never handed to loadstring/require and can never reach a Roblox service — a program can only call the builtins documented here and reach the terminal through command() and run.

It reads like a blend of Lua and Python: let declarations, if / elseif / else / end, while, for, fn functions, and short-circuiting and/or that return the operand.

The one thing to understand. A script is just you, typing faster. Every meaningful action a program takes flows through command("...") — the exact same commands you type at the prompt. The language around it (variables, loops, functions, string tools) exists to decide which commands to run and to read what they print back. Learn the terminal first; scripting is how you put it on rails.

Where the power is: command(text) runs a terminal command and hands you its output as a string. lines(), contains(), split() and friends turn that string into decisions. That loop — run a command, read the output, decide the next command — is the whole game.

Getting started

Here is the smallest useful program and everything it teaches. Open CodeEditor, paste it, save it as first.rot.

# first.rot — my first Roterm program
let target = arg(1) or "127.0.0.1"   # 1st argument, or a default
print "Scanning " + target

let scan = command("nmap -sV " + target)  # run a real command, capture its text

if contains(scan, "22/tcp open") then
    print "SSH is open on " + target
else
    print "No SSH here."
end

Now compile and run it in the terminal:

build first.rot        # produces the command /usr/bin/first
first 10.0.0.5         # run it; 10.0.0.5 becomes arg(1)
first                  # run with no argument -> target becomes 127.0.0.1

That eight-line program already uses the four pillars you will use in every script:

  • Arguments — arg(1) reads what the user typed after the command; or supplies a fallback.
  • Running commands — command("nmap -sV " + target) executes a terminal command and returns everything it printed.
  • Reading output — contains(scan, "22/tcp open") inspects that text to make a decision.
  • Branching — if / else / end acts on what you found.
Edit → rebuild. The source and the compiled command are separate. After you change first.rot, run build first.rot again or you will keep running the old version.

The runtime mental model

A dependable script separates terminal work from language work. Terminal commands observe or change the simulated world. Roterm Script validates inputs, captures their text, parses it, chooses the next action, and records a result.

Inputargs + files
Observecommand()
Decideparse + branch
Actcommand() + report

Four boundaries to keep straight

BoundaryReads fromUse it for
arg() / args()The words supplied when the compiled command was launchedTargets, modes, flags, and user choices
command()The terminal engineObserving or changing the simulated machine and network
cat() / ls()The current simulated host filesystemReading files that exist on the host you currently control
read_file() / write_file()Private per-runner session storageScratch data and durable script notes; not the target host filesystem

command() versus run

Use command(text) when your program needs to inspect the result. It runs silently and returns one string. Use the run "..." statement when the human should see the terminal output immediately and the script does not need to parse it.

let status = command("systemctl status sshd") # capture, do not echo
if contains(status, "active (running)") then
    print "service ready"
end

run "uptime" # execute and echo the result
No hidden world API. Roterm Script only knows what you pass in, what it reads from an allowed file, and what a terminal command prints. In particular, target() is a compatibility alias for the first program argument; it does not scan or auto-select a host.

Build & run

Write your source in the CodeEditor and save it as name.rot. Compile it into a runnable command with:

build name.rot     # produces /usr/bin/name
name 10.0.0.5      # run it; everything after the name is an argument

Inside the program, read those arguments with arg(1), args() (a list of them all) and argc() (how many there are). Arguments are always strings — use num() if you need one as a number.

A program always runs with the privileges of the shell that launched it. Run it from your own workstation and it acts as you; run it from a foothold shell on a compromised host and it acts as that account — it can read only what that account can read, exactly like typing commands there by hand.

To run a program……do this
From your workstationjust type name <ip> at your own prompt
On a foothold you holdthe program's command() calls run in your current shell context; get the shell first (exploit/shell <id>), then run it
Via the guided consolemsfconsole → search → use <#> → set rhost → run compiles & fires an owned module for you

Syntax basics

One statement per line; there are no semicolons. # begins a comment that runs to the end of the line. Declare a variable with let and reassign it with plain assignment:

# a comment
let name = "scanner"     # declare
name = name + "-v2"      # reassign, no 'let'

Line continuation

A line that ends on an operator, comma, colon, or an open bracket continues onto the next line, so long expressions wrap cleanly:

let total = first_value +
            second_value +
            third_value

let box = {
    host: "nas",
    port: 22,
}

then, else, and do deliberately do not continue a line — the newline after them starts the block body. This is why if x then and the statement under it are on separate lines.

Strings

Strings use double quotes. Inside them, \n is a newline, \t a tab, \\ a backslash and \" a literal quote. Join strings with +.

print "line one\nline two"
print "path: " + "/home/" + user()

Types & values

There are five value kinds:

TypeLiteralNotes
number42, 3.14Whole or decimal. Join to text only via str().
string"hello"Join with +. Double-quoted, with \n \t \\ \" escapes.
booleantrue / falseProduced by comparisons and and/or/not.
nilnilAbsent / empty value. Returned by cat/ls when something is unreadable.
list[1, 2, 3]Ordered. 1-based: list[1] is the first item; list[-1] is the last.
map{host: "nas", port: 22}Key/value. Access with m.host or m["host"]. Keeps insertion order.
let ports = [22, 80, 443]
let box = {name: "nas", open: true}
print ports[1]        # 22   (first item)
print ports[-1]       # 443  (last item)
print box.name        # nas
print box["open"]     # true

Truthiness

Only false and nil are "falsy". Everything else is truthy — including 0 and the empty string "". So test for empty explicitly:

if victim == nil or victim == "" then   # the right way to check "no target"
    print "nothing to do"
end

Nesting

Lists and maps nest freely — a list of maps is the natural shape for a report:

let findings = [
    {ip: "10.0.0.5", svc: "ssh"},
    {ip: "10.0.0.9", svc: "smtp"},
]
print findings[1].ip     # 10.0.0.5

Operators

+   -   *   /   %
==   !=   <   <=   >   >=
and   or   not
??   +=   -=   *=   /=   %=

Arithmetic and comparison behave as you expect. Comparisons return real booleans. Precedence follows maths: not binds tightest, then * / %, then + -, then the comparisons, then and, then or, then ??. Use parentheses when in doubt.

Short-circuit and / or

and/or short-circuit and return the operand itself rather than a coerced boolean, which makes two idioms very common:

# default value: use arg(1), or fall back if it is nil/empty-ish
let targetIp = arg(1) or "127.0.0.1"

# guarded read: only cat when a path was found
let text = path and cat(path)

Nil-only defaults and concise updates

a ?? b uses b only when a is nil, so valid false values survive. Compound assignment works on declared variables, list slots, and map fields.

let enabled = get(config, "enabled") ?? true
attempts += 1
let counts = {ssh: 0}
counts.ssh += 1

Control flow

Conditionals

if open and not patched then
    print "vulnerable"
elseif open then
    print "open but patched"
else
    print "closed"
end

Loops

Four loop forms. Every loop is bounded by the sandbox (see limits).

# condition loop
while attempt <= 3
    attempt = attempt + 1
end

# numeric range; 'step' is optional (default 1), and may be negative
for i = 1 to 10 step 2
    print i               # 1 3 5 7 9
end

# one name: list values, string characters, or map keys
for host in targets
    print host
end

# two names: index/value for lists, key/value for maps
for key, value in report
    print format("{} = {}", key, value)
end

# run a block a fixed number of times
repeat 3
    print "knock"
end

break exits the innermost loop; continue skips to the next iteration.

for row in lines(scan)
    if trim(row) == "" then
        continue            # skip blank lines
    end
    if contains(row, "filtered") then
        break               # stop at the first filtered port
    end
    print row
end

Use the two-name form when you need both a map's key and value:

for key, value in box
    print format("{} = {}", key, value)
end

Functions

Define reusable logic with fn. Functions are first-class values, may recurse, and return nil if they fall off the end. A bare return returns nil immediately.

fn service_is_open(scanText, port)
    return contains(scanText, str(port) + "/tcp open")
end

if service_is_open(scan, 22) then
    print "SSH is up"
end

Return a map or list to hand structured results back to the caller — the cleanest way to keep a program readable:

fn creds_from(text)
    let cfg = parse_config(text)
    return {
        account: get(cfg, "account", nil),
        password: get(cfg, "password", nil),
    }
end

let c = creds_from(cat("/root/Bank.txt") or "")
if c.account != nil then
    print "found account " + c.account
end
A function can call the builtins (command, cat, print, …) and read the program's globals, but its parameters and any let inside it are local to that call. Pass what you need in as arguments and hand results back with return.

Scope, nil & errors

Handling nil

Filesystem reads are the main source of nil: cat(path) returns nil if the file is missing or your account can't read it, and ls(dir) returns nil if the path isn't a readable directory. That is normal — branch on it rather than assuming success:

let text = cat("/etc/shadow")
if text == nil then
    print "can't read it from this account — escalate first"
else
    print text
end

Concatenating nil is an error, so guard before you join. A tidy trick is cat(path) or "", which turns an unreadable file into an empty string you can safely pass around.

What raises a runtime error

Most builtins simply return nil/false on a "normal" failure. A few things genuinely stop the program with an error message (shown in Output): adding a string to a number, indexing a non-list/non-map, calling something that isn't a function, dividing by zero, or blowing a sandbox limit. Errors are reported with the line number so you can jump straight to the fault.

There is no try/catch. Program defensively: check for nil, convert types with str()/num(), and confirm a command "worked" by searching its output text (e.g. contains(result, "shell opened as")) before relying on it.

Searchable API index

Use the handbook filter in the left rail to narrow both this index and the navigation. Every item below links to its full behavior, return values, and examples.

Terminal & world builtins

These connect your program to the terminal and tell it where it is running.

SignatureDescription
print valueEmit one value as a program output line and return that value. Parentheses are optional.
warn(value)Emit a warning-coloured output line and return the value.
assert(condition [, message])Stop with a line-numbered error when the condition is false or nil.
command(text)Run a terminal command and return its full text output as a string. The workhorse.
run "command"A language statement that runs one command and prints its output. It is not a normal function call.
arg(n)The n-th command-line argument as a string (nil if absent). n is 1–32.
args()All command-line arguments as a list.
argc()Number of command-line arguments.
target()Compatibility helper: returns arg(1) when present, otherwise nil. It does not discover or validate a host.
budget()Weighted operation points still available in this run.
command_cost(text)Preview how many operation points a terminal command would consume without running it.
user() / host()Username / hostname of the machine the current shell is on.
cwd() / ip()Current directory / IP of the machine the current shell is on.

command vs run

let out = command("whoami")   # capture "root" into a variable, decide on it
run "reverse-shell 4444"       # just do it and echo the result

Filesystem builtins

These act on whichever machine your current shell is on, with that shell's privileges — the ordinary post-exploitation move of landing somewhere and reading what the account can reach. They never touch your own workstation (use session files for that).

SignatureDescription
cat(path)Read a file's contents. Returns nil if missing or unreadable.
ls(path)List a directory as a list of names. Returns nil if not a readable directory. ls() lists the current directory.
is_dir(path)Whether a path is a readable directory (true/false).
exists(path)Whether a path exists and is reachable. Use it with cat to tell "not there" apart from "no permission".
parse_config(text)Parse key=value / key: value text into a map — perfect for config files, Bank.txt, and command output.
for name in (ls("/home/" + user()) or [])
    let body = cat("/home/" + user() + "/" + name)
    if body != nil and contains(lower(name), "bank") then
        print name + ":\n" + body
    end
end

Session files

Private scratch space on your own workstation, separate from the machine your shell is on. Ideal for remembering which targets a script has already touched, or building a running log across a sweep.

SignatureDescription
read_file(name)Read a session file (returns "" if it doesn't exist yet).
write_file(name, text)Overwrite a session file with text.
append_file(name, text)Append text to a session file (creates it if needed).
file_exists(name)Whether a session file exists (true/false).
remember(name, value)Append the complete value as a line only if that exact line is not already present.
# remember explicit targets without duplicates
for victim in args()
    let history = lines(read_file("touched.txt"))
    if not contains(history, victim) then
        remember("touched.txt", victim)
        print "queued " + victim
    end
end
Session files roll over instead of erroring: once a file passes its size limit the oldest lines are trimmed automatically, so a long-running logger never crashes.

String builtins

Turning command output into decisions is 90% string work. All position arguments are 1-based; searches are literal (not patterns) and case-sensitive.

SignatureDescription
len(x)Length of a string, list, or map.
upper(s) / lower(s)Change case — lower() before a comparison to make it case-insensitive.
trim(s)Strip leading/trailing whitespace.
contains(hay, needle)Whether a string contains a substring — or a list contains a value.
starts_with(s, p) / ends_with(s, p)Test the start / end of a string.
find(s, needle [, start])1-based position of needle in s, or 0 if not found. Optional start offset.
replace(s, from, to)Replace every literal occurrence of from with to.
substr(s, start [, last])Take part of a string by 1-based positions (negative counts from the end).
split(s, sep)Split into a list on sep. Empty sep splits into single characters.
join(list [, sep])Join a list into a string with sep (default: no separator).
format(template, ...)Replace each {} in order with the next displayed value.
lines(text)Split command output into a list of lines — the natural way to walk output.

Extracting a field

# pull the ip out of an arp -a row: "router (10.0.0.1) at .. on eth0"
let inside = split(split(row, "(")[2], ")")[1]
let device_ip = trim(inside)      # "10.0.0.1"

List & map builtins

SignatureDescription
list()Create an empty list (same as []).
range([first,] last [, step])Create an inclusive numeric range, ascending or descending, up to the collection limit.
sum(list) / count(list_or_text, value)Add a numeric list / count non-overlapping occurrences.
push(l, v) / pop(l)Append a value / remove-and-return the last value.
insert(l, i, v) / remove(l, i)Insert / remove at a 1-based position.
slice(l, first [, last])A sub-list by 1-based positions (negative counts from the end).
sort(l) / reverse(l)Order a list in place. sort is numeric if every item is a number, otherwise alphabetical.
get(l_or_m, key [, default])Read a list index or map key; return default (or nil) if absent — the safe way to read.
map()Create an empty map (same as {}).
set(m, k, v) / delete(m, k)Write / remove a key.
has(m, k)Whether a map has a key (true/false).
keys(m) / values(m)The keys / values as a list (insertion order).
let report = []
push(report, {ip: victim, status: "owned"})

let counts = {}
set(counts, "ssh", get(counts, "ssh", 0) + 1)   # tally with a default

Convert & math builtins

SignatureDescription
str(x) / num(x)Convert to text / to a number (arguments arrive as strings, so num() them for maths).
type(x)Name the type of a value: "number", "string", "boolean", "nil", "list", "map".
random(lo, hi)Random whole number from lo to hi inclusive.
min(a, b) / max(a, b)Smaller / larger of two numbers.
abs(x) / floor(x) / ceil(x) / round(x)Numeric helpers (round is to the nearest whole number).
clamp(x, lo, hi)Constrain a number to a range.

Commands you can drive

Since command()/run reach any terminal command, the "standard library" of a real script is the terminal itself. These are the commands you will script most; each returns text you can inspect. (Type help or man <command> in the terminal for the full set.)

Recon

CommandWhat it does
nmap -sV <ip>Service/version scan — find the software and version a host runs.
scan <ip>Quick reachability/role scan of a host.
arp -aFrom a foothold, list every neighbour on the LAN: host (ip) at mac [ether] on iface.
searchsploit <software>Search your advisory DB for matching exploits and whether you own each.
nc -v <ip> <port>Banner-grab a single service (netcat).
whois <ip> · dig <host>Resolve ownership / DNS of an address you scraped from a log.

Attack & escalate

CommandWhat it does
<program> <ip>Run a built payload (bought in a hackshop, compiled with build) at a target.
exploit <sig> <ip>The low-level exploit call your payload wraps; returns "shell opened as …" on success.
privescRun an owned local kernel-LPE payload on your foothold to become root.
sudo -l / sudo -iEnumerate then cash in a NOPASSWD sudo misconfiguration.
crack <hashfile> · su root <pw>Recover a password from a readable hash file, then become root with it.

Foothold, pivot & loot

CommandWhat it does
server start <port>Open a listener so a shell can call back.
reverse-shell <port>Background your current shell through the listener (persistent access).
shell <id> · shells · exitSwitch to a saved shell, list them, or drop the current one.
pivot / deployRoute through a compromised gateway to reach hosts on its LAN.
loot · exfil <path>List, then claim, valuable data files for Credits.
bank login/balance/transferUse credentials you harvested to drain an account.

Defend

CommandWhat it does
secure statusShow the posture of your own devices and any host you've rooted.
secure allHarden your whole GreyNet (patch + firewall + rotate).
secure <ip|here> patch|firewall|rotateHarden one device — or a box you've rooted — so others can't follow you in. You keep your shell; they're shut out.
Firewalls & pivoting: some hosts filter sensitive ports at the border — they read as filtered in nmap and refuse a payload from outside. Web and DNS are never filtered, so there's always a way in: compromise the gateway, then pivot onto the LAN and the filtered services become reachable from inside.

Plan the operation budget

Every run begins with 240 weighted operation points. Cheap inspection commands cost very little; disruptive or high-value actions cost more. This is intentionally not a simple command counter. Ask the runtime instead of maintaining a guessed integer.

Typical costExamplesDesign implication
1–3whoami, ls, cat, systemctl statusGood for validation and lightweight observation.
4–8ping, curl, scan, sshBatch deliberately; avoid repeating unchanged checks.
10–14nmap, ipgen, aircrack-ngCache their output and parse it once.
16–20exfil, exploit, secure, grantReserve points before entering the action phase.

Subcommands can have their own price—systemctl status and systemctl restart are not equivalent—so command_cost() is the authoritative answer.

fn can_afford(text, reserve)
    return budget() >= command_cost(text) + reserve
end

let scanCmd = "nmap -sV " + victim
let actionCmd = "exploit A7-22-5C-09 " + victim

if not can_afford(scanCmd, command_cost(actionCmd)) then
    print "stopping safely with " + str(budget()) + " points"
    return
end

let scan = command(scanCmd)
Budget is charged when a terminal command is attempted, not only when it succeeds. Validate arguments and filesystem state before spending points on a command that cannot work.

Parse terminal output reliably

Terminal text is an interface. Normalize it once, match the smallest stable phrase, and keep extraction logic in a function. That makes a script easier to repair when output gains an extra heading or spacing changes.

fn open_services(scanText)
    let result = []
    for raw in lines(scanText)
        let row = trim(lower(raw))
        if contains(row, "/tcp open") then
            push(result, raw) # retain original text for the report
        end
    end
    return result
end
AaNormalize for comparisonUse trim(lower(row)), but keep the original row if it will be printed later.
≠Separate absence from failureA missing phrase is not automatically an error. Look for known failure text before assigning a state.
[]Check before indexingsplit() always returns a list, but the piece you expect may not exist.
#Prefer structured filesWhen text is key=value or key: value, use parse_config() and get().

A safe extraction pattern

fn value_after(text, marker)
    for row in lines(text)
        if starts_with(trim(row), marker) then
            let parts = split(row, marker)
            if len(parts) >= 2 then
                return trim(parts[2])
            end
        end
    end
    return nil
end

Wireless automation, end to end

This pattern automates the Roterm simulation workflow for a BSSID you explicitly pass in. It validates the channel, enables monitor mode, reruns the capture until a .cap exists, decodes it, and restores managed mode even when recovery fails.

# usage: wifi-capture AA:BB:CC:DD:EE:FF 6
let bssid = arg(1)
let channel = num(arg(2) or "0")
let capture = "/tmp/wifi-" + replace(bssid or "", ":", "") + ".cap"

if bssid == nil or channel < 1 or channel > 11 then
    print "usage: wifi-capture <BSSID> <channel 1-11>"
    return
end

let start = command("airmon-ng start wlan0")
if contains(lower(start), "not found") then
    print start
    return
end

let captureCmd = "airodump-ng --bssid " + bssid + " --channel " + str(channel) + " --write " + capture + " wlan0mon"
let collecting = true
let passes = 0

while collecting and passes < 12
    if budget() < command_cost(captureCmd) + command_cost("aircrack-ng -w /usr/share/wordlists/roterm.txt " + capture) + 4 then
        print "not enough budget to finish and restore the interface"
        collecting = false
    else
        let out = command(captureCmd)
        passes = passes + 1
        if contains(out, "saved capture:") then
            collecting = false
            let cracked = command("aircrack-ng -w /usr/share/wordlists/roterm.txt " + capture)
            print cracked
        elseif contains(lower(out), "error") or contains(lower(out), "not in scan cache") then
            print out
            collecting = false
        end
    end
end

run "airmon-ng stop wlan0mon"
A BSSID must already be in the wireless scan cache. Run iw dev wlan0 scan first, then pass the displayed BSSID and channel to the compiled program. The capture is progressive: seeing “buffer is incomplete” means rerun the same capture command, not switch targets.

Patterns & recipes

Small, reusable building blocks. Copy them straight into your programs.

Walk a command's output line by line

let scan = command("nmap -sV " + ip)
for row in lines(scan)
    if contains(row, "open") then
        print trim(row)
    end
end

Is a service open?

fn open_port(scanText, port)
    return contains(scanText, str(port) + "/tcp open")
end

Confirm a step "worked" by reading its output

There are no exceptions — success is a string you look for. Never assume; always check.

let result = command("exploit A7-22-5C-09 " + ip)
if contains(result, "shell opened as") then
    print "in"
elseif contains(result, "filtered") then
    print "blocked by a firewall — pivot first"
else
    print "failed: " + result
end

Read a file safely

let body = cat(path) or ""       # nil-proof
if body == "" then
    print "nothing readable at " + path
end

Parse credentials out of a config/Bank file

let cfg = parse_config(cat("/root/Bank.txt") or "")
let account = get(cfg, "account", get(cfg, "bank_account", nil))
let secret  = get(cfg, "password", get(cfg, "bank_password", nil))

Loop over explicit targets without overspending

Accept targets as launch arguments, skip previously handled values, and ask the runtime what each command costs.

let touched = lines(read_file("touched.txt"))

for victim in args()
    let scanCmd = "nmap -sV " + victim
    if contains(touched, victim) then
        print "skip remembered target " + victim
    elseif budget() < command_cost(scanCmd) + 20 then
        print "stopping: action reserve reached"
        break
    else
        let scan = command(scanCmd)
        remember("touched.txt", victim)
        push(touched, victim)
        # ... inspect scan and choose the next action ...
    end
end

Extract every LAN address from arp -a

fn lan_ips(arpText)
    let found = []
    for row in lines(arpText)
        if contains(row, "(") and contains(row, ")") then
            push(found, trim(split(split(row, "(")[2], ")")[1]))
        end
    end
    return found
end

Debugging playbook

Debug from the boundary inward. First prove the program received the values you expected; then prove the terminal command is correct; only then debug parsing and branches.

  1. Rebuild the source. The executable is a compiled snapshot. A successful save does not update it.
  2. Print the inputs. Check argc(), each argument, current host(), and cwd().
  3. Preview the exact command. Store it in a variable and print it before calling command(cmd).
  4. Inspect raw output once. Temporarily print the captured result before applying contains() or split().
  5. Normalize and narrow. Compare trim(lower(row)) to one stable phrase rather than a full formatted line.
  6. Print state, not floods. Report counters, selected paths, and budget(); avoid dumping the same scan in every loop.
print "args=" + str(args())
print "context=" + user() + "@" + host() + ":" + cwd()
print "budget.before=" + str(budget())
print "command=" + cmd
let raw = command(cmd)
print "raw=" + raw
print "budget.after=" + str(budget())

Build errors versus runtime errors

BBuild-timeMalformed syntax, an unexpected token, or an unclosed block. Start at the reported line and inspect the line immediately above it too.
RRun-timeBad value types, missing indices, division by zero, or a sandbox limit. Print the value and type(value) before the failing operation.
Binary-search a long script. Add an early return halfway down. If the problem disappears, it is below that point; otherwise it is above. Move the boundary until the failing block is small.

Common errors & how to fix them

Message / symptomCause & fix
cannot add a string and a numberYou joined text with a number. Wrap the number: "n=" + str(count).
attempt to concatenate nilA cat/get/arg returned nil. Guard it, or use … or "".
list index out of rangeRemember lists are 1-based; list[0] is invalid. First item is [1].
operation budget exceededThe weighted total of terminal actions passed 240 points. Check budget() and command_cost(text) before expensive work.
a script is already runningOnly one program runs at a time. Let the first finish before launching another.
output truncatedYou printed past 300 lines / 20,000 bytes. Print summaries, not raw dumps.
nothing happens after "then"then/else/do don't continue a line — put the block body on the next line.
my edits didn't take effectYou forgot to recompile. Run build name.rot after every source change.

Sandbox & limits

Every program runs server-side inside a bounded interpreter. It has no access to Roblox APIs and inherits the privileges of the shell that launched it. Hard limits stop runaway scripts:

LimitValueWhat to do about it
Saved source / runtime source12,000 / 24,000 bytesThe editor library enforces the smaller persistence limit; the interpreter keeps a separate hard ceiling for trusted generated programs.
Evaluation steps20,000Avoid needless nested loops over big lists.
Loop iterations (while / repeat)1,000Bound your loops with a counter or condition.
Weighted operation budget240 pointsUse budget() and command_cost(); costs depend on the command and sometimes its subcommand.
Nesting / call depth24Prefer loops to deep recursion.
Output lines / bytes300 / 20,000Print conclusions, not raw command dumps.
Single value size20,000 bytesParse large terminal output promptly and retain only useful fields.
Collection items512Slice or summarise very large lists.
A script cannot do anything you could not do by typing commands yourself. Reading /etc/shadow from an unprivileged foothold fails exactly as it does at the prompt. Terminal execution through command()/run spends weighted points; language operations and direct read-only builtins such as cat() and ls() do not.

Worked examples

1 · SSH sweep

Accept explicit targets, fingerprint them, respect the weighted budget, and summarize any successful simulated footholds.

# usage: ssh-sweep 10.0.0.5 10.0.0.8 10.0.0.12
let payload = "A7-22-5C-09"
let historyFile = "ssh-touched.txt"
let report = []

fn service_is_open(scanText, port)
    return contains(scanText, str(port) + "/tcp open")
end

if argc() == 0 then
    print "usage: ssh-sweep <ip> [<ip> ...]"
    return
end

let touched = lines(read_file(historyFile))
run "server start 4444"
run "apt install sshwire"

for victim in args()
    let scanCmd = "nmap -sV " + victim
    let exploitCmd = "exploit " + payload + " " + victim
    let reserve = command_cost(exploitCmd) + command_cost("reverse-shell 4444")

    if contains(touched, victim) then
        print "skip remembered target " + victim
    elseif budget() < command_cost(scanCmd) + reserve then
        print "budget reserve reached; stopping"
        break
    else
        print "scanning " + victim
        let scan = command(scanCmd)
        remember(historyFile, victim)
        push(touched, victim)

        if not service_is_open(scan, 22) then
            print "  no SSH service exposed"
        elseif not contains(scan, "OpenSSH") then
            print "  port 22 is not OpenSSH"
        else
            let result = command(exploitCmd)
            if contains(result, "shell opened as") then
                push(report, victim)
                run "reverse-shell 4444"
            else
                print "  no foothold: " + trim(result)
            end
        end
    end
end

print "Shells opened: " + str(len(report))

for host in report
    print "  compromised: " + host
end
print "Budget remaining: " + str(budget())

2 · Credential harvest

Walk a home directory, read whatever the current shell may read, and act on any file that looks like an account and a secret.

# Credential harvest (.rot)
fn credentials_in(path)
    let text = cat(path)
    if text == nil then
        return nil          # missing, or this account cannot read it
    end
    let cfg = parse_config(text)
    let account = get(cfg, "account", get(cfg, "bank_account", nil))
    let secret = get(cfg, "password", get(cfg, "bank_password", nil))
    if account == nil or secret == nil then
        return nil
    end
    return {path: path, account: account, password: secret}
end

fn walk(dir, results)
    let entries = ls(dir)
    if entries == nil then
        return results      # not a directory, or permission denied
    end
    for name in entries
        let path = dir + "/" + name
        if is_dir(path) then
            walk(path, results)
        else
            let hit = credentials_in(path)
            if hit != nil then
                push(results, hit)
            end
        end
    end
    return results
end

let home = "/home/" + user()
print "Searching " + home + " as " + user() + "@" + host()
let found = walk(home, [])

if len(found) == 0 then
    print "No credential files readable from this account."
else
    for hit in found
        print "  " + hit.path + "  account=" + hit.account
    end
    let creds = found[1]
    print "Authenticating with " + creds.account + " from " + creds.path
    print command("bank login " + creds.account + " " + creds.password)
    print command("bank balance")
end

3 · LAN sweep from a foothold

Once you hold a gateway shell, enumerate the whole LAN with arp -a and fingerprint each device — one run maps an entire network.

# LAN sweep (.rot) — run from a shell on a compromised gateway
fn lan_ips(arpText)
    let found = []
    for row in lines(arpText)
        if contains(row, "(") and contains(row, ")") then
            push(found, trim(split(split(row, "(")[2], ")")[1]))
        end
    end
    return found
end

print "Enumerating the LAN from " + host() + " (" + ip() + ")"
let devices = lan_ips(command("arp -a"))
print "Found " + str(len(devices)) + " host(s)"

for dev in devices
    let scanCmd = "nmap -sV " + dev
    if budget() >= command_cost(scanCmd) then
        print "--- " + dev + " ---"
        print command(scanCmd)
    else
        print "budget exhausted before " + dev
        break
    end
end

4 · Defensive audit

A tiny program that reports your network's security posture and hardens anything still exposed.

# Defensive audit (.rot) — run from your workstation
let status = command("secure status")
print status

if contains(status, "open:") or contains(status, "firewall off") then
    print "Exposed devices found — locking the whole GreyNet down."
    print command("secure all")
else
    print "All devices already hardened."
end

Cheat sheet

Syntax

let x = 1            # declare           x = 2              # reassign
if c then … elseif c then … else … end
while c … end       for i = a to b step s … end
for v in coll … end    repeat n … end    break / continue
fn name(a, b) … return v end
[1, 2, 3]  # list (1-based)   {k: v}  # map (m.k / m["k"])

The core loop

let out = command("nmap -sV " + ip)   # 1. run a command, get text
if contains(out, "22/tcp open") then   # 2. read it to decide
    run "exploit … " + ip              # 3. run the next command
end

Builtins at a glance

Output & world: print warn assert command run arg args argc target budget command_cost user host cwd ip
Files (current host): cat ls is_dir exists
Session (your box): read_file write_file append_file file_exists remember
Strings: len upper lower trim contains starts_with ends_with find replace substr split join lines format parse_config
Lists/maps: list range sum count push pop insert remove slice sort reverse map get set delete has keys values
Convert/math: str num type random min max abs floor ceil round clamp