# Local clients

> kin --stdio hosts the ordinary interactive session for one local program that owns Kin's standard input and output — the same journal, workspace trust, tools, and questions as the terminal, projected as typed JSON lines. It is how an OS-native surface such as the Kinra OS Front Door carries a live Kin conversation…

`kin --stdio` hosts the ordinary interactive session for one local program that owns Kin's standard input and output — the same journal, workspace trust, conversation lease, tools, modes, and questions as the terminal, projected as typed JSON lines. It is how an OS-native surface such as the Kinra OS Front Door carries a live Kin conversation without a second harness, transcript, or authority model.

It is not [headless mode](/docs/kin/guide/headless/). `kin -p` runs one unattended turn and turns every human decision into a `needs-human` exit; `kin --stdio` keeps the session open, streams, asks, waits for the person, and takes follow-ups in the same conversation. It is also not a network service: there is no socket or port, and the client is whichever process holds the pipes.

<!-- SOURCE: src/kin/harness/stdio_host.py, src/kin/harness/tools/open_url.py, src/kin/harness/events.py, src/kin/harness/session/roundtrip.py, src/kin/tui/cli.py, src/kin/harness/workspace_trust.py, src/kin/harness/persistence/lease.py -->

```bash
kin --stdio --workdir ~/code/project              # a fresh conversation
kin --stdio --workdir ~/code/project --resume ID  # exactly one saved conversation
```

## The contract in one exchange

Every line is one JSON object. The client speaks first.

```text
→ {"type":"hello","protocol":1,"client":"front-door","capabilities":{"open_url":true}}
← {"type":"welcome","protocol":1,"kin":"0.6.10","capabilities":["open_url"]}
← {"type":"session","session_id":"s_…","model":"…","workspace":"/home/…/project","posture":"…"}
← {"type":"ready"}
← {"type":"mode_changed","mode":"auto"}
→ {"type":"turn","id":"t1","text":"What does this project's release gate check?"}
← {"type":"accepted","of":"turn","id":"t1"}
← {"type":"thinking","active":true}
← {"type":"tool_call","id":"call_1","name":"read_file","arguments":"{\"path\":\"Taskfile.yml\"}"}
← {"type":"tool_result","id":"call_1","name":"read_file","result":"…","result_truncated":true}
← {"type":"stream_chunk","message_id":"m1","content":"The gate runs …"}
← {"type":"stream_done","message_id":"m1"}
← {"type":"done","reason":"stop"}
← {"type":"turn_ended","id":"t1"}
→ {"type":"close"}
← {"type":"accepted","of":"close"}
← {"type":"closed"}
```

The events between `accepted` and `turn_ended` are the ordinary [event vocabulary](/docs/kin/reference/events/) — the same shapes the terminal renders — with a few fields bounded for the pipe.

## Client messages

