---
slug: 70-cli
title: CLI
description: The Synsema command-line — run, test, check, serve, conform, repl, daemon — plus the flags and exit codes that make it script- and CI-friendly.
example_ids: []
---

# CLI

One static binary. The core commands:

```sh
synsema init [dir]             # scaffold: hello.syn (language tour + test), .env.example, .gitignore, .mcp.json (synsema-code)
synsema init [dir] --synfide   # + install the Synfide framework (durable workflows, approval
                               #   inbox, persistent kv), version-pinned from its latest release:
                               #   per-file sha256 verification, synfide/VERSION records the tag,
                               #   re-run to upgrade (framework files only — yours are never
                               #   overwritten, and a framework file YOU edited is kept too: the
                               #   new version lands beside it as <file>.new with a loud warning),
                               #   plus an app.syn starter and its test suite
synsema init [dir] --pwa       # an INSTALLABLE app (v0.6.15+): app.syn + index.html + public/{manifest.webmanifest,
                               #   sw.js, app.js, icon.svg} + the PNG icons generated from the SVG + push_keys.syn
                               #   (VAPID pair for native push) — see "Your app on the phone"
synsema run program.syn        # execute (exits when the program finishes); `-` reads the source from stdin
synsema run program.syn -- a b # everything after `--` is the program's own argv → args() == ["a", "b"]
synsema test program.syn       # run `test` blocks (a file or a directory)
synsema build program.syn -o app  # a single self-contained binary: the engine + your program baked in
synsema check program.syn      # no execution: parse + resolve all `use` imports + validate render("…") templates + (v0.6.19+) refuse what serve would refuse in an `export routes` group (stream/socket)
synsema code outline [path]    # code intelligence for agents (v0.6.13+): outline / symbol / refs / routes / caps /
                               #   check / search / deps — static, from the parser (--json); `synsema code --mcp` serves
                               #   the same tools as the `synsema-code` MCP server that `init` registers in .mcp.json
synsema openapi app.syn        # the /openapi.json the server would publish, from the source — no execution,
                               #   no port (--out openapi.json, --base-url https://api.example); exit 2 without `serve`
synsema serve program.syn      # stay alive for HTTP / crons / agents (--watch = restart on .syn changes)
synsema repl                   # interactive REPL
synsema conform --swarm app.syn  # post-run state dump (blackboard + agents) as JSON
synsema daemon start app.syn   # background daemon (see Deploy)
synsema llm status             # resolved LLM config + diagnosis (--json for scripting)
```

### Re-running `init` is safe — and it repairs

`init` never decides by mere existence. Each file it manages is classified by **provenance**:

| Its content | What happens |
|---|---|
| identical to the current version | `ya está al día` — untouched |
| identical to **any** version that shipped before | `actualizado (estaba sin ediciones tuyas)` — it was still factory, so it gets the new one |
| matches no released version | it's yours: kept, and the new version lands beside it as `<file>.new` |

Nobody types a file byte-identical to an old release by hand, so matching one is proof enough
that there's no work of yours to protect. This is what lets a project that has been skipping
upgrades catch up instead of staying frozen — and with `--synfide`, re-running also **repairs**
a scaffold with deleted or stale files, even when `synfide/VERSION` already names the latest
release (engine v0.5.9+; before that, existence alone counted as ownership).

### `init --pwa` — an installable app (engine v0.6.15+)

