Skip to main content
The CLI is built to be driven by AI coding agents and scripts. In agent mode it prints JSON, never opens the interactive UI, never prompts and never animates. Every failure has a stable error code and a semantic exit code.

Agent mode

Agent mode turns on automatically when the CLI sees that a coding agent started it. It checks these environment variables, in this order: Any of these counts when it is set to a value other than empty, 0, false, no or off. Set SYNHEART_AGENT=0 to force agent mode off, even inside an agent’s shell. This is useful when you run the CLI by hand in a terminal that an agent also uses. In agent mode:
  • The output format defaults to JSON. An explicit --format text still wins.
  • The interactive UI never opens. Bare synheart prints the command map instead.
  • There are no spinners, no colour and no passive update notices.
  • Nothing prompts. A command that would prompt fails at once with SH-AGENT-001, and the hint names the flag to pass. For example, synheart login needs --ci-token <token> or SYNHEART_CI_TOKEN. synheart update does not prompt either: without --yes it only reports whether a newer version exists (a result with up_to_date, current and latest, and exit code 1 when there is one), and --yes applies it. synheart local --https and synheart local cleanup need sudo to trust the certificate and edit /etc/hosts; when sudo would ask for a password they fail with SH-AGENT-001 instead. Run sudo -v first, or run the command yourself with SYNHEART_AGENT=0.
synheart guard, which holds coding agents to rules you set, ignores SYNHEART_AGENT=0. An agent cannot switch detection off to get around a rule, and only a person at a terminal can grant an exception. See AI & agents.

Ask for JSON

Pass --json or --format json on any command that supports it. The command reference marks each command, and supports_json in the command map says the same thing for scripts. --json together with --format text is a usage error. synheart install and synheart sync count too: under --json they print one result object. A few commands have no JSON form, such as guard allow, guard init, guard install, guard sync, guard team join and guard team leave. An explicit --json on one of them is a usage error (exit 2) instead of silently printing text. guard rules, guard report, guard check, guard team status and explain do print JSON.

JSON conventions

Every JSON body follows the same rules, so scripts and agents can rely on it.
  • Names use snake_case. Common fields keep the same spelling everywhere: id, name, description, created_at, error_code, logged_in, next_cursor.
  • Lists are objects with an items array, even when there is only one page. This leaves room for next_cursor later without a breaking change.
  • Platform commands follow the same shape. org, project and api-key lists print {"items": [...]}, plus next_cursor and pagination when the platform sends them. A show prints the object itself. The platform’s success and message wrapper is dropped. A command with nothing to return, such as a delete or a revoke, keeps the platform’s body.
  • Timestamps are RFC 3339, such as 2026-05-06T18:42:11Z, never Unix integers. Time zone offsets are kept as given.
  • Durations are integers with a _ms (milliseconds) or _s (seconds) suffix.
  • Required fields are always present, even when empty. Optional fields are left out when they have no value.
  • Adding a field is not a breaking change. Removing or renaming one, or changing its type, is. Array order is only stable when the field name says so.

Errors in JSON

When a command fails in JSON mode, it still prints a valid JSON body and exits with a non-zero code. You never have to choose between the exit code and the body.
Every failure has the same seven fields, whatever went wrong. All of them are always present, and detail and hint can be empty strings.
  • error_code is the stable code. See Errors and environment.
  • class is user (fix it locally), server, transient (retrying may work), usage or unknown.
  • exit_code repeats the process exit code, in case a consumer lost the status.
  • title is the short message. error holds the same text, for older readers.
  • detail and hint are the longer explanation and the command to try next.
When the platform API answered with its own error, the body adds upstream with status, code, message and trace_id, and detail is the platform’s message.
A command that reports status, such as whoami, auth status, auth refresh, runtime current and config path, keeps its own fields and adds the error fields beside them. For example, whoami signed out still has logged_in: false. Its exit code follows the table below. A mistake in how you called the CLI, such as an unknown flag, uses error_code: "SH-CLI-USAGE" and exits 2. It is a JSON body whenever --json or --format json appears anywhere on the line, even after the bad flag. In text mode the usage pointer goes to stderr. An unknown scenario name is SH-MOCK-NOTFOUND and exits 4. In a stream, the error event carries the same fields plus ok: false. syni version --json prints spec, runtime and source. source is vendor when it read the installed files. With no vendored copy it falls back to what synheart.lock pins and reports "source": "lockfile". whoami --json and usage --json include claims_refreshed. It is true when that run refreshed a stale access token first, so a recent plan change is already reflected in the result.