| Message | Fields | Meaning |
|---|---|---|
| `hello` | `protocol` (must be `1`), `client` (≤64 chars, optional), `capabilities` (object of booleans, optional) | Required first message. Declares the protocol and the handoffs the client can perform. Today the only capability is `open_url`. |
| `turn` | `id` (≤64 chars), `text` (≤32,000 chars), `context` (optional, ≤8 items) | Start one root turn. The text is literal turn data — no `!` shell passthrough and no `/` skill dispatch, exactly like an [interactive launch prompt](/docs/kin/reference/cli/#launching-with-an-initial-prompt). |
| `cancel` | — | Interrupt the running turn, as ++escape++ does in the terminal. Refused as `idle` when nothing runs. |
| `resolve` | `kind`, `id`, and the answer | Answer the exact pending blocking event: `question` (`index`, optional `text`), `approval` (`outcome` `once`/`always`/`deny`, optional `note`, and the `display_signature` the event carried), `plan` (`index`), `proposal` (`decision` `apply`/`reject`). |
| `handoff_result` | `id`, `ok`, `detail` (≤500 chars) | Report what the environment did with a `handoff`. |
| `close` | — | End the session: the turn is cancelled, the journal closes, the lease is released, and the last line is `closed`. |

End of input, `SIGTERM`, `SIGINT`, and `SIGHUP` close the same way. Anything else is refused by name — never guessed at.

### Attention context

A turn may carry a short list of facts about where the person's attention was when they invoked Kin:

```json
{"type":"turn","id":"t2","text":"Summarize this page for me.",
 "context":[{"kind":"window","label":"Brave","value":"kinra-ai/kin — GitHub"},
            {"kind":"workspace","label":"workspace","value":"2 on DP-5"}]}
```

`kind` is one of `window`, `workspace`, `output`, `selection`, `path`, or `note`; labels are capped at 80 characters and values at 500. Kin frames the whole block as untrusted data ahead of the request and says what it is: a window title or path is a hint about context, never proof of a file, a page's content, or intent. Kin reads or asks before acting on it.

## Host messages

Beside the ordinary events, the host writes a small set of its own:

| Message | Fields | Meaning |
|---|---|---|
| `welcome` | `protocol`, `kin`, `capabilities` | The hello was accepted; the accepted capabilities follow. |
| `accepted` | `of`, `id` | A `turn`, `cancel`, `resolve`, `handoff_result`, or `close` was taken. |
| `refused` | `of`, `reason`, `detail` | A message was not taken: `malformed`, `unknown`, `bounds`, `busy`, `idle`, `stale`, `closing`, or `protocol`. The session is unaffected. |
| `turn_ended` | `id` | The turn settled, whichever way — after `done`, `interrupted`, or an `error`. |
| `handoff` | `id`, `kind`, `payload` | Kin asks the environment to perform one typed effect; answer with `handoff_result`. |
| `fatal` | `reason`, `detail` | The host cannot serve — `protocol`, `untrusted`, `lease`, `resume`, `backend`, or `host` — and exits after this line. |
| `closed` | — | The last line before exit. |

## One turn, exact answers, bounded lines

Only one turn runs at a time; a second `turn` is refused as `busy` until `turn_ended`. Cancel first if the new request replaces the old one.

A `resolve` is accepted only for the exact pending block of the stated kind. A wrong id, a wrong kind, a duplicate answer, or an answer after the turn settled is refused as `stale` and resolves nothing. An `approval` must echo the `display_signature` its event carried, so a stale review can never approve a changed call. Git credential and GitHub connection requests are forwarded but cannot be resolved over stdio; they belong to the terminal, and a client hands the conversation off (see below).

Client lines are limited to 64 KiB; an oversize or malformed line is refused and drained, and the stream continues. In the other direction, tool arguments and results are capped and flagged (`arguments_truncated`, `result_truncated`), image blocks are dropped, and `reasoning`, `todos`, `tasks_changed`, and `tool_call_draft` are not forwarded. Review bodies — approvals, plans, proposals — arrive complete.

## Handoffs: how an action leaves prose

A client that declares `open_url` gives the session one extra tool. When Kin decides a page should open, it calls `open_url` with one exact `http`/`https` URL; the host parks a `handoff` on the client:

```text
← {"type":"handoff","id":"ho_3","kind":"open_url","payload":{"url":"https://kinra.ai/kin"}}
→ {"type":"handoff_result","id":"ho_3","ok":true,"detail":"Brave"}
← {"type":"tool_result","id":"call_4","name":"open_url","result":"opened https://kinra.ai/kin in the person's browser (Brave)"}
```

The environment owns the effect: it decides how the page opens, may decline (`"ok":false`), and reports a fact, never authority over Kin. No page content comes back — reading stays with [`web_fetch`](/docs/kin/guide/web-tools/). A handoff the environment never answers settles on the tool's own timeout. Without the capability the tool does not exist, so a plain terminal Kin cannot open a browser. A client must never infer an action from a response; the typed handoff is the only door.

## Trust, leases, and handing off to the terminal

A host cannot create workspace trust. Like a headless or resumed launch, it needs a persistent trust record for the workspace before any project material or inference loads; otherwise the first line is `{"type":"fatal","reason":"untrusted",…}` and the process exits `2`. Trust the workspace once from an interactive `kin` there.

One conversation has one live client. `--stdio --resume ID` is refused (`fatal` with reason `lease`) while a terminal or another host owns that conversation, and the terminal's `--resume` is refused while a host lives. To move a conversation to the terminal — for a Git credential prompt, a plan review the client does not render, or simply deeper work — the client sends `close`, waits for `closed`, and then launches `kin --resume ID`. The journal is the transcript; the client keeps at most the opaque session id.

## Exit codes

| Code | Meaning |
|---|---|
| `0` | The client closed the session or its end of the pipe. |
| `1` | The host failed after the handshake (see the `fatal` line's `detail`). |
| `2` | The host could not start: bad hello, untrusted workspace, held lease, unknown resume target, or invalid model configuration. Argparse usage errors also exit `2`. |

`--stdio` is mutually exclusive with `-p`, with `--agent`, and with a positional prompt; it composes with `--workdir`, `--resume`, `--continue`, `--new`, `--no-save`, `--mode`, and the model and preset flags.

_Source authority: Kinra Site (src/content/docs/kin/guide/local-clients.md)._