`synsema init [dir] --pwa` scaffolds a site that installs on Android, iOS and desktop: `app.syn`
(the server: `static "/" from "./public"`, the page and `mount api.api`), `api.syn` (v0.6.19+: the
API as an `export routes` group — `/api/ping` and the push routes), `index.html`,
`public/manifest.webmanifest`, `public/sw.js` (service worker), `public/app.js`, `public/icon.svg`,
and `push_keys.syn` (prints the VAPID pair for `.env`). The PNG icons (`icon-192.png`,
`icon-512.png`, `apple-touch-icon.png`) are **generated from `icon.svg`** with the engine's own
rasterizer — edit the SVG and re-run `init --pwa` to refresh them; PNGs you brought yourself are
left alone when `icon.svg` is the factory one, and never touched if you removed the SVG. No
`hello.syn` (the starter is `app.syn`); the base files (`.env.example`, `.gitignore`,
`.mcp.json`) come as usual, with the same provenance rules — re-running never overwrites yours.
`--pwa` and `--synfide` are different starters: passing both is a usage error (exit 2), as is any
unknown flag. The directory may go before or after the flag. Walkthrough: **[Your app on the phone
(PWA)](/en/0.6.x/41b-pwa)**. Since engine v0.6.19 the API lives in `api.syn` (an `export routes api`
group that `app.syn` mounts) so the same routes can serve a desktop entry; a factory `app.syn` from
an older `init --pwa` is refreshed to the modular one, an edited one is kept (`app.syn.new` beside it).

### `init --desktop` — the same app as a desktop app (engine v0.6.19+)

`synsema init [dir] --desktop` writes everything `--pwa` writes plus `desk.syn` — the desktop
entry: `bind "127.0.0.1"`, `mount api.api`, the page rendered with `desktop: true`, a `socket`
route that counts open windows, the browser opened as an app window with `run()` under `exec`
(`platform()` picks `cmd` / `open` / `xdg-open`), and `shutdown()` when the last window closes or
when no window opened in 30 s — and `public/desk.js`, the socket each window opens (loaded by
`index.html` only under `{ when desktop }`). `DESK_NO_WINDOW=1` skips the browser (tests, CI).
`synsema serve desk.syn` opens the window; `synsema build desk.syn -o desk --serve --no-console
--icon public/icon.svg [--bundle]` ships it. `--desktop` on an existing `--pwa` project just adds
the two files. Walkthrough: **[Your app on the desktop](/en/0.6.x/41c-desktop)**.

### What `.env.example` covers

The generated `.env.example` is commented section by section: the LLM provider pairs
(provider + its key), the host ceilings the program can't raise (`SYNSEMA_SPEND_CEILING`
and the per-identity `SYNSEMA_SPEND_CEILING_PER_IDENTITY`), the human-approval knobs, and
finally **your own secrets** — the ones your code names: `JWT_KEY` for `jwt_sign`, 
`CAPTOKEN_ROOT_KEY` for `captoken_mint` (attenuating needs no key, which is why a delegated
sub-agent never sees it) and `AGENT_SIGNING_KEY` for `http_sign` (which also needs
`require sign("AGENT_SIGNING_KEY")`). The name you choose *is* the capability scope, so add
your own the same way. See [Secrets](/en/0.6.x/21-secrets) and
[Agent identity](/en/0.6.x/46-agent-identity).

## `synsema code` — code intelligence for agents

An agent working on a Synsema repo should not read whole files to learn what is in them. `synsema code` answers from the parser — it never runs the program and never talks to a running server:

```sh
synsema code outline                # every .syn: intent, requires, imports, tasks/agents/types/routes/tests with line ranges
synsema code routes app.syn         # the HTTP table a `serve` would publish (auth, stream/socket/proxy, expect, capabilities)
synsema code refs send_report       # where a task/agent/type is used, with the enclosing symbol
synsema code caps                   # declared vs needed capabilities, and what is MISSING (with the `require` to add)
synsema code check                  # parse + imports + templates over the whole project (exit 1 on errors; warnings = missing caps)
synsema code search "proxy to"      # text search over .syn/.html/.js/.css/… with the enclosing symbol; --regex, --kinds, --limit
synsema code deps                   # task → task call graph + imports
```

Add `--json` for the exact structure. The same eight tools are served over MCP by `synsema code --mcp`, registered as **`synsema-code`** in the `.mcp.json` that `synsema init` writes — so any agent that opens the folder discovers them. It is development-time tooling about the source in that folder, **not** the MCP of your application (that is discovery: `/openapi.json`, `/llms.txt`). Full reference: the `code.md` page of the agent skill.