Follow long-running commands (NDJSON)

Installs, servers and recordings can be followed line by line. Each line on stdout is one JSON object, flushed immediately. Progress bars, spinners and banners are off, and stderr carries only genuine warnings.

Which flag gives what

A command that prints one final object keeps doing so under --json, so existing scripts do not break. Streaming is opt-in with --format ndjson. A server has no single result, so --json and agent mode already mean the stream. --json together with --format ndjson is a usage error (exit 2).

The envelope

Every event starts with the same three fields:
ts is RFC 3339 with milliseconds. Other fields are snake_case and sorted alphabetically; do not rely on their order. Ignore unknown fields and unknown event names, because new ones can be added.

Events

Exactly one terminal event (done or error) ends a run, and nothing follows it. The process exit code matches exit_code. A server stops on SIGINT or SIGTERM by emitting done with "interrupted": true and exits 0. A failure before the stream starts, such as a bad flag, is a single error event.

Receiver exports

In event mode synheart receiver writes only events to stdout. Each received export is an item event with kind: "export" and an export_id. The export is in payload (one compact line) when you pass no --out, or path is the file written when you do. --output-format then only affects the files under --out. The ready event carries endpoint and token, and done carries received, duplicates and errors.

Recipe: start the mock server and connect

  1. Start synheart mock start --json and read stdout line by line.
  2. Wait for the ready event. Do not connect before it, because the listeners are not bound until then. A port already in use produces an error event instead.
  3. Read ws_url (also sse_url and udp_url) and connect. dashboard_url is in the event only when you started the server with --web.
  4. Stop the server with SIGINT and read the final done event for counts.

Exit codes

Branch on the exit code instead of parsing messages.

Command map

synheart help --json prints a machine-readable map of every public command and flag. synheart --help --json and bare synheart in agent mode print the same map. For one command, run synheart <command> --help --json or synheart help <command> --json. It prints just that command’s entry, the same object as one element of commands. In agent mode --help defaults to JSON; pass --format text to get the text back.
  • Commands are sorted by path. Hidden commands and flags are left out.
  • flags holds a command’s own flags. global_flags holds the flags every command inherits.
  • subcommands lists the paths of a command’s visible direct subcommands.
  • supports_json is derived from the commands themselves, so it is true for every command that prints a single JSON body, including install and sync. It is false for the streaming commands (mock start, mock record, mock replay, local, receiver): their --json emits an NDJSON event stream instead of one object.
  • group is the section a top-level command appears under in synheart --help. It is empty for subcommands.
  • exit_codes and env list the exit codes and the environment variables the CLI reads.
An agent that has not seen the CLI before can read this once and know every command it may call.

Check the setup with doctor

synheart doctor --json reports each check as an entry in checks[]:
  • ok is true when no check has status error. summary counts the checks by status.
  • scenarios_dir is left out when there is no local scenarios directory.
  • status is ok, warn or error.
  • fix.command is the exact command that resolves the check.
  • safe_to_run is true only when the command restores declared state and then exits, such as synheart sync or synheart install runtime. When it is false, ask a person before running it.
  • needs_human is true when a person has to take part, such as synheart login, which opens a browser.
  • doctor exits 1 only when your app cannot work: the runtime is missing or incompatible, or its files are missing or modified. Warnings do not change the exit code.
In agent mode, synheart login fails fast with SH-AGENT-001 instead of opening a browser. Pass --ci-token <token> or set SYNHEART_CI_TOKEN for unattended sign-in.

MCP server

synheart mcp serve runs an MCP (Model Context Protocol) server on stdin and stdout, so agents can call the CLI as tools. It is read-only: it describes state and never signs in or changes anything. It needs no network of its own.
Once it is connected, you can ask the agent things like:
  • “What can you tell me about HSI?” The agent calls synheart_explain with topic: hsi.
  • “Run synheart doctor and tell me if anything is broken.” The agent calls synheart_doctor and summarizes.
  • “What mock scenarios can I use?” The agent calls synheart_list_scenarios.
To check the server from a shell, without any client:
When a tool fails, the reply sets MCP’s isError flag and puts the error text in the content, so the conversation does not abort. If the agent cannot find synheart, give the MCP client the absolute path from which synheart, because client subprocesses often inherit a minimal PATH.

Recipes

Bootstrap a project

An agent sets up a Flutter or native project from a clean checkout:
Run synheart doctor --json again at the end. "ok": true means the app can run.

Branch on failure

Run the CLI unattended