# Remote & Cross-Device Connections

> Applies to: ChengOS v0.2.0+ ｜ Last verified: 2026-09-25 ｜ Source: `crates/cheng-cli/src/config.rs`, `crates/cheng-cli/src/main.rs`, `crates/cheng-cli/src/auth`, `crates/cheng-local-executor`

The `cheng` CLI does not have to run on the same machine as the ChengOS server. When it does
not, one question decides everything else: **when a workflow reads a file, whose disk is it?**

## Two workspace modes

| Mode | File tools run | Sees |
|---|---|---|
| `server_local` | On the API host | The server's filesystem |
| `client_local` | In this CLI process | **Your** directory |

`client_local` is what makes a remote server useful for local work: the server orchestrates,
your machine executes, and your files never leave it.

### The default is chosen for you

If you do not specify a mode, the CLI derives it from the server URL's host:

```text
localhost | 127.0.0.1 | ::1   →  server_local
anything else                 →  client_local
```

That default is right almost always. A local server sharing your filesystem should just use it;
a remote server obviously cannot.

Override per session with `--workspace-mode server_local|client_local`, or permanently with
`workspace_mode` in the config file. An unsupported value is rejected outright rather than
silently falling back.

**`cheng back` is always `client_local`** and deliberately has no `--workspace-mode` flag, even
when the server is on localhost: the product promise is that the selected CLI process owns and
serves its launch directory.

## Connecting

```bash
cheng chat --server https://chengos.example.com
```

Use the server's public HTTPS address. A hardened server binds its API to `127.0.0.1`, so
`http://<host>:19225` is unreachable from another machine by design; the reverse proxy forwards
`/api` and `/ws` on 443.

Precedence, highest first: **command-line flags → environment variables → the config file →
built-in defaults**. The environment variables are `CHENG_SERVER_URL` and `CHENG_TOKEN`; the
default server is `http://localhost:19225`.

The config file lives at `$CHENG_CONFIG` or `~/.config/cheng/config.json`:

```json
{
  "server_url": "https://chengos.example.com",
  "token_file": "~/.config/cheng/token",
  "default_workspace": "Default Workspace",
  "default_workflow": "main_chat",
  "request_timeout_secs": 30,
  "auth_storage": "auto",
  "workspace_mode": "client_local",
  "workspace_tools": { "shell_enabled": true, "git_enabled": true }
}
```

## Credentials

Bearer tokens are **never echoed, never written to REPL history, and never written to project
files**. Persisting one is an explicit choice, made in the user-scoped config file — or better,
via `token_file`, a pointer to a file whose trimmed contents are the token.

Storage policy (`auth_storage`, or `--auth-storage`):

| Policy | Stores the credential |
|---|---|
| `auto` (default) | OS keyring; on failure, ask on a TTY or error when non-interactive |
| `keyring` | Require the OS credential service |
| `host-bound` | AES-256-GCM file encrypted with a host/user-derived key — transparent, no master password |
| `encrypted-file` | AES-256-GCM file encrypted with a master password |
| `file` | Plaintext file — requires explicit opt-in |
| `none` | In-memory only; lost on exit |

`host-bound` is the pragmatic choice on a headless box: encrypted at rest, no keyring daemon, no
password prompt in a script.

Note that `auto` **errors rather than downgrading** when there is no TTY. A CI job with no
keyring fails loudly instead of quietly writing a plaintext token.

Credentials are **server-scoped**, so one machine can hold credentials for several ChengOS
servers and switching `--server` switches identity with it.

## Local tools in client-local mode

In `client_local` mode the CLI executes workspace tools itself, through
`cheng-local-executor` — the same execution shell used by other local clients, so file and
process semantics do not drift between them.

The `workspace_tools` config section controls what is permitted:

| Setting | Default | Meaning |
|---|---|---|
| `shell_enabled` | on | Allow shell commands |
| `shell_allow_shell_string` | **off** | Allow raw `sh -c` strings — high risk |
| `shell_default_timeout_ms` / `shell_max_timeout_ms` | | Command timeouts |
| `shell_env_allowlist` | empty | Extra environment variables commands may set |
| `shell_deny_patterns` | empty | Extra denied command substrings, defence in depth |
| `git_enabled` | on | Git operations |
| `tests_enabled` | on | Test runner |
| `markdown_enabled` | on | Markdown tooling |
| `preview_enabled` | on | Preview |
| `code_index_enabled` | on | Code index |

