# Reasonix Guide
README
·
简体中文
·
Spec
> Day-to-day configuration and usage. For the engineering contract and internals
> (data types, registries, package layout, roadmap), see the **[Spec](./SPEC.md)**.
## Contents
- [Configuration](#configuration)
- [Billing and display currency](./BILLING.md)
- [CLI reference](./CLI.md)
- [Environment variables](#environment-variables)
- [Web frontend](#web-frontend)
- [Configuration paths](./CONFIG_PATHS.md)
- [Reasoning language](./REASONING_LANGUAGE.md)
- [Task contracts and pause policy](./TASK_CONTRACT.md)
- [Custom OpenAI-compatible providers](#custom-openai-compatible-providers)
- [Hooks](#hooks)
- [Desktop hooks](#desktop-hooks)
- [Keyboard shortcuts](#keyboard-shortcuts)
- [Permissions & sandbox](#permissions--sandbox)
- [Capability diagnostics](#capability-diagnostics)
- [Plugins (MCP)](#plugins-mcp)
- [Slash commands](#slash-commands)
- [Embedded documentation retrieval](#embedded-documentation-retrieval)
- [@ references](#-references)
- [Two-model collaboration](#two-model-collaboration)
- [Cross-project task dispatch](#cross-project-task-dispatch)
- [Letting the model change a setting](#letting-the-model-change-a-setting)
- [Google Gemini, native (experimental)](#google-gemini-native-experimental)
## Configuration
Resolution order: **flag > `./reasonix.toml` > the user config file >
built-in defaults**. Starting with **Reasonix v1.8.1**, the user config lives at
`~/.reasonix/config.toml` on macOS/Linux and
`%AppData%\reasonix\config.toml` on Windows; see
[Configuration paths](./CONFIG_PATHS.md) for migration and related data paths.
Fields marked user/global only are not overridden by `./reasonix.toml`.
Provider entries name secrets with `api_key_env`, while the secret values live in
Reasonix's global `/.env`, shared by CLI and desktop. Project
`.env`, home `.env`, inherited shell environment variables, legacy credentials,
and the OS keyring are not provider-key runtime fallbacks; legacy credentials are
only migration sources. Project `.env` still feeds workspace-scoped,
non-provider `${VAR}` expansion for MCP/plugin settings without importing
provider keys or Reasonix control variables. See
[Configuration paths](./CONFIG_PATHS.md) for the full `config.toml` and `.env`
structure.
For the desktop and CLI usage of visible reasoning language, see
[Reasoning language](./REASONING_LANGUAGE.md).
```toml
default_model = "deepseek-flash" # executor; set [agent].planner_model to add a planner
# language = "zh" # ui language; empty = auto-detect from $LANG / $REASONIX_LANG
[ui]
# shortcut_layout = "desktop" # classic|desktop; compatibility setting
# cursor_shape = "bar" # block|underline|bar; CLI/TUI text cursor
show_turn_usage = false # hide per-request token/cost receipts in the TUI; default true
[agent]
reasoning_language = "auto" # visible reasoning text: auto|zh|en
# plan_mode_read_only_commands = ["gh issue view"] # legacy compatibility only; Plan bash now uses Permissions
# planner_model = "deepseek-pro" # optional low-frequency planner
# subagent_model = "deepseek-pro" # optional default for runAs=subagent skills
# subagent_models = { review = "deepseek-pro", security_review = "deepseek-pro" }
# max_subagent_depth = 8 # nested delegation depth; a named stop against runaway recursion, 0 removes it; set 1 for the single-layer boundary (user config only)
# max_subagent_concurrency = 0 # session-wide sub-agent concurrency (task/fleet/skills); 0 = no limit (user config only)
# max_parallel_writers = 0 # concurrent writers with non-overlapping write_paths; 0 = no limit, write claims keep writers apart (user config only)
# max_side_sessions = 0 # side conversations open at once; 0 = no limit (user config only)
# max_provider_retries = 10 # retries of a failed provider connection before the turn stops; 0 = keep trying until cancelled (user config only)
# max_queued_inputs = 0 # prompts waiting in a session's queue; 0 = no limit, the queue's byte limits still apply (user config only)
# max_subagent_steer_bytes = 16384 # bytes in one message of guidance to a running sub-agent, which keeps one message from filling its context; 0 = no limit (user config only)
# max_subagent_steers_per_minute = 30 # messages of guidance per minute to one running sub-agent, a stop for a loop; 0 = no limit (user config only)
# write_lease = "strict" # user file only; default optimistic: a writer that declares no path (bash, MCP) does not hold the whole workspace; strict makes it hold it
# write_lease_max_wait_minutes = 0 # minutes a write waits behind another session's claim before it fails with workspace.claim_timeout; 0 (default) = wait as long as the turn lasts, 120 is a sensible limit (user config only)
# subagent_claims = "strict" # user file only; default optimistic: a sub-agent that declares no write_paths holds only the path it is writing; strict makes it hold the whole workspace
# plan_approval = "auto" # user file only; a submitted plan starts executing without the approval click, and the transcript says so; default ask
# default_mode = "plan" # user file only; a new conversation starts in plan mode (a resumed one keeps its own; Settings has a switch); default normal
# compact_ratio = 0.90 # sole auto trigger; presets 0.70 / 0.80 / 0.90
# max_output_tokens = 0 # recommended: automatic (DeepSeek default high → ~64K; not unlimited)
# max_output_tokens = 32768 # ordinary coding / cost control
# max_output_tokens = 65536 # heavy reasoning / long tool loops
# max_output_tokens = 131072 # only after repeated finish_reason=length
# max_output_tokens never changes compact_ratio; only the final send-time clip does
[[providers]]
name = "deepseek-flash"
kind = "anthropic"
base_url = "https://api.deepseek.com/anthropic"
model = "deepseek-flash"
api_key_env = "DEEPSEEK_API_KEY"
web_search = true
# also preset: deepseek-pro
# idle_timeout_seconds = 300 # per-provider stream idle watchdog; how long a call may send nothing before it is read as dropped (default 300s; a silent pre-header wait is retried once, so up to ~2×)
[tools]
enabled = [] # omit/empty = all built-ins
# disabled = ["web_fetch", "ask"] # removed from every request by name, framework and MCP tools included; a call to one is refused with the reason; a tool the host itself reads (todo_write, complete_step, await_user, conclude_blocked, update_goal, use_capability) stays on and a warning says which; a project file can only add names, and Settings lists those as turned off by the project; Settings has a switch per tool; applies to conversations opened afterwards
bash_timeout_seconds = 120 # foreground safety cap; set 0 for no tool-local cap
protect_changed_files = true # refuse a whole-file write over a change made since the agent last read or wrote the file
browser_tools = true # built-in browser tools; the browser starts on first use
# mcp_resources = true # user file only; a read_mcp_resource tool for resources a connected MCP server lists or builds from its declared templates (a {+x} template is wide: read the full address on the card before approving); asks first; default off
# web_search_backends = ["searxng"] # user file only; ordered chain for web_search
# searxng_url = "http://localhost:8888" # your own SearXNG instance
mcp_startup_timeout_seconds = 30 # background initialize + tools/list safety cap
mcp_call_timeout_seconds = 300 # default MCP call safety cap; per-plugin/tool overrides may raise it
[tools.system_one]
api_key_env = "TYPESAFE_API_KEY" # optional TypeSafe AI System One decision protocol
model = "jev-latest"
# base_url = "https://api.typesafe.ai"
[tools.system_one.laya]
local = false # true: run the official Python SDK locally (`pip install laya`)
python = "python" # interpreter containing the laya package
model = "auto" # auto|english|multilingual|typed-decisions
# http_base_url = "http://127.0.0.1:8080" # self-hosted POST /v1/systemone gateway
# http_api_key_env = "LAYA_API_KEY" # optional gateway Bearer token
[environment]
enabled = true # inject a stable startup summary of OS, shell, and common tools
offline = false # set true when outbound network access is unavailable; prevents futile retries
# [environment.tools]
# go = "/opt/homebrew/bin/go" # optional explicit trusted path; workspace-local paths are not auto-executed
[skills]
# paths = ["~/my-skills", "../shared/skills"] # extra custom skill roots
# excluded_paths = ["~/.agents/skills"] # hide convention roots without deleting folders
# disabled_skills = ["review"] # hide skills until /skill enable
# suppress_warnings = true # quiet non-fatal skill loading warnings; doctor still reports issues
[permissions]
mode = "ask" # writer fallback when no rule matches: ask|allow|deny
deny = ["Bash(rm -rf*)", "Bash(git push*)"] # hard-blocked in every mode
allow = ["Bash(go test:*)"] # never prompted
[sandbox]
# workspace_root = "" # file-writers confined here; empty = current dir
# allow_write = ["/tmp"] # extra dirs write_file/edit_file/multi_edit/move_file may touch
# forbid_read = ["${HOME}/.ssh"] # paths the agent must not read or list
# allow_devices = ["/dev/nvidia*"] # host device nodes bound into the Linux bash sandbox (user config only)
# egress_unlisted = "allow" # unattended long tasks (user config only): hosts outside allowed_domains pass without asking, said once each; denied_domains still refuse
[peers]
# enabled = false # your sessions on this machine can message each other; every message waits for that session's user to approve it; also a switch in Settings, applied at the next turn (user config only)
# dispatch = false # the model may hand a task to a session in another of your projects, via dispatch_to_project (the session it opens sends its final answer back on its own), and ask you to open folders as projects with open_projects (one card lists every path; nothing is added or trusted until you approve it, and no mode answers it for you); needs enabled; turning it on is your consent for those sessions' tasks and replies, no card, and a project in your sidebar you have not decided about is trusted by being named; the opened session keeps the sender's model, preset, approval mode and planning, so a YOLO sender opens a YOLO session that answers the target project's ask rules too (deny rules still hold) (user config only)
# max_dispatch_depth = 1 # how many sessions deep a dispatch may chain; 0 removes the ceiling (user config only)
# max_dispatched_sessions = 8 # sessions one session may keep open from its dispatches; 0 removes the ceiling (user config only)
# release_projects = [] # projects that take messages from your other sessions without the card; their own approvals and sandbox still apply; they start over a pause the host set itself (session switch, unreadable @ reference, reopen) but never over one you pressed (user config only)
[mods]
# enabled = false # run the hooks module of installed Claude Code mods; full trust, no sandbox (user config only)
# hook_budget_ms = 0 # one hook's own running time in milliseconds; 0 keeps 10000 (user config only)
[serve]
# auth_mode = "token" # token (default)|password|none; none still requires the launch token for changes
# token = "" # optional fixed token; empty token mode generates one at startup
# password_hash = "" # bcrypt hash generated with reasonix serve --hash-password --password '...'
# behind_proxy = false # true only behind a trusted reverse proxy
[desktop]
# max_workspaces = 0 # most projects the sidebar remembers; 0 = no limit (user config only)
# session_ref_tokens = 8000 # most tokens one referenced past conversation (@session:) adds to a message; 500..200000
[[plugins]]
name = "example"
command = "reasonix-plugin-example"
startup_timeout_seconds = 60 # optional initialize + tools/list cap
call_timeout_seconds = 600 # optional per-server MCP call timeout
tool_timeout_seconds = { "generate_video" = 1800 } # optional raw MCP tool names
```
For the full schema and every field's contract, see [`SPEC.md` §5](./SPEC.md#5-configuration-toml).
The startup tool summary reuses a trusted snapshot in the current Reasonix data
directory for up to 24 hours. Probes refreshed by another data directory do not
replace that valid snapshot.
On expiry, transient probe failures retain previous
successful observations, keeping the system prefix stable across rebuilds and
restarts; definitive changes, such as a removed tool, are adopted on refresh.
Installed and project-configured MCP servers need no per-tool trust
list. The dedicated two-model Planner may use every non-destructive MCP tool,
even when the server omits `readOnlyHint`; strict read-only sub-agents still
require `readOnlyHint: true` and no `destructiveHint`.
`[agent].plan_mode_read_only_commands` is also retained for config round trips,
but the main Plan workflow no longer has a separate bash allowlist or trust
prompt. Bash classification and approval use the same Permissions rules in Plan
and Standard mode; the Sandbox remains the filesystem, process, and network
boundary. Dedicated planner and read-only subagent runners keep their own strict
read-only tool registry and foreground-command classifier.
### Environment variables
Most day-to-day settings belong in `config.toml` or the global Reasonix `.env`
described above. The variables below are process-level advanced switches; set
them before launching Reasonix. Project `.env` files are not a runtime source for
Reasonix control variables.
To hand extra variables to every command the `bash` tool runs, set the
user-global `[tools.shell.env]` table in `config.toml` (for example
`BASH_ENV`). It is applied over the inherited environment; a project
`reasonix.toml` cannot set it, so a cloned repository cannot inject variables
into your commands.
A preset is not scoped to a command you asked for: `BASH_ENV` is sourced before
*every* bash command, including the ones the host classifies as read-only and the
ones it runs as verification.
So keep it side-effect free and outside the workspace's writable roots: anything
it writes can invalidate what a verification just proved. The table reaches the
local bash tool only — a remote workspace and `reasonix review` build their own
sandbox and do not carry it.
### CLI telemetry
The CLI can send a once-per-day anonymous active-install ping and bounded,
content-free event counters to `https://crash.reasonix.io`. Configure the
user-global policy with:
```bash
reasonix config telemetry # print the effective mode
reasonix config telemetry auto # default: local interactive TTY only
reasonix config telemetry on # also allow local headless `reasonix run`
reasonix config telemetry off # disable and delete pending counter files
```
On the first eligible release-build interactive session, Reasonix explains the
exact data boundary and asks once before any telemetry request. The prompt is
`[Y/n]`: pressing Enter, `y`, or `yes` stores `auto`; `n` or `no` stores `off`
and deletes pending counters. After the choice is saved, enabled reporting is
silent and the prompt is not shown again. If the preference cannot be saved,
nothing is uploaded.
Reporting is always disabled in CI, development builds, and when
`DO_NOT_TRACK` is set or `REASONIX_TELEMETRY=0`. Under `auto`, redirected/piped
or otherwise non-interactive sessions do not report. When no choice has been
saved yet, these ineligible sessions neither prompt nor report. Network failures
after consent are silent and never change stdout, stderr, or the process exit
code; unsent counters stay in a bounded local queue for a later invocation.
The ping contains a dedicated random 128-bit CLI install ID, CLI version, OS,
architecture, and the `cli` surface marker. This ID is separate from the desktop
install ID and is not an account, hardware, repository, or session identifier.
Counter batches use that same ID for daily active-install deduplication and
contain only fixed buckets such as CLI mode/profile, permission/session mode,
turn latency, finish reason, cache-hit range, generic Provider/tool error class,
compaction, recovery counters, per-turn token-volume buckets, workspace-lease
contention buckets, and normalized UI language.
Closing the prompt without answering (end of input, Ctrl+D) stores nothing and
uploads nothing; it asks again the next time.
Reasonix never uploads prompts, answers, reasoning, tool names/arguments/output,
paths, repositories/branches, session IDs, exact token or cost values,
Provider/model names, base URLs, or environment variables.
### Trace export (OpenTelemetry)
Reasonix can hand each turn to your own observability stack as OpenTelemetry
GenAI traces. It is off, and nothing leaves the machine, until your user config
switches it on and names a collector:
```toml
[observability.otlp]
enabled = true
endpoint = "http://localhost:4318" # OTLP/HTTP; /v1/traces is appended
header_name = "Authorization" # optional: the header carrying a credential
header_env = "OTEL_EXPORTER_AUTH" # the variable holding its value, never the value
service_name = "reasonix" # optional
```
- **What is sent.** One trace per turn: an `invoke_agent` span, a `chat` span per
model attempt (provider, model, token counts, finish reason), and an
`execute_tool` span per tool call (tool name, failure class), nested under the
tool that delegated the work.
- **What is never sent.** Prompts, answers, tool arguments and results, file
paths and error text. Spans are built from typed event fields through one fixed
attribute set, and a name that does not look like a name is sent as `unknown`.
- **Who decides.** Only your user config. A repository's `reasonix.toml` cannot
switch it on or move it; the ignored key is reported at load.
- **Credentials.** `header_env` must be an `OTEL_*` or `REASONIX_OTEL_*` variable, so no
setting can read an unrelated secret. A credential is refused over plain `http`
to any host but loopback, the endpoint may not contain user info, and redirects
are not followed.
- **Failure.** Export never blocks a turn: a slow or dead collector drops spans
from a bounded queue.
- **Stability.** The upstream conventions are marked Development (read
2026-10-18); every payload carries `reasonix.semconv_experimental = true` and
attribute names can change. Not run against a real backend: built against the
OTLP/HTTP JSON encoding and tested with a local collector.
Studio has the same switches under Settings, Network. A save reaches sessions
opened afterwards.
### CLI crash reports
An unhandled Go panic that reaches the CLI entrypoint is saved locally as a sanitized report under
`/cli-crash-reports`. Reasonix keeps at most 10 files with owner-only
permissions. The panic value is never serialized. Absolute source paths become
`/.go:`, function arguments are removed, and the same secret,
token, email, and long-identifier scrubbers run both when saving and immediately
before sending.
Crash reports are never uploaded automatically. Review and manage them with:
```bash
reasonix report # preview newest; prompt before sending on a TTY
reasonix report list # list local reports
reasonix report show [ID] # preview without sending
reasonix report send [ID] # explicit send; delete locally only after success
reasonix report delete [ID] # delete without sending
```
Piped or redirected `reasonix report` calls only preview and never prompt or
send. The CLI telemetry setting does not auto-send or auto-delete
these separately reviewed reports. Runtime fatal throws, operating-system kills,
and panics in unwrapped background goroutines cannot be recovered by Go and do
not produce this local report.
## Web frontend
For local use, `reasonix web` starts the browser UI and opens it in your default
browser. Inside an interactive CLI session, `/web` snapshots the current session,
restores the terminal, and opens an explicit `/sessions/#token=...` deep link.
Even a never-used session keeps its reserved ID without forcing an empty
transcript onto disk, so the first Web turn continues the same session identity.
Studio and the Web UI offer **Side question** beside the composer for read-only
questions without adding them to the main conversation.
A submitted question appears immediately. Answer text streams as it arrives,
with the current phase, tool name and elapsed time shown while it runs.
Collapsing the panel keeps the request running; closing it cancels the request
and discards that side conversation. If a request fails, partial text is marked
interrupted and the draft remains for retry.
```bash
cd your-project
reasonix web
```
Use `reasonix web --no-open` when you want to start the foreground Web server
and print its URL without opening a browser tab. The lower-level
`reasonix serve` command starts the same engine without opening a browser by
default. It remains the right entry point for remote development boxes,
supervisors, tunnels, reverse proxies, and shareable authenticated sessions.
`reasonix web` starts at `127.0.0.1:8787`, automatically tries 8788, 8789, and
so on when a port is busy (up to 100 retries), and defaults to a newly generated
token even when `[serve].auth_mode` is `none`. Each live process registers a
single-writer heartbeat file under `/server/instances/`; clean
shutdown removes its own file, while later instances lazily remove records whose
owner process is confirmed dead. Multiple Web instances can therefore share one
Reasonix home without overwriting registry state. The process stays attached to
the terminal; stop it with Ctrl-C.
`reasonix serve` also defaults to a freshly generated per-launch token unless
`[serve].auth_mode` or `--auth` says otherwise.
- `--auth none` (or `auth_mode = "none"`) is an explicit opt-out for a Serve
fronted by its own authentication. Reads are open to whoever reaches a
loopback or bound address; other Host names get 421 `serve.host_rejected`.
- Every state-changing request, approvals included, still needs the launch
token: `Authorization: Bearer `, or the cookie the printed
`approvals:` link sets. Without it the answer is 403
`auth.launch_token_required`.
- Serve writes the token to a 0600 file under `/remote/` and
prints only its path; append `#token=` to the printed link.
A managed launch with `--token-file` names that file instead. The phone QR
code a terminal shows in token mode still carries the token.
- Prefer `--token-file` over `--token`: argv is visible to other processes,
sandboxed ones included. A plaintext `[serve].token` in the global config is
readable from inside the sandbox; keep the secret in a file.
- On macOS and Linux the OS sandbox denies the remote state directory and every
token file. Windows has no bash sandbox, so there only the read tools refuse.
- `[serve]` is read from the user config only; a project `reasonix.toml`
cannot set it.
Choose the mode explicitly when binding outside loopback:
```bash
reasonix serve --auth token
reasonix serve --addr 0.0.0.0:8787 --auth token
reasonix serve --auth password --password 'temporary-password'
```
Token mode prints a share URL with `#token=...`; the Web page exchanges the
fragment for an HttpOnly cookie before starting API or SSE requests, keeping the
token out of request URLs, browser history, referrers, and access logs. Pass `--token` or set
`[serve].token` to reuse a stable token. Password mode requires either
`--password` at startup or a stored bcrypt hash:
- Prefer `--password-file` over `--password`: argv is visible to every local
user through `ps` and `/proc//cmdline`.
- The file holds a single line and is readable only by its owner (`chmod 600`).
```bash
reasonix serve --auth password --password-file ~/.reasonix-serve.password
reasonix serve --hash-password --password-file ~/.reasonix-serve.password
# /config.toml
[serve]
auth_mode = "password" # none|token|password
password_hash = "$2a$12$..."
behind_proxy = true # only behind a trusted reverse proxy
```
The web UI exposes chat, tool approvals, session history, rewind/fork/summarize,
model and reasoning-effort controls, Goal, a live todo panel fed by the
`todo_write` tool, extension status/card/form/notification surfaces, and
provider balance when configured. Extension-hosted providers appear in the
model picker. Run `/reload` while idle to fail-atomically reload extension
sidecars and the runtime generation without restarting Serve. Use `--model`,
`--max-steps`, or `--resume` for one-off launches; otherwise `serve` uses the
user-global `default_model`.
If the selected Provider has no saved API key, a loopback-bound Serve still
starts and shows a Provider setup page instead of failing before the browser can
connect. After authentication, enter the key there; Reasonix writes it to this
host's global credential file with restricted permissions, rebuilds the active
controller in the same process, and opens the normal UI. The credential-writing
endpoint is disabled for non-loopback listeners. For a remote SSH window,
"this host" means the remote host reached through the SSH tunnel; the key is
not copied from the desktop machine.
## Editor integrations over ACP
`reasonix acp` exposes Reasonix as an ACP v1 stdio agent for editors and other
host clients. The dedicated **[ACP editor integration](./ACP.md)** guide covers
startup, capability negotiation, session lifecycle, independent model/work/
collaboration/approval controls, client filesystem and terminal capabilities,
MCP servers, permission requests, and the Reasonix mid-turn steering extension.
## Using Reasonix from DeepSeek Harness
DeepSeek Harness (`dsh`) can delegate a task to Reasonix as a subagent over ACP, with configuration only. [Using Reasonix from DeepSeek Harness](./DSH.md) covers the install through DSH's plugin market or by hand, switching the plugin on, the permission policy and the limits.
## Remote SSH
The remote module runs Reasonix on a remote host and reaches it over your own
SSH connection — VS Code Remote-SSH style. It bootstraps a persistent headless
`reasonix serve` on the remote host, forwards a local loopback port to it, and
opens the existing serve web client through that tunnel. The agent, its tools,
and its files all live on the remote host at full fidelity; nothing runs through
a lossy file proxy.
Linux, macOS and Windows remote hosts are supported. A Windows host needs PowerShell and OpenSSH, whichever login shell (cmd,
PowerShell or Git Bash) its `DefaultShell` names.
Hosts live in a user-global `[remote]` section of `config.toml`. Like
`[secrets]`, a project `reasonix.toml` cannot inject or override remote hosts —
a cloned repo can never steer where Reasonix opens SSH connections. Credentials
follow the provider idiom: the host names an env var (`passphrase_env`,
`password_env`) whose value lives in Reasonix's global `.env`; key material
itself is never stored — `identity_file` is a path.
Studio uses that host's explicit `workspace` as the default for host-level reads
and opens that omit a workspace.
Only when it is unset does the first nonempty
`workspaces` entry supply the default; with neither set, the attach receives an
empty workspace. An explicit open path takes precedence.
Reordering other
entries cannot override a declared `workspace`. These values come only from the
user's host configuration, and disabled hosts are refused before any link reuse.
The active SSH project's secondary line shows its actual connection directory,
including isolated worktrees. Its project menu shows the default connection
directory used by Refresh; these can differ. Settings' default action explicitly
chooses a new default.
A host-level read never borrows an unrelated pane's path.
```toml
[remote]
[[remote.hosts]]
name = "gpu-box"
host = "203.0.113.7"
user = "dev"
identity_file = "~/.ssh/id_ed25519"
workspace = "~/projects/app"
serve_install = "auto" # Remote CLI: auto | npm | upload | never
provider = "local" # Model credentials: local (this machine, over the tunnel) | remote (that host's own)
[[remote.hosts.forwards]]
type = "local" # local (-L) | remote (-R)
bind = "127.0.0.1:5432"
target = "127.0.0.1:5432"
```
A WSL distribution on a Windows machine is a host with `kind = "wsl"` and a `distro` (see `reasonix remote wsl list`):
- It has no address, so it takes only the folder and install fields. `host`, `port`, `user`, keys, passwords,
`proxy_jump`, `use_ssh_config` and forwards are rejected on it.
- Add one under Settings, Remote: the "WSL distribution" choice appears only on a machine that has WSL, and lists
its distributions (WSL 1 ones are shown disabled). Studio then opens it like any other machine in the list.
- Connecting needs WSL 2 and a distribution that has `ps`
and `nohup`; a minimal image without them is refused with the missing name.
- The kernel runs inside a shell session Studio keeps open, because WSL ends everything a session started when it ends.
- When Studio closes its link, the kernel goes away with the distribution (WSL stops an idle one after about 15
seconds).
- Conversations are kept on disk and resume on the next connect.
- Nothing keeps a distribution running behind your back.
- Model credentials come from the distribution's own configuration (`provider = "remote"`): under WSL's default
networking the distribution cannot reach this machine's loopback, so this machine's provider is not shared.
- A folder chosen as a Windows path (`C:\work\app` or `\\wsl.localhost\\home\me\app`) is translated by
the distribution; a folder it cannot see is refused.
- From the command line, `reasonix remote wsl add ` adds one, and `remote test` and
`remote connect` then work as they do for an SSH host.
- `connect` keeps the session open until you press Ctrl-C, and the kernel stops with it.
- `status`, `forward`, `serve` and `fs` stay SSH-only and refuse a WSL machine with
`remote: host is not reached over ssh`.
For an agent's own shell commands, only `list`, `help` and `wsl list|test` run:
- Every other `reasonix remote` subcommand refuses there (exit 77), because it changes the machine book or opens a
link, which a person does.
- The host marks the commands it runs for the agent to tell them apart. It guards against an agent that does not
hide; it is not the sandbox.
CLI:
```bash
reasonix remote add gpu-box dev@203.0.113.7 --workspace '~/projects/app'
reasonix remote import --all # import aliases; ssh -G resolves Include/Match rules when connecting
reasonix remote test gpu-box # dial + auth + host-key confirmation
reasonix remote connect gpu-box --open # bootstrap serve, tunnel, open the URL
reasonix remote serve status gpu-box
reasonix remote fs ls gpu-box:'~/projects/app'
```
Hosts with `use_ssh_config` enabled resolve the final effective configuration
through the local OpenSSH `ssh -G`, including `Include`, wildcard `Host`,
`Match` (including `Match exec`), repeated `IdentityFile`, `ProxyJump`, and
`IdentitiesOnly`. Import stores the original alias instead of a stale snapshot.
`connect` is a foreground supervisor (like `ssh -N` plus the serve bootstrap):
it keeps the tunnel and configured forwards alive, auto-reconnects with
exponential backoff if the link drops, and re-attaches forwards on reconnect.
Ctrl-C disconnects the local side only — the remote serve keeps running, so the
next `connect` reuses it. There is no background daemon in V1.
Host keys are verified against your OpenSSH `~/.ssh/known_hosts` (read-only)
plus a Reasonix-managed `~/.reasonix/remote/known_hosts`. A first-seen key
prompts for trust-on-first-use and is recorded in the managed file; a key that
contradicts a recorded one is a hard error that names the offending line and is
never auto-accepted.
Remote-side state lives under the remote host's `~/.reasonix/remote/`:
`serve-.json` (pid, bound loopback address, workspace),
`serve-.token` (0600; the auth token, passed to serve via `--token-file`
so it never appears in `ps`), `serve-.broker` (0600; the provider broker
token, see below), and `serve-.log`.
### Getting the kernel there
The remote runs a `reasonix serve`. The binary is a static Go build — no
runtime, no Node, no glibc — so putting it there is a file transfer, not an
install. `serve_install = "auto"` tries, in order:
1. **The remote fetches its own release.** Reasonix reads `SHA256SUMS` here and
hands that host the archive URL and the digest; the host downloads over its
own connection and keeps the archive only if it matches. Nothing but the
command crosses the SSH link, and a machine in a datacenter pulls the ~21MB
archive far faster than a laptop can push it.
2. **Upload this machine's binary**, when the platforms match.
3. **Download here and push over SFTP**, for a host with no route to the
release.
4. **npm**, last. `npm i -g reasonix` installs the same static binary but needs
Node ≥18 over there to do it; it stays because a network that mirrors npm
while throttling GitHub is a real one.
Set `serve_install` to `npm`, `upload` or `never` to take exactly one route.
`reasonix remote test ` reports which routes are open on a host before
you connect, and names what closes each one that is not.
### Model credentials
A remote session resolves its models on **your** machine by default. The
connection carries a reverse (`-R`) forward back to a provider broker Reasonix
runs on loopback here; the remote kernel calls that instead of a model
endpoint, and the request is issued from this machine with this machine's key.
So a remote host needs **no API key of its own and no egress to the model
API** — a locked-down build machine works. The key is never written to the
remote disk: only a per-connection broker token is, and only as a 0600 file.
The remote also never sees the endpoint, headers, or proxy settings behind a
provider; it sees the same catalog you do and nothing else.
Set `provider = "remote"` on a host to leave it resolving providers from its
own config instead, for a machine whose providers are deliberately not yours.
`reasonix remote serve start` never uses a broker: that serve is meant to
outlive the command that started it, and a broker published from a command that
has exited is a port nobody holds.
Two costs are worth knowing. Model traffic now leaves through your own uplink
rather than the remote's, which is a downgrade when the remote sits on a much
fatter pipe. And the models available to a remote pane are the ones configured
here, so a provider only that host had configured no longer appears unless you
switch it to `provider = "remote"`.
In the desktop app, manage hosts under **Settings -> Remote SSH**, then use the
status-bar chip or the host row's **Remote explorer** button to browse and edit
files over SFTP, manage port forwards, and start/open the remote workspace.
Opening a workspace creates a separate native Reasonix window, similar to a
VS Code Remote SSH window. The primary window owns the SSH tunnel; the remote
window is an isolated, lightweight shell and does not restore or acquire local
conversation sessions. The remote window uses **this** machine's providers
through the broker described above, so opening a workspace on a host that has
never held an API key just works. On a host set to `provider = "remote"` the
old path applies: the window shows the authenticated setup page first, saves
the key only in the remote Reasonix credential file, and activates the Provider
without restarting the remote Serve process. A transient SSH outage keeps the
remote window open;
the desktop reconnects in the background, re-attaches its loopback forward, and
reloads the window against the recovered Serve. An authentication or host-key
failure is terminal and closes the unusable remote window instead.
### Lifetime of the remote serve
- The `reasonix serve` process on the host is persistent. Quitting the desktop
or losing the link does not terminate it, and the next connect attaches to it.
- A pane's session on that serve does not outlive the desktop. Each pane drives
its own runtime there; closing the pane or quitting the desktop makes Studio
call `POST /runtimes/{id}/close` on the remote serve before it takes the
tunnel down.
- That close cancels a turn in flight and releases the session lease, so another
window can open the session. Only the pane's runtime is closed: the serve
process keeps running and the next connect can attach to it. Runtimes opened
by other clients are left alone too, whichever way the serve was started.
- `provider = "remote"` decides only where model credentials come from. It does
not detach a pane from the desktop.
- A serve already running for the workspace is attached to, never replaced,
whether `reasonix remote serve start`, another window or a hand-started
`reasonix serve --port-file` launched it. A later connect never deletes its
port or pid files and never stops it.
- `serve start` uses no broker, so a host with `provider = "local"` cannot
attach to it. That connect fails with an explanation and leaves the serve
running. Set the host to `provider = "remote"` (it then needs its own
credentials), or stop the serve with `reasonix remote serve stop `.
## Custom OpenAI-compatible providers
In the desktop app, open **Settings -> Model -> Access -> Add model service ->
Custom provider** for proxies, aggregators, or self-hosted services that speak
the OpenAI-compatible chat API or Anthropic-compatible Messages API.
For common providers, choose **Add model service -> Recommended preset** instead.
New official DeepSeek entries use the Anthropic-compatible Messages endpoint by
default and enable provider-side `web_search`; the same `DEEPSEEK_API_KEY` works
for both protocols. On startup, Reasonix upgrades unmodified legacy
`deepseek-flash` / `deepseek-pro` entries that still use the official endpoint
and standard key/model settings. Customized official Chat Completions entries
stay unchanged and show an **Upgrade protocol** action in Settings. Proxy
endpoints, custom headers, model lists, and capability overrides are never
migrated automatically. Existing
separately named `deepseek-anthropic` entries remain compatible, but that
redundant preset is no longer offered for new access. Reasonix can prefill editable custom-provider entries for Kimi CN,
Kimi Global,
Kimi Coding Plan, MiMo API, MiMo Anthropic, MiMo Token Plan CN/SGP/AMS and their
Anthropic-compatible variants, MiniMax CN/Global API, MiniMax CN/Global
Anthropic, GLM CN, Z.AI Global, GLM/Z.AI Coding Plan OpenAI-compatible and
Anthropic-compatible endpoints, OpenCode Go, OpenCode Go Anthropic, OpenCode Go
DeepSeek Anthropic, OpenCode Go DeepSeek Responses, OpenCode Zen
Anthropic, Qwen/DashScope CN/Global, Qwen Coding Plan CN/Global
OpenAI-compatible and Anthropic-compatible endpoints, StepFun OpenAI-compatible
and Anthropic-compatible endpoints, NovitaAI, GMI Cloud, Vercel AI Gateway,
HuggingFace Router, NVIDIA NIM, KiloCode, and Ollama Cloud. Plan names describe
the access/payment route; they include CN/Global only when the provider exposes
distinct regional endpoints. Kimi Coding Plan is therefore a dedicated plan
endpoint, while Kimi direct API is split into CN and Global. The preset path
usually needs only the provider API key: the key value is stored in Reasonix home
`.env`, while `config.toml` stores the endpoint, model list, key
environment-variable name, context window, vision model metadata, proxy bypass
for China-only endpoints, MiniMax `reasoning_split`, GLM/MiniMax thinking
heuristics, Anthropic-compatible Bearer auth where needed, Ollama Cloud
max-effort support, and OpenCode Go per-model reasoning overrides. The dedicated
OpenCode Go DeepSeek Anthropic and DeepSeek Responses presets expose the verified
Flash routes and enable provider-side `web_search` by default; the Responses
variant uses stateless context replay. The existing mixed OpenCode Go Anthropic
preset remains scoped to Qwen and MiniMax so server tools are not sent to
unverified models. DeepSeek Pro remains on the Chat Completions preset because
live Anthropic and Responses requests currently fail in the OpenCode Go upstream
conversion. The OpenCode Go preset includes its native `kimi-k3` subscription
route with image input,
`high`/`max` reasoning effort, and a 1,048,576-token context window. Existing untouched
OpenCode Go preset installs are upgraded automatically; edited model catalogs
are preserved. The Kimi CN and Kimi Global direct-API presets also include
`kimi-k3` with image input, a 1,048,576-token context window, and the official
`low`/`high`/`max` effort scale (default `max`). For the official K3 endpoints,
Reasonix preserves complete assistant messages across turns, sends output limits
as `max_completion_tokens`, and omits K3's fixed sampling parameters. Untouched
legacy Kimi direct-API catalogs are upgraded automatically without changing the
default model; custom catalogs and endpoints are preserved. After adding a
preset, open its provider card if you need to change models, headers, endpoint,
or compatibility settings.
Fill **API address** with the provider endpoint that should receive the standard
chat path. In this mode Reasonix previews and sends chat requests to:
```text
/chat/completions
```
Enable **Full URL** when the service gives you a complete request URL, for
example `https://gateway.example.com/v1/chat/completions`. Reasonix then sends
chat requests directly to that URL and does not append `/chat/completions`. The
preview under the field shows the exact request URL that will be used.
Model discovery uses the API address to try likely model-list URLs such as
`/models` and `/v1/models`. If the gateway requires a separate model-list
endpoint, open **Compatibility settings** and set `models_url`, for example
`https://gateway.example.com/v1/models`. If discovery is not available, fill the
model list manually.
**Full URL** still uses the OpenAI-compatible chat request body. It does not
switch the request schema to the OpenAI Responses API.
### Compatibility settings
The **Compatibility settings (usually leave unchanged)** section is for gateways
whose authentication, model-list endpoint, or reasoning/thinking request shape
differs from the normal OpenAI-compatible defaults. Leave these fields at their
defaults unless the provider documentation or a proxy error tells you otherwise.
For Anthropic-compatible services, such as some coding-plan endpoints, choose
**Anthropic-compatible** as the connection protocol before saving.
| Field | What it controls | When to change it |
| --- | --- | --- |
| `api_key_env` | The environment-variable name used for this provider's API key. Desktop-saved key values are stored in Reasonix home `.env` under this name; the TOML config stores only the name. | Change it when several providers need distinct keys, or leave it blank for a service that does not require an API key. |
| `models_url` | The URL used only for model discovery. Chat requests still use the API address or Full URL above. | Set it when `/models` or `/v1/models` is not where the gateway exposes its model list. |
| Extra request headers | Static HTTP headers, one `Header: value` per line. | Use for gateways such as OpenRouter that require `HTTP-Referer`, `X-Title`, or similar site headers. Keep bearer/API keys in the key field instead of duplicating them here. |
| Extra request body | A JSON object merged into the top-level chat request body. | Use only for provider-specific flags such as `{"enable_thinking": true}`. Reasonix still owns core fields such as `model`, `messages`, `tools`, `stream`, and `thinking`, and null values are rejected. |
| Authorization: Bearer | For Anthropic-compatible providers, sends the saved API key as `Authorization: Bearer ` instead of `x-api-key`. | Enable it only when the gateway documents Bearer auth, such as MiniMax Global or Vercel AI Gateway. |
| Model capability mode | Which reasoning request protocol Reasonix should use for this provider. | Keep **Auto-detect** unless the gateway is misdetected or the model docs require a specific reasoning format. |
| Thinking override | Provider-specific override for `thinking.type`. | Keep **Auto** unless the backend documents `enabled`, `disabled`, or `adaptive`. Unsupported values can make some OpenAI-compatible gateways reject the request. |
| Balance URL | Optional endpoint for wallet/balance lookup. | Set it when the provider exposes a balance endpoint and you want the desktop status bar to show it. The key is sent only to this address and never follows a redirect. |
| Balance format | How that endpoint answers: `deepseek`, `moonshot` or `openrouter` (`balance_format`). | A vendor's own domain (`api.deepseek.com`, `api.moonshot.cn`/`.ai`, `openrouter.ai`) is read in that vendor's format with nothing set. Any other address, a relay for example, must name the format, or it is shown as not configured and the endpoint is not asked. Existing entries on such addresses are written as `deepseek` once, which is how they were always read. Reasonix 1.x rewrites the shared config file without keys it does not know, so a format it dropped is put back at the next start from `provider-keys.json` beside the config, unless you removed it in the connection editor. |
| Context window | The provider-wide token budget Reasonix uses for automatic context cleanup. `0` disables automatic compaction. | Set it to the provider's model context limit; use a per-model override below when selected models differ. |
Each selected model also has an optional **Context window** input. Leave it blank
to inherit the provider-wide value, or enter a positive token count to override
that value for this model. This avoids premature compaction for long-context
models and provider errors for shorter-context models sharing the same endpoint.
Use the context-window limit from the model documentation, not the maximum output
tokens. For example, 128K commonly means `128000`; if the provider documents
`131072`, use that exact value. Values below 16384 show a non-blocking warning
because they can trigger frequent compaction and reduce cache hit rates.
Model capability mode options:
| Option | Effect |
| --- | --- |
| Auto-detect (recommended) | Reasonix chooses the request shape from model capability metadata and endpoint detection. |
| DeepSeek thinking | Uses DeepSeek-style thinking control, including `thinking.type` and DeepSeek-supported reasoning depth. |
| OpenAI reasoning | Uses the standard OpenAI-compatible `reasoning_effort` levels. |
| Plain chat | Sends no reasoning or thinking control fields. Use this for text-only proxies that reject reasoning parameters. |
Thinking override options:
| Option | Effect |
| --- | --- |
| Auto (provider default) | Does not write an explicit provider-level `thinking` override. Reasonix uses the provider/model default behavior. |
| Enabled | Sends `thinking.type = "enabled"` for compatible providers. |
| Disabled | Sends `thinking.type = "disabled"` for compatible providers. On DeepSeek-style providers this also avoids sending a reasoning depth hint. |
| Adaptive (self-adjusting) | Sends or preserves `thinking.type = "adaptive"` only for providers that document adaptive thinking, such as MiniMax-M3-style endpoints. |
Some OpenAI-compatible gateways require non-standard top-level request body
fields. Add them with `extra_body` on the provider entry:
```toml
[[providers]]
name = "spark"
kind = "openai"
base_url = "https://maas-coding-api.cn-huabei-1.xf-yun.com/v2"
models = ["xopglm52"]
api_key_env = "SPARK_API_KEY"
extra_body = { enable_thinking = true }
```
`extra_body` is merged into the chat JSON request body. Reasonix keeps core
fields such as `model`, `messages`, `tools`, `stream`, and `thinking` under its
own control.
## Hooks
Reasonix has hooks: shell commands that run at fixed points of the agent loop
and can observe, inject context, or (on two events) veto what is about to
happen. They are separate from permissions: `[permissions]` rules decide
whether a tool call is allowed or prompted, a hook runs your own code.
| Event | Fires | Can block |
| --- | --- | --- |
| `PreToolUse` | before a tool call, after matching; `toolName` and `toolArgs` are in the payload | yes |
| `PostToolUse` | after a tool call (success or failure) | no |
| `PostToolUseFailure` | after a tool call returned an error | no |
| `PermissionRequest` | before an approval prompt is shown | no |
| `UserPromptSubmit` | before a user prompt starts a turn | yes |
| `Stop` / `StopFailure` | when a turn ends / fails | no |
| `SessionStart` / `SessionEnd` | when a session becomes active / is closed or rotated by `/new` | no |
| `SubagentStart` / `SubagentStop` | around a foreground `task` call | no |
| `Notification` | when the agent needs the user's attention | no |
| `PreCompact` | before compaction; stdout becomes extra summary guidance | no |
| `PostLLMCall` | after each model turn; non-empty stdout on exit 0 replaces the stored reasoning text | no |
Hooks are configured in `/settings.json` (global) or
`/.reasonix/settings.json` (project). Each event maps to a list of hooks:
```json
{
"hooks": {
"PreToolUse": [
{
"match": "bash",
"command": "sh ~/.reasonix/hooks/no-git-push.sh",
"description": "block git push",
"timeout": 3000
}
],
"SessionStart": [
{ "command": "echo 'Team rule: run make lint before every commit.'" }
]
}
}
```
Fields:
- `command` (required): run by the platform shell.
- `match`: tool events only; an anchored regex over the tool name, so `file`
does not match `read_file`. Empty or `*` means every tool.
- `description`: the label shown in `/hooks` and in block notices.
- `timeout`: milliseconds; default 5000 for `PreToolUse`, `PermissionRequest`
and `UserPromptSubmit`, 30000 otherwise.
- `cwd` and `env`: working directory and extra environment.
A malformed file loads no hooks and does not stop Reasonix.
**Contract.** The event payload arrives as one line of JSON on stdin
(`event`, `sessionId`, `cwd`, and by event `toolName`, `toolArgs`, `prompt`,
`toolResult`, `error`, ...). The exit code is the verdict:
- `0` passes. On `SessionStart`, stdout (plain text, or JSON with
`hookSpecificOutput.additionalContext`) is injected once into the next real
user turn, as described under [Desktop hooks](#desktop-hooks).
- `2` blocks, but only on `PreToolUse` and `UserPromptSubmit`. A blocked tool
call is not run and the model receives `blocked: hook () stopped
this call — · command: ... · source: ...`, so write
the reason for the model to read.
- Any other code, or exit 2 on a non-blocking event, only warns the user.
- A timeout blocks on `PreToolUse` and `UserPromptSubmit` and warns elsewhere.
A hook that cannot be spawned does not block, and neither does a script that
crashes (exit 1, or 127 for a missing file): only an explicit exit 2 vetoes.
**Example: refuse `git push` in bash.** The payload's `toolArgs` for `bash` is
the tool's argument object, so the command is `toolArgs.command`. Save as
`~/.reasonix/hooks/no-git-push.sh` (needs `jq`):
```sh
#!/bin/sh
cmd=$(jq -r '.toolArgs.command // empty')
case "$cmd" in
*"git push"*)
echo "git push is disabled in this setup; ask the user to push." >&2
exit 2 ;;
esac
exit 0
```
Without a hook, the simpler tool is a permission rule, which needs no script
and holds in every mode:
```toml
[permissions]
deny = ["Bash(git push*)"]
```
Prefer the rule when a command pattern is enough; use a hook when the decision
needs code (inspect arguments, consult a file, log, or react to a prompt).
**Scope.**
- Global hooks (`/settings.json`) and hooks from installed
plugin packages apply, except in the read-only observe posture, which loads
no hooks at all.
- Project hooks (`/.reasonix/settings.json`) run only after the user
approves them as they stand (`reasonix trust`); until then they are held back
and a notice says so. Editing the file or a workspace script it names
withdraws the approval.
- `reasonix review` never runs project hooks, approved or not: the checkout is
untrusted input, so only global and plugin hooks apply.
- Only tool hooks (`PreToolUse`, `PostToolUse`, `PermissionRequest`),
`PostLLMCall` and `PreCompact` fire in every agent a session runs, subagents
included (see the `session_id` table below).
**Limits.**
- A hook cannot force the model's wording: a block reaches the model as a
refused call, and the model decides what to say or try next.
- There is no per-session scope: hooks come from the settings files and apply
to every session that loads them.
- Native hooks cannot answer an approval prompt; only a `PreToolUse` block
refuses a call.
## Desktop hooks
Desktop hooks run local commands at lifecycle events such as `SessionStart`,
`UserPromptSubmit`, `PreToolUse`, and `PreCompact`. A successful `SessionStart`
hook may write plain text to stdout, or return JSON with
`hookSpecificOutput.additionalContext`; Reasonix injects that text once into the
next real user turn as `...`.
This is intended for plugin or workflow bootstrap context, including
Superpowers-style startup instructions, without baking that workflow into
Reasonix's system prompt.
Plugin packages can provide this startup context through
`hooks/session-start-codex` or a plugin-root `CLAUDE.md`. Claude-style
`.claude/settings.json` command hooks are also mapped to matching Reasonix hook
events.
The injected hook context is dynamic current-turn context. It does not change
the stable system prompt, memory prefix, or tool schema, though dynamic content
can still reduce cache reuse for that turn.
Tool hooks (`PreToolUse`, `PostToolUse`, `PermissionRequest`), `PostLLMCall`
and `PreCompact` fire in every agent a session runs. Each fires under a
`session_id` derived from the parent session's id at the moment the hook runs:
| Agent | Hook `session_id` |
| --- | --- |
| Executor | `` |
| Planner | `:planner` |
| Guardian | `:guardian` |
| `task`, `read_only_task`, `parallel_tasks`, `fleet`, `run_skill`, `read_only_skill` children | `:subagent:` |
| `reasonix review` | a fresh id per run |
`/new` or a branch switch moves every child to the new id along with the parent.
`SubagentStart` and `SubagentStop`:
- bracket a foreground `task` call only; `read_only_task`, `parallel_tasks`,
`fleet`, skill children and background tasks fire neither;
- carry the call's id as `callId`, so a consumer can pair them;
- `SubagentStop` fires on every end: answer, failure, cancel or refusal;
- neither can block: exit 2 only warns.
## Studio appearance schedule
In **Settings → Appearance → Light and dark**, choose **Scheduled** to use
dark mode during a daily local-time range and light mode outside it. The initial
range is 20:00–07:00; edit either time to apply it immediately.
The start is inclusive and the end is exclusive. Overnight ranges cross
midnight. Equal times mean light all day; choose **Dark** for dark all day.
**Follow the system**, **Light**, and **Dark** retain their existing behaviour.
Choosing one stops the schedule without forgetting its times. The title-bar
theme toggle also chooses a fixed mode.
The range and mode stay on this device, using the same local preferences as
other theme choices. Desktop restores these across launches; web preferences
belong to the browser origin. If storage cannot be written, the choice lasts
only for the current window.
Scheduling uses the device's clock and timezone, not a server timezone. Skipped
or repeated daylight-saving times follow the actual local clock. Returning to
the window, restoring a page, or reloading recalculates the current scheme.
The active theme pack and text contrast still apply. Scheduling changes the
pack's light/dark variant, not the selected pack. The desktop window's native
frame and background follow the same resolved scheme.
Before the page loads, the shell uses the last saved resolved scheme as a
startup tint. The page then recalculates from the current clock and updates
the shell. CLI/TUI theme settings are unchanged.
## Keyboard shortcuts
Shortcuts are documented by client because users usually look for the keys that
work in the surface they are using. Desktop keeps its Plan toggle, while the CLI
cycles Ask, Auto, and Plan with `Shift+Tab`. Desktop uses `Cmd+Y` on macOS or
`Ctrl+Y` elsewhere for YOLO by default. If YOLO is rebound on Windows/Linux,
`Ctrl+Y` becomes the standard composer redo fallback. Desktop paste stays on the
platform paste key; in the CLI, terminal-native text paste and
application-owned image paste use separate shortcuts.
`[ui].shortcut_layout` is still accepted for old configs, but the shortcut
behavior below is unified across layouts.
For CLI/TUI text input, `[ui].cursor_shape` accepts `underline`, `block`, or
`bar`. The default is `bar`: it remains easy to locate without covering
double-width CJK characters in mixed-language input. Set it to `block` for a
traditional terminal cursor or `underline` for a lower-profile cursor. This
setting does not change desktop or web text fields.
### Desktop GUI
Desktop shortcuts are managed from **Settings → Shortcuts**. Pick a configurable
row, press a new key combination, and Reasonix saves it for the desktop app.
Standard editing shortcuts such as Undo and Redo are shown as locked rows because
the WebView's native text history uses those platform chords. Conflicting
bindings are rejected so one shortcut never triggers two actions. Press `?` or
use the help button in the topic bar to open the shortcuts sheet; it is generated
from the same shortcut registry, so it reflects any custom bindings.
Global shortcuts:
| Key or control | What it does | Notes |
| --- | --- | --- |
| `Cmd+K` on macOS, `Ctrl+K` on Windows/Linux | Toggles the command palette | The palette focuses search when it opens; `Esc` closes it. |
| `Cmd+,` on macOS, `Ctrl+,` on Windows/Linux | Opens Settings | Use **Shortcuts** in Settings to customize desktop bindings. |
| `Cmd+W` on macOS, `Ctrl+W` on Windows/Linux | Closes the active top tab | The last tab is kept by the normal close-tab guard. |
| `Cmd+B` / `Ctrl+B` | Shows or hides the left sidebar | Same action as clicking the sidebar toggle. |
| `Cmd+Shift+B` / `Ctrl+Shift+B` | Expands or collapses the most recent shell output | Same action as clicking the collapsed shell-output hint. |
| `Cmd+1`-`Cmd+9` on macOS, `Ctrl+1`-`Ctrl+9` elsewhere | Jumps to the matching visible chat in the sidebar | Hold `Cmd`/`Ctrl` briefly to reveal the numbered badges. Existing custom shortcuts that already use the same key take precedence. |
| `Cmd++`, `Cmd+-`, `Cmd+0` on macOS; `Ctrl++`, `Ctrl+-`, `Ctrl+0` elsewhere | Increases, decreases, or resets the interface size | Steps through the same sizes as **Settings > Appearance > Size > Interface** (80% to 180%) and saves the same setting. `=` is accepted for the plus key on keyboards that report it that way. |
| `?` | Opens the keyboard shortcuts sheet | The sheet shows the current effective desktop bindings. |
Composer shortcuts:
| Key or control | What it does | Notes |
| --- | --- | --- |
| `Enter` | Sends the current message | IME composition confirmation is left alone. |
| `Shift+Enter` | Inserts a newline | The composer keeps focus. |
| `Shift+Tab` | Toggles Plan on/off | Plan changes the workflow instruction; built-in writers keep the active Ask/Auto/YOLO and Sandbox boundary, while MCP writer/destructive targets stay hard-blocked for the whole planning phase. |
| `Cmd+Z` on macOS, `Ctrl+Z` on Windows/Linux | Undoes the latest composer edit | Native typing stays in the WebView history; Reasonix-managed paste, cut, folded blocks, and structured tokens are restored as complete transactions. |
| `Cmd+Shift+Z` on macOS, `Ctrl+Shift+Z` on Windows/Linux | Redoes the latest composer edit | On Windows/Linux, `Ctrl+Y` is also accepted after the YOLO shortcut has been rebound. |
| `Cmd+Y` / `Ctrl+Y` (default) | Toggles YOLO on/off | Turning YOLO off restores the previous Ask/Auto base when known. The current binding is shown in **Settings → Shortcuts**. |
| `Cmd+V` on macOS, `Ctrl+V` on Windows/Linux | Pastes clipboard content | Clipboard images are attached. Studio references ordinary files by their native path when available, including files selected with Add attachment, pasted, or dropped. Sources without a native path stream into the workspace. |
| Plain `Up` / `Down` at the prompt boundary | Recalls older or newer submitted prompts | Modified arrows and native text navigation stay with the textarea. |
| `Esc` while a turn is running | Cancels the running turn | If the turn has not produced a response yet, the draft is restored. |
**Settings → Appearance → Conversation display → Composer hint** controls
guidance in the empty task input. It is on by default. Turning it off hides
both task and queue placeholders immediately and persists on this machine.
The input's accessible name, entered text, queue content, IME handling and
send shortcuts stay unchanged. Turning the hint back on restores the
responsive guidance at the theme's normal contrast.
Remembered projects have Move up / Move down in their project menu. Open it with the keyboard, navigate with arrows or Home/End, and use Escape to return to the trigger. Moving a project changes sidebar order without changing the launch project.
Studio remembers which local and remote projects you collapse in the sidebar,
including Collapse all / Expand all, when you reopen it in the same browser or
desktop profile. New projects start expanded. These display choices stay on
this device; they do not change sessions or synchronize to another client.
**Recent** in the project heading finds saved conversations across projects,
including completed work that has left **Running**. It uses each conversation's
last saved activity, not its unread status, pin order or last opening time.
The picker reads 50 conversations per page from the complete matching history.
**Previous page**, **Next page** and its session/project search reach older
conversations without the sidebar's 300-row display ceiling.
Selecting a conversation opens it, or focuses its existing pane, switches the
sidebar to **All**, clears its search and expands the owning project. Worktree
conversations retain their own working directory. On phones, opening a session
closes the rail as usual; reopening it shows the selected project and session.
Archived conversations stay in **Archived**. Recent respects the saved
hide-legacy preference; an explicit search can still find legacy history.
Deleted files or unavailable folders report the normal opening error.
The environment selector starts with this machine. Selecting a remote host
reads its recent history through the existing connection flow; hidden or disabled
hosts are not offered. Older kernels ask for an update rather than showing a
partial sidebar window as complete recent history.
Activity comes from persisted session metadata and survives restart. The picker
reopens on the first page with no search; it does not create a separate visit
history or change the saved project order.
Ordinary Reasonix-managed worktree chats appear under their original project,
without a separate project row for each checkout.
Only isolated worktrees carry a neutral Isolated worktree label in the chat
header and composer directory picker. Otherwise, the header has no directory
badge and the picker shows just the branch name.
Hover for an explanation and the actual directory path. Worktrees separate code
directories; ports, databases, and other runtime resources may still be shared.
Before sending, a selected worktree explains that creation starts when the
message is sent. Worktree chats also carry a sidebar marker.
Reopening history uses that chat's working directory, while starting a chat
from the project uses the original directory. Ordering, search, paging, pins
and archives span the project.
When reading legacy history for the first time, the sidebar shows discovered
conversations while summaries and turn counts are filled in the background.
Conversations marked as reading their summary can already be opened.
Search indicates incomplete coverage instead of reporting a definitive empty result.
Failed summaries can be retried without blocking other conversations. Summary
caches live in the current version's directory; legacy history is not rewritten.
History search index rebuilds after an upgrade run in the background. Background
reads yield when a conversation is opened or read, then continue after the
foreground operation. Search results may remain incomplete until indexing finishes.
Removing the project closes its idle panes and retains every directory and
transcript. Existing transcripts stay in place and group on restart without migration.
Worktrees added separately outside Reasonix-managed storage remain independent projects.
With a paging-capable server, long conversations initially display the latest 200 messages. Scroll upward or
choose Load earlier messages to read more.
In-conversation find reads earlier pages as needed and indicates incomplete
coverage; clearing the query stops further prefetching. Rewind, compaction and
session changes reload recent content.
Paging affects presentation only, not the full conversation used by the model.
The list holds up to 32 projects; adding another requires removing one first. `serve-workspaces.json` now stores an object with `paths` and `launch`. Older binaries read only arrays and cannot preserve this format: do not share the same state home with an older binary after reordering.
Menus and controls:
| Key or control | What it does | Notes |
| --- | --- | --- |
| `Up` / `Down` in slash, `@`, or past-chat menus | Moves the highlighted item | Past-chat search uses the same navigation keys. |
| `Enter` / `Tab` in those menus | Accepts the highlighted item | Directory-like entries can keep the menu open for the next level. |
| `Esc` in those menus | Closes the current menu or returns from past-chat search | Regular typing continues after the menu closes. |
| Ask / Auto / YOLO approval controls | Picks the tool approval posture directly | Clicking these controls is unchanged by keyboard shortcuts. |
| Tool approval card | `Left` / `Right`, `Enter`, `1`-`4`, `Esc` | Move the highlighted action, confirm it, pick a numbered action, or deny. The default highlighted action is Allow once. |
| Plan approval card | `Left` / `Right`, `Enter`, `1`-`3`, `Esc` | Move between Revise plan, Start execution, and Exit plan. The default highlighted action is Start execution. |
| Plan control | Toggles Plan on/off | Same mode as `Shift+Tab`. |
| Goal item in the collaboration menu | Starts, views, or clears Goal | Goal is not in any keyboard cycle. |
#### Studio file attachments
Ordinary local files, including large APKs, use path references whenever the
desktop shell can identify the file. Selecting one external file grants read
access to that file, not its siblings; selecting a folder grants its descendants,
but a symlink inside it does not grant access outside that folder.
Files without a native path use binary streaming into `.reasonix/attachments`.
Streaming bounds memory use. Ordinary uploads have a default 1 GiB safety ceiling
per request to protect disk space; native path references do not copy bytes.
Incomplete files are removed if the transfer fails or is canceled. Removing an uploading
attachment cancels its transfer.
| Entry point | Limit |
| --- | --- |
| Native ordinary-file references | No attachment size cap; the file stays where it is. |
| Ordinary-file binary upload | Default 1 GiB safety ceiling per request, adjustable in user config. Disk space and upstream limits still apply. |
| Image upload | 10 MiB per image, checked before upload and by the kernel. |
| Legacy JSON/Base64 attachment endpoint | 25 MiB general-file limit; retained for older clients. |
| Encrypted cloud relay | 16 MiB per request body; oversized uploads stop at that budget. This is a relay limit, separate from native references and direct uploads. |
In the user's `config.toml`, `[attachments] max_stream_bytes = 1073741824`
sets the ordinary upload ceiling in bytes.
- `0` removes that ceiling; negative values use the default. Project `reasonix.toml` cannot set this value.
- An upload one byte over the limit is refused and its partial file removed.
- The typed `attachment.too_large` response includes `limit_bytes` and `limit_source` (`default` or `user`).
- A malformed user config refuses the upload until repaired. The image and relay limits remain independent.
These limits govern attachment transport. A referenced file is read on demand;
its full bytes do not become model input automatically.
### CLI / TUI
The composer uses theme-coloured top and bottom borders and a slim bar cursor by
default. Long drafts grow to the available maximum height; once they overflow,
wheel events inside the composer scroll the draft without moving the insertion
cursor, while wheel events in the transcript keep scrolling the conversation.
Use `/theme auto|light|dark` to select the background mode, or `/theme