> ## Documentation Index
> Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Use the CLI from AI agents

> Agent mode, machine-readable output, exit codes, the command map and the MCP server, so coding agents can drive synheart without surprises.

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:

| Variable | Set by |
| - | - |
| `SYNHEART_AGENT=1` | You, to opt in explicitly |
| `CLAUDECODE` | Claude Code, in the shells it spawns |
| `CLAUDE_CODE_ENTRYPOINT` | Claude Code, to say how it was launched |
| `CURSOR_AGENT` | Cursor's agent terminal |
| `CODEX_SANDBOX` | OpenAI Codex, when it runs commands in its sandbox |
| `GEMINI_CLI` | Gemini CLI's shell tool |
| `COPILOT_AGENT` | GitHub Copilot's coding agent |

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](#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`.

<Note>
  `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](/cli/reference/ai-and-agents#synheart-guard).
</Note>

## Ask for JSON

Pass `--json` or `--format json` on any command that supports it. The [command reference](/cli/reference/index) marks each command, and `supports_json` in the [command map](#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.

```bash theme={null}
synheart whoami --json
synheart mock list-scenarios --json
synheart env --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.

  ```json theme={null}
  {"items": [{"name": "baseline", "description": "..."}]}
  ```

* **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.

```json theme={null}
{
  "error_code": "SH-AUTH-001",
  "class": "user",
  "exit_code": 3,
  "title": "Not logged in.",
  "error": "Not logged in.",
  "detail": "No Synheart credentials found in the keyring or config file.",
  "hint": "synheart login"
}
```

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](/cli/errors).
* `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.

```json theme={null}
{
  "error_code": "SH-PLATFORM-403",
  "upstream": {"status": 403, "code": "...", "message": "...", "trace_id": "..."}
}
```

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

| Command kind | `--format text` | `--json` or agent mode | `--format ndjson` |
| - | - | - | - |
| One-shot: `install`, `sync` | Human text | **One result object** | Event stream, ending in `done` |
| Servers: `mock start`, `mock record`, `mock replay`, `local`, `receiver` | Human text | **Event stream** | Event stream (same) |

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:

```json theme={null}
{"event": "ready", "ts": "2026-05-06T18:42:11.123Z", "command": "synheart mock start", "...": "..."}
```

`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

| Event | When | Fields |
| - | - | - |
| `started` | Once, first | The inputs the command accepted |
| `progress` | Repeatedly while work runs | `package`, `artifact`, `bytes_done`, and `bytes_total` and `percent` when known |
| `ready` | Servers only, once, after every listener is bound | `url`, `ws_url`, `port`, and per-command URLs |
| `item` | One result per artifact, file or received export | `kind`, `status`, plus per-command fields |
| `warning` | A non-fatal condition | `message`, `kind`, plus context |
| `done` | Terminal, success | `ok: true` and a summary |
| `error` | Terminal, failure | `ok: false`, `error_code`, `exit_code`, `class`, `title`, `detail`, `hint` |

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

```bash theme={null}
synheart mock start --json | while read -r line; do
  [ "$(echo "$line" | jq -r .event)" = "ready" ] || continue
  ws_url=$(echo "$line" | jq -r .ws_url)
  echo "connect to $ws_url"
  break
done
```

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.

| Code | Meaning |
| - | - |
| `0` | Success. |
| `1` | General failure. |
| `2` | Usage error: bad flags or arguments, an unknown command, or `--json` with a different `--format`. |
| `3` | Authentication required or forbidden (`SH-AUTH-*`, and any `-403` such as `SH-PLATFORM-403`). |
| `4` | The requested resource was not found. |
| `5` | Transient failure (network or timing). Retrying may work. |

## 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.

```bash theme={null}
synheart help --json
```

```json theme={null}
{
  "name": "synheart",
  "version": "0.19.0",
  "commands": [
    {
      "path": "synheart mock start",
      "short": "Stream mock wearable data to your app",
      "long": "...",
      "usage": "synheart mock start [flags]",
      "aliases": [],
      "flags": [
        {"name": "scenario", "shorthand": "", "type": "string", "default": "baseline", "usage": "...", "required": false}
      ],
      "examples": ["synheart mock start --scenario baseline"],
      "supports_json": false,
      "hidden": false,
      "subcommands": [],
      "group": ""
    }
  ],
  "global_flags": [{"name": "format", "shorthand": "", "type": "string", "default": "text", "usage": "...", "required": false}],
  "exit_codes": {"0": "success", "2": "usage error: ..."},
  "env": [{"name": "SYNHEART_AGENT", "description": "..."}]
}
```

* 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](#follow-long-running-commands-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[]`:

```json theme={null}
{
  "ok": false,
  "summary": {"passed": 4, "warnings": 1, "errors": 1},
  "checks": [
    {
      "id": "runtime.files",
      "status": "error",
      "subject": "synheart-core-runtime-edge",
      "message": "synheart-core-runtime-edge v1.2.3 · 2 files missing",
      "missing_files": ["synheart/vendor/runtime/..."],
      "error_code": "SH-RT-FILES",
      "fix": {"command": "synheart sync", "safe_to_run": true, "needs_human": false}
    }
  ]
}
```

* `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.

<Note>
  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.
</Note>

## 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.

| Tool | Returns |
| - | - |
| `synheart_explain` | A plain-language primer on a concept such as HSI. Call it with no topic to list topics. |
| `synheart_doctor` | The environment diagnosis, same shape as `synheart doctor --json`. |
| `synheart_config_show` | The resolved project config. |
| `synheart_auth_status` | Where credentials load from, and whether they do. |
| `synheart_whoami` | The signed-in account. |
| `synheart_list_scenarios` | The mock scenarios, as `{"items": [...]}`. |
| `synheart_version` | The CLI version and build info. |

<Tabs>
  <Tab title="Claude Code">
    ```bash theme={null}
    claude mcp add --scope user synheart "$(which synheart)" mcp serve
    ```
  </Tab>

  <Tab title="Cursor / Claude Desktop">
    Add this to the client's MCP config. The file path depends on the client.

    ```json theme={null}
    {
      "mcpServers": {
        "synheart": {
          "command": "synheart",
          "args": ["mcp", "serve"]
        }
      }
    }
    ```
  </Tab>
</Tabs>

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:

```bash theme={null}
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | synheart mcp serve
```

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:

```bash theme={null}
# 1. Read the setup. Nothing here changes anything.
synheart doctor --json

# 2. For each check with status "warn" or "error" and fix.safe_to_run == true,
#    run fix.command. Typical fixes: "synheart sync", "synheart install runtime".

# 3. If there is no runtime or lockfile yet, install it.
SYNHEART_CI_TOKEN=... synheart login
synheart install runtime

# 4. Start mock data for the app to connect to.
synheart mock start --scenario baseline --duration 5m
```

Run `synheart doctor --json` again at the end. `"ok": true` means the app can run.

### Branch on failure

```bash theme={null}
synheart sync
case $? in
  0) echo "synced" ;;
  3) echo "sign in first" ;;       # SH-AUTH-*
  5) sleep 5 && synheart sync ;;   # transient, retry
  *) echo "failed; read error_code in the JSON error body" ;;
esac
```

### Run the CLI unattended

```bash theme={null}
export SYNHEART_AGENT=1                 # explicit, in case detection misses your tool
export SYNHEART_NO_UPDATE_CHECK=1
export SYNHEART_CI_TOKEN="$TOKEN"       # prefer the variable over --ci-token
synheart login
synheart update --check --json          # exit 1 means a newer version exists
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.