`shell_allow_shell_string` is off by default because a raw shell string defeats argument-level
policy: once the command is one opaque string, the deny patterns are the only line left. Turn it
on only when you genuinely need shell features such as pipes.

**File changes are decided by the server, described by you.** Before a mutating file operation
runs, the server's approval policy asks the executor that owns the files for a **preflight**: a
side-effect-free description of the planned change (canonical paths, whether targets exist,
sizes, hashes). In `client_local` mode that executor is this CLI. Small, scoped, undoable changes
then run without a prompt; everything else asks. The facts are pinned, and the CLI refuses the
call with `stale_precondition` if a file changed in between. An older CLI that cannot answer the
preflight still works, but every file change asks for approval (`preflight_unavailable`). See
[Tool-call Approval Policy](/docs/agent-approval-policy).

Read-only sessions are the safest way to point the CLI at a directory you do not want touched:

```bash
cheng chat --read-only --cwd ~/projects/my-repo
```

## Sessions across devices

```bash
cheng sessions                     # your CLI sessions on the server
cheng chat --resume <session-id>   # rejoin one
cheng history                      # durable conversations, incl. paused work
cheng chat --resume-conversation <uuid>
```

Sessions live on the **server**, so a session started on your laptop is listed from your
desktop. `--resume-conversation` exists for when the old CLI session has expired but the
conversation is still there; it conflicts with `--resume`, `--workspace`, and `--workflow`
because those would contradict the conversation's own binding.

Resuming a client-local session checks that the stored root label matches your current
directory — a resumed session should serve the same tree it was serving before. Labels longer
than the server's display cap are stored truncated with a leading ellipsis, and the comparison
accounts for that.

## Background agents

```bash
cheng back                  # serve this directory as a client-local workspace
cheng ps [-a]               # list background agents
cheng logs <pid|session>    # where its logs go
cheng stop|restart|rm <id>
cheng prune                 # clear stopped and stale metadata
```

`cheng back` runs headless and serves the current directory to the **Web UI**: you keep working
in the browser while your files stay on your machine. `--status`, `--stop`, and `--logs` act on
the agent recorded for the current directory, which is usually easier than looking up a pid.

To serve several projects from one agent, declare the roots — on the command line with
repeatable `--root PATH[:ro]`, or once in the config file:

```json
{ "back_roots": ["/home/me/works/api", "/home/me/works/docs:ro"] }
```

Use absolute paths in the config file. Each root gets its own sandbox and session; roots must not nest. `--root` overrides
`back_roots`, and with neither the launch directory is served.

## A remote setup that works

```jsonc
// ~/.config/cheng/config.json
{
  "server_url": "https://chengos.example.com",
  "auth_storage": "host-bound",
  "workspace_mode": "client_local",
  "default_workspace": "Default Workspace",
  "default_workflow": "main_chat",
  "workspace_tools": {
    "shell_enabled": true,
    "shell_allow_shell_string": false,
    "git_enabled": true,
    "tests_enabled": true
  }
}
```

Then `cheng chat` in any project directory connects to the remote server while executing file
and shell work locally.

## Troubleshooting

| Symptom | Cause |
|---|---|
| File tools see the wrong files | Wrong workspace mode — check the auto-selection rule above |
| Auth fails in CI | `auto` errors without a TTY; use `host-bound` or `CHENG_TOKEN` |
| Shell commands are refused | `shell_enabled` off, a deny pattern matched, or the session is `--read-only` |
| `--resume` refuses | The stored root does not match your current directory |
| `cheng --version` differs from the server | It reports the ChengOS release version; a mismatch means genuinely different builds |
| Connection refused on port 19225 | The server's API is loopback-only; use its HTTPS address |
| Every small edit asks for approval | This CLI predates preflight (`preflight_unavailable`); upgrade it |
| `cheng back` refuses the roots | Two roots nest; serve their common parent as one root |

## Next steps

- [Using the CLI](/docs/cli-usage) — commands and the REPL.
- [Installing the CLI](/docs/cli-install) — getting the binary.
- [Authentication](/docs/api-authentication) — the tokens behind all of this.
