Documentation menu

Commands

embr run

Last updated October 22, 2026

embr run [name] [args...]

With no name it lists the project's scripts. With one, it runs the [scripts] entry named <name> through the platform shell (sh -c on unix, cmd /C on Windows), forwarding the script's exit code.

Scripts#

Scripts are plain shell strings in the manifest:

[scripts]
build = "rojo build -o game.rbxl"
fmt = "stylua src"

Pinned tools are on PATH inside scripts (via the shims), at the project's pinned versions — so rojo above runs exactly the version in [tools].

A script can also be a list#

Give a script a list of commands and they run at once — what the concurrently npm package is for, built in:

[scripts]
serve = ["larvae process -w", "rojo serve .larvae/build.project.json"]

Every line of output is tagged with the command's position in the list, so [1] is larvae and [2] is rojo, and a blank line separates each switch between them:

> acme/rocket@0.1.0 serve
> [1] larvae process -w
> [2] rojo serve .larvae/build.project.json

[1] watching 42 files

[2] Rojo server listening on port 34872

[1] rebuilt in 12ms
  • Colour is preserved. On macOS/Linux each command gets a real pseudo-terminal, so tools behave exactly as they do when run alone — even the ones that decide by asking isatty and ignore every colour environment variable, which most do. Output is then passed through byte for byte. Ember also sets FORCE_COLOR=1/CLICOLOR_FORCE=1 for tools that read those instead.
  • On a pty, stdout and stderr arrive merged, exactly as they would on your screen — a terminal has one stream. So an interactive embr serve cannot redirect one without the other.
  • Redirecting turns all of that off. When Ember's own output is not a terminal (embr serve > log, CI), commands get plain pipes instead: tools drop colour on their own, the log stays clean text, and stdout/stderr stay separate. NO_COLOR forces this path too.
  • Windows always uses pipes (no ConPTY support yet), so colour there depends on the tool honouring FORCE_COLOR/CLICOLOR_FORCE — the clap/anstream-based ones do. stdout and stderr stay separate.
  • Every command runs to its own end. One failing does not stop the others (concurrently's default); it prints [2] exited with code 4 where it happened, and embr exits with the first non-zero code. Ctrl+C stops everything, as always.
  • The two forms are not the same execution model. A single command inherits Ember's stdio, so it owns the terminal outright. A list gets a pty per command, which reproduces almost everything a terminal gives — but the commands share your keyboard, so anything wanting interactive input (a prompt, a REPL, a debugger) belongs in the string form.
  • serve = [] is an error, not a script that does nothing.
  • Extra arguments are appended to every command in a list.

embr run with no name lists them#

npm's behavior, under three headings rather than as one flat list. Each heading carries a dimmed hint saying how to run what's under it:

Scripts in acme/rocket@0.1.0

Scripts embr run <name>
  lint
    selene src

Lifecycle Scripts embr <name>
  build
    rojo build -o game.rbxl
  test
    lune run tests

Hooks run by embr
  postinstall
    embr build
  prebuild
    rm -rf build

Names are alphabetical within each group, and a list script shows every command under the [n] tag its output will carry.

  • Scripts — ordinary entries, reachable only as embr run <name>.
  • Lifecycle Scripts — the six whose names are subcommands too, so run is optional: build, test, start, serve, dev, fmt/format. A name that starts with one of them and a colon, such as build:prod, goes here too.
  • Hooks — the pre/post entries Ember fires by itself. See Lifecycle hooks.

Empty groups are omitted, so a project with only ordinary scripts gets one list and no taxonomy it didn't ask for.

A name is only sorted into Hooks when something actually answers to the part after pre/post — either a command event (install, add, publish) or another entry in the same table. So prelude is an ordinary script, and so is a prebuild with no build: it can never fire as a hook, and embr run prebuild is the only way it runs.

Extra arguments#

Anything after the script name is appended to the script's command line, quoted for the platform shell so each argument arrives as one word whatever whitespace or shell syntax it contains. Arguments that need no quoting don't get any. An npm style -- separator is accepted and dropped, so these two are identical:

embr run build -- --watch "my game.rbxl"
embr build --watch "my game.rbxl"       # → rojo build -o game.rbxl --watch 'my game.rbxl'

The six script subcommands#

build, test, start, serve, dev and fmt are subcommands too, so run is optional for those six:

embr build          # same as embr run build
embr test
embr start
embr serve
embr dev
embr fmt

A name that starts with one of the six and a colon also drops run:

embr build:prod     # same as embr run build:prod
embr dev:gameplay   # same as embr run dev:gameplay
embr fmt:check

The colon must follow a whole shortcut, and a name must follow the colon. embr lint:fix and embr build: still need run.

embr format is an alias of embr fmt, and either subcommand runs whichever of the fmt / format scripts your manifest defines (fmt wins if both exist) — both spellings are common, so neither has to be memorized.

This mirrors npm, which promotes test, start, stop and restart the same way, and only those.

Every other script needs embr run. embr lint is not a command even when lint is in [scripts], and it says so rather than leaving you guessing:

✗ Unrecognized subcommand 'lint'
'lint' is a script in ember.toml; run it with `embr run lint` (only build, test, start, serve, dev and fmt, and a name that starts with one of them and a colon, can drop `run`)

The six are real subcommands, not a fallback for unrecognized input. A [scripts] entry can never shadow or replace an Ember command, and embr build with no build script is a plain No script named 'build' under [scripts] in ember.toml.

What Ember prints before a script#

Every script Ember runs — one you named, a shortcut, or a hook — is announced first, in dimmed text, in npm's two-line form: which package's script it is, then the command line it expands to (one line per command for a list).

> acme/rocket@0.1.0 prebuild
> rm -rf build

> acme/rocket@0.1.0 build
> rojo build -o game.rbxl --watch

The package is named because in a workspace the same script name means different things in different directories, and because a hook's output would otherwise have no attributable source.

Hooks#

A [scripts] entry named pre<something> or post<something> is a hook: prebuild runs before build, and postinstall runs after embr install. Nothing needs enabling. See Lifecycle hooks.