## `synsema build` — one program, one binary

`synsema build main.syn -o app` produces a **single self-contained executable**: it copies
the running engine and appends your program — the main `.syn`, the transitive `use` modules it
imports, the templates it `render`s by a literal name, and whatever you add with `--include` —
sealed with a trailer and a sha256. The result is a file, like `docker` is a file; no Python,
no npm, nothing to install on the target.

```sh
synsema build lamp.syn -o lamp                       # the binary for this platform
synsema build lamp.syn -o lamp --include assets/     # bundle a dir (recursive) …
synsema build lamp.syn -o lamp --include "data/*.csv"  # … or a one-level glob, repeatable
synsema build lamp.syn -o lamp --sandbox             # bake a host ceiling into the binary
synsema build lamp.syn -o lamp --cap-set "stdout,net=api.example.com"   # tailored baked ceiling
synsema build lamp.syn -o lamp --profile pure        # bake the pure profile (no filesystem/exec/db…)
synsema build lamp.syn -o lamp-linux --engine-binary ./synsema-linux-x86_64  # "cross": a donor engine
synsema build app.syn -o app --serve --bind 127.0.0.1                  # a SERVER binary (v0.6.16+): runs the
synsema build app.syn -o app --serve --bind 0.0.0.0 --port 8080 --secure   #   serve runtime with these deployment
synsema build app.syn -o app --serve --bind 0.0.0.0 --domain app.example.com --tls-auto ops@example.com   # flags baked in
synsema build desk.syn -o desk --serve --no-console --icon icon.svg     # a DESKTOP app (v0.6.18+): desk.exe with your icon and
synsema build desk.syn -o desk --serve --icon icon.svg --bundle          #   no console window; .app / dir + install.sh on macOS / Linux
```

Prints `built <out> (<n> files, <bytes>)`. The bundle is **closed**: a `use` whose path is a
runtime expression can't be resolved statically and is a build error, not a silent gap; an
`--include` that escapes the program's directory is rejected; assets larger than the engine are
fine (the binary is ~30–60 MB either way — nobody weighs the Docker CLI).

**A server in one binary (engine v0.6.16+).** A program with a `serve on` block only runs under
the serve runtime, so it must be built with `--serve` (without it the build stops with exit 2 and
says so, instead of the binary failing at run time). A bind is **required** — a distributable
must say where it listens (`127.0.0.1` for a local app, `0.0.0.0` for a public one): `--bind`, or
the serve block's `bind "…"` clause (v0.6.18+; the flag wins); `--port`,
`--domain`, `--tls-auto`, `--tls-cert`/`--tls-key` and `--secure` are the same deployment knobs
as `synsema serve`, baked in (TLS files are read from disk at start — the key never travels inside
the binary). The **static mounts of the serve block are bundled automatically** (`static "/x"
from "./public"` → `public/` goes in; a missing directory is a build error) and served **from the
bundle before the disk**, with a content ETag — the whole [installable app](/en/0.6.x/41b-pwa)
fits in the one file. Ctrl-C / SIGTERM is the ordered shutdown, exit 0. `--serve` with
`--profile pure` is refused (a server binds a socket).

**A desktop app in one binary (engine v0.6.18+).** The serve block's `bind "127.0.0.1"` clause is
baked when there is no `--bind` (without either, the build stops with exit 2). `-o desk` becomes
`desk.exe` when the engine being wrapped is a Windows executable; `--no-console` opens no console
window (stdout/stderr are discarded); `--icon <svg|png|ico>` puts your icon in the `.exe`, the `.app`
or the Linux launcher; `--bundle [--name "My App"] [--id com.example.app]` writes `My App.app/` (macOS)
or `<stem>/` + `install.sh` (Linux) — nothing to do on Windows, and the `built` line says so. All of
it is decided by the **engine's format** (PE / Mach-O / ELF), never by the building machine. The
recipe — the browser as an app window, a socket as the truth of "a window is open", `shutdown()` when
the last one closes — is in **[Your app on the desktop](/en/0.6.x/41c-desktop)**.

