A local coding agent written in Rust. rho runs as a headless process communicating via JSON-RPC 2.0 over stdin/stdout, talks to a model on your machine, and uses tools to read files, edit code, and run commands — with your approval at every step.
Features
- 🔒 Safety-first — file sandbox, approval gates, secret redaction, and untrusted-data framing by default
- ✏️ Hashline editing — content-addressed line references (LINE#HASH:) prevent stale-context corruption in file edits
- 💻 Cross-platform — runs on Windows, macOS, and Linux
- 🐚 PowerShell-native — the shell is PowerShell (via
pwsh); the model generates PowerShell commands, not bash - 📂 Project-aware — auto-detects project root, loads context files (
AGENTS.md,CLAUDE.md,.cursorrules, etc.) with hash-verified trust - ⚙️ Configurable — two-tier TOML config (user-level
~/.rho/config.toml+ project-level.rho/config.toml), per-tool approval policies, command denylist - 🧠 Local and remote models — native OpenAI Responses plus Chat Completions compatibility for LM Studio, Ollama, Groq, OpenRouter, DeepInfra, Z.ai, and custom endpoints, with named presets and multi-provider support
- 🔌 JSON-RPC 2.0 — headless protocol over stdin/stdout for embedding in editors, bots, and custom UIs. OpenRPC schema available for client generation.
Quick Start
Prerequisites
- Rust toolchain (edition 2024)
- PowerShell 7+ (
pwsh) — required for shell command execution - A local model server (e.g. LM Studio or Ollama) on
localhost:1234, or an API key for an external provider
Using a local model
-
Start a local model server on
localhost:1234. -
Build and run:
cargo run --package rho --model <model-id>
Or send prompts via JSON-RPC:
echo '{"jsonrpc":"2.0","method":"prompt","params":{"message":"list the source files"},"id":1}' | \ cargo run --package rho --model <model-id>
rho will use its tools to read your project, emit streaming events as notifications, and return a response via JSON-RPC.
Using an external provider (e.g. OpenAI)
-
Set your API key:
export OPENAI_API_KEY="sk-..."
-
Create
.rho/config.tomlin your project root:[agent] model = "gpt-4o" token_budget = 131072 [[providers]] preset = "openai" api_key_env = "OPENAI_API_KEY"
-
Run:
cargo run --package rho -- --accept-external-provider
The
--accept-external-providerflag skips the consent warning that warns your data will be sent to an external server. Omit it on first use to see the warning.See External Providers for more providers (Groq, OpenRouter, DeepInfra) and detailed configuration.
Protocol note: the
openaipreset uses OpenAI's native Responses API (POST /v1/responses). Other presets and custom providers use Chat Completions (POST /v1/chat/completions) unlessapi = "responses"is explicitly configured. Providers with unrelated native formats require an OpenAI-compatible proxy.
CLI Options
rho [OPTIONS]
Options:
-m, --model <MODEL> Model identifier (uses config default if omitted)
-s, --system <SYSTEM> Override the system prompt
--compact Use a compact prompt for small-context models (~100 tokens)
--root <ROOT> Project/sandbox root (auto-detected if omitted)
--endpoint <URL> API endpoint URL (overrides config)
--api-key-env <VAR> Environment variable holding the API key
--max-iterations <N> Maximum agent loop iterations
--accept-external-provider Skip consent warning for external endpoints (also implied by --endpoint)
--token-budget <TOKEN_BUDGET> Context window token budget (default: 32768)
--session <PATH> Resume a previous session from a JSONL file
-c, --continue Resume the most recent session for this project
--ephemeral Run without disk persistence
JSON-RPC 2.0 Protocol
rho communicates via JSON-RPC 2.0 over stdin/stdout. Diagnostic output (warnings, budget info) goes to stderr.
Example
echo '{"jsonrpc":"2.0","method":"prompt","params":{"message":"list the source files"},"id":1}' | \ cargo run --package rho -- \ --ephemeral \ --endpoint http://localhost:1234/v1/chat/completions \ --model my-model
Output is a stream of JSON-RPC responses and notifications (ready, agent/start, message/delta, tool/call, tool/result, agent/end, etc.). See docs/src/rpc-mode.md for the full protocol reference, or docs/rpc-schema/openrpc.json for the machine-readable OpenRPC schema.
Frontends
rho is headless by design, so you can drive it from any UI that speaks its
JSON-RPC 2.0 protocol. Two frontends are available:
- rho-code — an interactive terminal UI (TUI) that spawns
rhoin headless JSON-RPC mode and renders a chat over the stream. - rho-ui — a native desktop UI for
rho, built with Makepad.
Configuration
rho loads config from two TOML files, with project-level overrides taking precedence:
| Source | Path | Purpose |
|---|---|---|
| User-level | ~/.rho/config.toml |
Global defaults: default model, API endpoint |
| Project-level | .rho/config.toml |
Per-project: model, approval policies, command denylist, context files |
Example .rho/config.toml (local model with preset):
[agent] model = "qwen3-8b" token_budget = 32768 [[providers]] preset = "lm-studio" [approval.per_tool] write_file = "ask" run_command = "ask" edit_file = "ask" [shell] denied_commands = ["Stop-Process"] [redaction] enabled = true
Example .rho/config.toml (OpenAI Responses via preset):
[agent] model = "gpt-5" token_budget = 131072 reasoning_effort = "medium" [[providers]] preset = "openai" # selects api = "responses" api_key_env = "OPENAI_API_KEY"
A custom endpoint remains on Chat Completions by default. Opt into Responses only when the server supports its wire format:
[[providers]] name = "custom-responses" endpoint = "https://llm.example.com/v1/responses" api = "responses" api_key_env = "CUSTOM_API_KEY"
API keys are never stored in config. Reference environment variables instead:
[[providers]] preset = "openai" api_key_env = "OPENAI_API_KEY"
Security Model
rho treats model output as untrusted and applies defense-in-depth:
| Layer | What it does |
|---|---|
| File sandbox | All file operations are confined to the project root |
| Approval gate | Write, edit, and shell commands require your confirmation |
| Command denylist | Dangerous commands (Remove-Item, Invoke-WebRequest, etc.) are blocked by default |
| Secret redaction | API keys and tokens in tool output are replaced with [REDACTED] |
| Untrusted-data framing | File contents are wrapped in <context> tags so the model treats them as data, not instructions |
| Context-file trust | Project instruction files (AGENTS.md, etc.) are hash-verified; changed files require re-confirmation |
| Provider consent | Connecting to an external API triggers a warning before any data leaves your machine |
Architecture
┌─────────────────────────────────────────────────┐
│ rho (binary) │ ← Headless JSON-RPC 2.0 agent
├─────────────────────────────────────────────────┤
│ rho-ext rho-tools │ ← Extensions / Built-in tools
├─────────────────────────────────────────────────┤
│ rho-memory rho-highlight │ ← Siblings of rho-tools
├─────────────────────────────────────────────────┤
│ rho-core │ ← Agent kernel: loop, types, traits, config
├─────────────────────────────────────────────────┤
│ rho-ai │ ← Unified LLM provider abstraction (streaming, retry, SSE)
└─────────────────────────────────────────────────┘
rho-test-helpers ← Dev-only: mocks, fixtures, tempdir helpers
Dependency rule: crates only depend on layers below them. rho-ai is the lowest layer; rho-core depends on it for the LlmService trait. rho-highlight and rho-memory depend on rho-core. rho-tools depends on rho-core, rho-highlight, and rho-memory. rho-ext depends on rho-core for trait implementations. The binary assembles everything.
Development
cargo xtask ci # Full CI pipeline (fmt → lint → audit → build → test) cargo xtask test # Run all tests cargo xtask test -- --nocapture # Run with stdout visible cargo xtask changelog <ver> # Generate CHANGELOG.md cargo xtask schema # Update version in OpenRPC schema cargo xtask generate-models # Regenerate the rho-ai model catalog from OpenRouter cargo xtask fmt-fix # Auto-format the workspace (cargo fmt --all)
Pre-push hook
The fast gates (cargo xtask fmt, cargo xtask lint) run in CI on every
push to trunk. To catch them locally before a push — rather than as a
red check after the fact — install the pre-push hook:
git config core.hooksPath .githooks
This runs fmt and lint (the exact same commands CI uses) and blocks the
push on failure. Bypass once with git push --no-verify. The hook skips the
slow build/test steps; run cargo xtask ci for the full local pipeline.
Project layout
rho/ # Headless JSON-RPC 2.0 agent
src/
main.rs # Thin: parse CLI, build App, run
lib.rs # Module declarations
cli.rs # CLI argument parsing
app.rs # App struct — startup orchestration, extension loading, AgentResult extraction
model.rs # Model resolution
ext_observer.rs # CompositeObserver — fans out to RPC + extension observers
rpc.rs # JSON-RPC 2.0 protocol (methods, notifications, approval gate)
rpc_wire.rs # Typed wire-format structs (params, results, notifications)
transport.rs # Transport trait (StdioTransport)
presenter.rs # Presenter module root
presenter/
rpc.rs # RpcPresenter — diagnostic output to stderr
rho-ai/ # Unified LLM provider abstraction (streaming, retry, SSE)
rho-ext/ # TypeScript extension runtime (V8/deno-core)
rho-core/ # Agent kernel (loop, types, traits, config)
rho-tools/ # Built-in tools (files, shell, rust tooling, memory)
rho-memory/ # Persistent knowledge base (SQLite/FTS5)
rho-highlight/ # Tree-sitter syntax analysis
rho-test-helpers/ # Shared test utilities (dev-only)
xtask/ # Dev task runner
Community Discord - Coming Soon
Contributing
Contributions are welcome! Please follow conventional commits:
feat:— new featuresfix:— bug fixesdocs:— documentation changesrefactor:— code changes that neither fix bugs nor add featurestest:— adding or updating testschore:— maintenance tasks
Run cargo xtask ci before opening a PR.