One codebase, every surface: how rysh-shared powers CLI, server, and chatbots
The same agent engine runs in a terminal pane, on the collaboration server, and behind a website chatbot. One Go module makes that true.
rysh's product pitch is one sentence: build an agent once, run it everywhere — your terminal, your server, your Slack, your website chat. That sentence is doing architectural work. Marketing pages can say "every surface" cheaply; a codebase either backs the claim or it doesn't. If the terminal agent and the website chatbot are two implementations, they will drift — different tool behavior, different safety semantics, different bugs — and "one platform" becomes a brochure.
So the load-bearing artifact isn't a feature. It's a Go module: rysh-shared.
The monorepo shape
rysh lives in a multi-project monorepo. The parts that matter for this story:
rysh-cli → the terminal client (TUI, panes, local agents)
rysh-server → the collaboration server (Go + Gin + PostgreSQL)
rysh-shared → the agentic engine, imported where the engine runs
rysh-proto → protocol definitions
A Go workspace wires rysh-proto, rysh-server, and rysh-shared together. rysh-cli is deliberately excluded from the workspace and keeps an independent dependency tree — a client that installs as one self-contained binary shouldn't have its dependency graph entangled with a server's (Gin, GORM, Postgres drivers have no business near the CLI build). More on that trade at the end.
rysh-shared is where the engine lives: the agentic execution core — orchestration of LLM tool-use loops, tool execution plumbing, approval semantics — plus the safety layers that give the product its character, like SecretNAT (the secret-translation layer under the ##snat commands) living in rysh-shared/secretnat, and even presentation details like diff colorization for edit previews. If it defines how an agent behaves, it lives here.
Three consumers, one behavior
The terminal. In rysh-cli, every pane is a conversation; a pane's LLMPromptExecutionActor runs the engine locally — tool-use loops on Claude, with your own Anthropic key, executing against your machine with approval gates on dangerous operations.
The server. rysh-server — multi-tenant workspaces, NATS subject-ACL isolation, shared panes — imports the same module for server-side execution. When a teammate interacts with a shared pane, the engine responding is not a reimplementation of the CLI's engine. It's the import.
The chatbot. letchat.ai, the embeddable customer-support chatbot built on rysh-server, is the same engine again, wearing a widget. The thing answering on a customer's website is the thing that runs in your pane.
The consequence that actually matters is governance defined once. rysh's safety story — visible tool calls, explicit approval for dangerous operations, secret handling — is engine behavior, implemented in rysh-shared, not per-surface etiquette. When approval semantics tighten, they tighten for the terminal, the server, and the chatbot in the same commit. A governance model reimplemented five times isn't a model; it's five roadmaps that used to agree.
Same for agent definitions. An agent in rysh is a markdown skill file — name, model, system prompt, optionally channel wiring. That file is portable across surfaces because the interpreter is literally the same code. "Write the agent once" is honest only when there's exactly one thing that reads it.
The failure mode this avoids
The default trajectory for a multi-surface AI product goes: terminal agent first, then "chatbot version" forked from it under deadline, then a Slack adapter forked from that. Eighteen months later: three tool registries, three approval behaviors, a security fix applied to two of them, and a sales call where nobody can say which surfaces redact secrets.
That's not a strawman; it's the gravitational pull of shipping. Resisting it is a structural decision made early — one module that is the engine — not a refactor performed later. Extracting a shared engine from three divergent implementations is the kind of project that quietly never finishes.
What rysh-shared is not
Boundary discipline is the other half of the story. The module is the engine, not a junk drawer:
- The TUI (Bubble Tea rendering, pane geometry, keybindings) is the CLI's own.
- The server's HTTP layer, billing, and models are the server's own.
- Actor wiring and NATS transport live with their processes;
rysh-shareddoesn't own the bus, it's hosted by things that do.
Rule of thumb: rysh-shared answers "what does the agent do, and what is it allowed to do?" Everything else — how it's displayed, transported, billed — belongs to a surface.
Honest answers to the two questions this raises
"Is it open source?" No. rysh is self-hostable — the engine runs on your infrastructure, with your Anthropic API key, and your data stays yours — but the source is not open, and it would be dishonest to let "one shared codebase" imply otherwise. Self-hostable and open source are different promises; we make the first one.
"So the AI is yours?" Also no, and happily: the engine runs on Claude. rysh's value isn't a secret model — it's everything wrapped around one: the surfaces, the governance, the tool system, the collaboration layer. The provider layer is pluggable by design, but today, Claude with your own key is the honest description.
Trade-offs, honestly
- Coupled release trains. One engine serving three surfaces means an interface change fans out everywhere at once. A change to the tool-executor contract touches CLI, server, and chatbot in one motion — safer than drift, but heavier per change. Contract stability becomes a first-class engineering concern earlier than you'd like.
- The CLI's independence has a seam. Keeping
rysh-cliout of the workspace protects its dependency tree, but means CLI-side integration with shared code is managed more carefully than a plain in-workspace import — occasional duplication at the boundary is the tax. We pay it for the one-binary install story. - What we'd do differently: freeze the tool-execution interface earlier — early churn there was the most expensive kind, rippling into every consumer. And add cross-surface contract tests (same skill file, same scripted scenario, asserted identical behavior on CLI and server) sooner than we did; when your product claim is "same engine everywhere," that test suite is the claim, executable.
The one-liner version: your architecture is your marketing claim, compiled. If the pitch says one engine on every surface, there had better be a module you can point at. Ours is rysh-shared.
rysh is in early access and we're recruiting design partners — teams who want agents in their real workflow and will tell us what breaks. Direct line to the founders: rysh.ai/design-partner.
More in this series: Why rysh is built on proto.actor and NATS · JetStream KV persistence patterns