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 (GORM and 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. Most of what defines how an agent behaves 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 by default 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 — is engine behavior, implemented in rysh-shared, not per-surface etiquette. SecretNAT lives in rysh-shared too, though today only the terminal switches it on. 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. Today the CLI is what reads that file; the server's chatbot is configured in its own settings. What both run is the same engine. "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.
- The NATS server and actor system belong to the processes that host the engine;
rysh-sharedsupplies the envelope, subjects and bridge they share, but doesn't run 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, hosted, billed — belongs to a surface.
Honest answers to the two questions this raises
"Is it open source?" Partly. The rysh CLI, its shared module and the desktop app are open source under Apache-2.0; the collaboration server is proprietary, and it would be dishonest to let "one shared codebase" imply the whole platform is open. The CLI runs on your own machine, with your Anthropic API key, and your data stays yours; the server ships as containers with its migrations and a config template, and runs on your own infrastructure. Self-hostable and open source are different promises; for the server, we make the first one.
"So the AI is yours?" Also no, and happily: the engine runs on Claude by default. 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, and by default it runs on Claude with your own Anthropic key.
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) early; when your product claim is "same engine everywhere," that test suite is the claim, executable — and it's a suite we still haven't written.
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