**The built binary runs your program.** Its whole `argv` is the program's (`args()`), the baked
ceiling and profile apply, and it reads its own bundle — a `read_file("assets/x")` on a bundled
file needs **no `file` capability** (it's part of the program, like a `use`), and a disk write to
a bundled path is refused. `render`ing a bundled template needs no capability either.

**Reaching the engine:** `app --engine <subcommand>` runs the plain engine CLI inside the built
binary — the only way in, since `--sandbox` and friends are now the program's argv. `app --engine
version` prints the engine version; `app --engine run other.syn` runs another program. (The stock
`synsema` binary also accepts `--engine` as a no-op prefix, so scripts are uniform.) `app --engine
update` is refused — a built program is rebuilt with `synsema build`, not updated in place. A
tampered bundle (`sha256 mismatch`) refuses to run.

Deploy it straight from `FROM scratch`/distroless — see **[Deploy](/en/0.6.x/71-deploy)**.

## `synsema llm status`

Prints the LLM configuration the runtime will **actually** use — each value with its source
(`environ` / `.env` / `default`), key **presence only** (never values, prefixes, or lengths),
which `.env` file was loaded, and a warning if several `synsema` binaries shadow each other in
`PATH`. When offline it names the exact missing variable — including the *"there's a key under
`DEEPSEEK_API_KEY`: did you store it under the wrong variable name?"* hint. Never touches the
network. Exit `0` = live, `1` = offline (scriptable: `synsema llm status && synsema serve app.syn`).
Full knob reference: **[Provider config](/en/0.6.x/52-llm-provider-native)**.

## Useful flags

| Flag | Command | Effect |
|---|---|---|
| `--flat` | `run` / `test` / `conform` | parse a `.fsyn` (flat document) file (also auto-detected by the `.fsyn` extension) |
| `--explain` | `run` | rich error report on stderr (source context, call stack, suggestions) |
| `--format json` | `run` | **without** `--explain`: the whole run as one JSON document on stdout — `{ok, output, errors, audit, exit, llm_tokens}` (the native form of the wasm `run()`; output is collected, not streamed). **With** `--explain`: structured error diagnostics |
| `--provider <name>` | `run` | force the LLM provider (`anthropic`/`openai`/`minimax`/`deepseek`) |
| `--sandbox` | `run` / `test` / `conform` / `serve` | host ceiling `stdout,time` — run untrusted code (see **[Capabilities](/en/0.6.x/20-capabilities)**). Under `serve` (engine v0.6.7+) it adds `serve` so the port can bind, and caps the **preamble**, handlers, cron ticks and spawned agents alike |
| `--cap-set "<list>"` | `run` / `test` / `conform` / `serve` | tailored host ceiling (`name` or `name=scope`, e.g. `"stdout,time,serve=8080,net=api.example.com"`); `--cap-set none` = an empty ceiling (nothing, not even `stdout`); mutually exclusive with `--sandbox` |
| `--profile native\|pure` | `run` / `test` / `conform` / `build` | `pure` (default `native`) is a **second wall**: every filesystem/exec/socket/db/cron builtin fails with the truth of the environment, independent of the ceiling — see **[Sandboxing § the pure profile](/en/0.6.x/22-sandbox)**. `serve --profile pure` is a usage error (a server binds a socket) |
| `--audit json\|<path>\|fd:N` | `run` / `test` / `conform` / `serve` | one JSON line per capability check — `{ts, context, capability, granted, source, reason, origin, file, line}` — to stderr (`json`), a file, or an fd (Unix only), plus a final `{"summary": {granted, denied, exit}}`. Secret **values** never appear (only names). See **[Observability](/en/0.6.x/63-observability)** |
| `--env-file <path>` / `--no-env-file` | all | override / disable `.env` loading |
| `--out <file>` / `--base-url <url>` | `openapi` | write the document to a file instead of stdout / set OpenAPI `servers` (default: none — the running server derives it from `domain`/`Host`) |
| `--watch` | `serve` | dev loop: restart on any `.syn` change (templates/statics already hot-reload per request) |
| `--port` / `--domain` / `--tls-auto` / `--bind` / `--secure` | `serve` | deployment knobs (see **[Deploy](/en/0.6.x/71-deploy)**) |
| `--serve --bind <addr>` [`--port` `--domain` `--tls-auto` `--tls-cert` `--tls-key` `--secure`] | `build` | (v0.6.16+) a server binary: the serve runtime with these knobs baked in; a bind is required (`--bind`, or the block's `bind "…"` clause, v0.6.18+); the serve block's static mounts are bundled automatically |
| `--no-console` / `--icon <svg\|png\|ico>` / `--bundle [--name <n>] [--id <id>]` | `build` | (v0.6.18+) desktop: GUI subsystem (Windows), the app icon (`.exe` resource / `.icns` / launcher PNGs), a `Name.app` (macOS) or a `dir/` + `install.sh` (Linux) — all by the engine's format; `.exe` is appended automatically for a Windows engine — see **[Your app on the desktop](/en/0.6.x/41c-desktop)** |
| `-o <file>` / `--include <p>` / `--engine-binary <p>` | `build` | output path / bundle a file, dir (recursive) or `*`/`?` glob (repeatable) / build against a donor engine binary (cross) — see **[`synsema build`](#synsema-build--one-program-one-binary)** |
| `-- <args...>` | `run` | end of engine flags: everything after `--` is the program's own argv, readable with `args()` |
| `--engine <subcommand>` | any binary | run the plain engine CLI (a no-op prefix on `synsema`; the only way into the engine of a `synsema build` binary) |

`conform` honors `--sandbox`/`--cap-set`/`--profile`/`--audit` with the same meaning as `run`
(engine v0.6.14+; before that it silently ignored them — the one subcommand that didn't sandbox).
Its stdout stays the `{ok, out, err}` JSON contract; a denial shows up inside `err`.

## REPL

`synsema repl` opens an interactive session: each line is a top-level statement, and **state persists across lines** (a `let` on one line is visible on the next). Results are shown with `print`/`show` — bare expressions are evaluated but not echoed:

```sh
$ synsema repl
>>> let who be "repl"
>>> show "hola " + who
hola repl
>>> print(type_of(who))
text
```

Exit with **Ctrl+D** (Ctrl+Z then Enter on Windows). It also works non-interactively — pipe statements in (`printf '...' | synsema repl`) to script quick checks.

## Exit codes

`0` on success; `1` on a parse error, a runtime error, or if any spawned **agent** ended in `ERROR`; `2` on a usage error — a missing argument, `synsema openapi` on a file without `serve`, or (engine v0.6.14+) **an unknown `--flag`**, which is now rejected instead of silently ignored on `run`/`test`/`conform`. Plain `run` prints the stable one-liner `Runtime error: file:line:col: msg`; add `--explain` for the rich report. (For measuring exit codes in a shell, don't pipe before `echo $?` — redirect instead.) Since engine v0.6.19 an output nobody reads is not a failure: when stdout or stderr's reader is gone (`synsema run x.syn | head -1`, a parent that closed the pipe, PowerShell capturing a GUI program it does not wait for) the process ends **quietly with 0** — the SIGPIPE convention — instead of a Rust panic.

> **v0.6.14, breaking on purpose.** Three things a program that ran before may now stop on, each
> to keep the ceiling honest: an unknown flag is an error (above); under a host ceiling, `stdout`
> is a real capability, so `--cap-set` without `stdout` denies output at the first `print`
> (`--sandbox` still includes it); and a `render` of a **disk** template needs `require
> file.read("…")` like any other file read (bundled templates and nested `include`/`layout` don't).
