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 textstill wins. - The interactive UI never opens. Bare
synheartprints 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 thehintnames the flag to pass. For example,synheart loginneeds--ci-token <token>orSYNHEART_CI_TOKEN.synheart updatedoes not prompt either: without--yesit only reports whether a newer version exists (a result withup_to_date,currentandlatest, and exit code 1 when there is one), and--yesapplies it.synheart local --httpsandsynheart local cleanupneed sudo to trust the certificate and edit/etc/hosts; when sudo would ask for a password they fail withSH-AGENT-001instead. Runsudo -vfirst, or run the command yourself withSYNHEART_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
itemsarray, even when there is only one page. This leaves room fornext_cursorlater without a breaking change. -
Platform commands follow the same shape.
org,projectandapi-keylists print{"items": [...]}, plusnext_cursorandpaginationwhen the platform sends them. Ashowprints the object itself. The platform’ssuccessandmessagewrapper 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.detail and hint can be empty strings.
error_codeis the stable code. See Errors and environment.classisuser(fix it locally),server,transient(retrying may work),usageorunknown.exit_coderepeats the process exit code, in case a consumer lost the status.titleis the short message.errorholds the same text, for older readers.detailandhintare the longer explanation and the command to try next.
upstream with status, code, message and trace_id, and detail is the platform’s message.
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 modesynheart 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
- Start
synheart mock start --jsonand read stdout line by line. - Wait for the
readyevent. Do not connect before it, because the listeners are not bound until then. A port already in use produces anerrorevent instead. - Read
ws_url(alsosse_urlandudp_url) and connect.dashboard_urlis in the event only when you started the server with--web. - Stop the server with SIGINT and read the final
doneevent 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. flagsholds a command’s own flags.global_flagsholds the flags every command inherits.subcommandslists the paths of a command’s visible direct subcommands.supports_jsonis derived from the commands themselves, so it istruefor every command that prints a single JSON body, includinginstallandsync. It isfalsefor the streaming commands (mock start,mock record,mock replay,local,receiver): their--jsonemits an NDJSON event stream instead of one object.groupis the section a top-level command appears under insynheart --help. It is empty for subcommands.exit_codesandenvlist the exit codes and the environment variables the CLI reads.
Check the setup with doctor
synheart doctor --json reports each check as an entry in checks[]:
okistruewhen no check has statuserror.summarycounts the checks by status.scenarios_diris left out when there is no local scenarios directory.statusisok,warnorerror.fix.commandis the exact command that resolves the check.safe_to_runistrueonly when the command restores declared state and then exits, such assynheart syncorsynheart install runtime. When it isfalse, ask a person before running it.needs_humanistruewhen a person has to take part, such assynheart login, which opens a browser.doctorexits 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.
- Claude Code
- Cursor / Claude Desktop
- “What can you tell me about HSI?” The agent calls
synheart_explainwithtopic: hsi. - “Run synheart doctor and tell me if anything is broken.” The agent calls
synheart_doctorand summarizes. - “What mock scenarios can I use?” The agent calls
synheart_list_scenarios.
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:synheart doctor --json again at the end. "ok": true means the app can run.