Rowboat: Build a Local-First Agent Runtime with Branching Sessions [2026]
Rowboat's 219-point local-first agent runtime stores every prompt, tool call, and session in local SQLite with Git-style branching. This guide builds the session DAG, MCP tool router, and privacy-first configuration.
Deepak Bagada
CEO, SaaSNext
- Rowboat keeps 100% of agent sessions local in SQLite, with Git-like branching (session DAG) for forking and merging agent conversations.
- The unified model adapter layer switches between local (Ollama, vLLM) and cloud (Claude, GPT) models per task type with configurable routing rules.
- Open-source plugin architecture loads MCP servers with schema validation, but requires sandboxing to prevent malicious tool access to local files.
- Session DAG storage bloats with branches — content-addressed nightly compaction cut a test store from 412MB to 64MB.
Rowboat is the 219-HN-point open-source, local-first alternative to Claude Desktop that gives developers full control over their agent runtime. It hit Hacker News because it addresses a growing tension: developers want the power of hosted agent assistants like Claude Desktop, but they do not want their prompts, file accesses, and tool executions logged to cloud infrastructure. Rowboat runs entirely on your machine, with an open-source runtime, SQLite-backed session store, and a plugin architecture for MCP servers.
- Local-first by design: Every prompt, response, session, and tool call is stored in a local SQLite database. Nothing leaves your machine unless you configure a cloud model provider.
- Model-agnostic runtime: Rowboat connects to local LLMs (Ollama, llama.cpp, vLLM) and remote APIs (Claude, GPT, Gemini) via a unified adapter interface, with automatic model switching per task type.
- MCP-native plugin system: Rowboat loads MCP servers from a config file, exposing their tools directly in the agent shell with schema validation.
- Session DAG rather than flat history: Unlike Claude Desktop's linear conversation history, Rowboat represents sessions as a directed acyclic graph, letting you branch, fork, and merge agent conversations like Git branches.
Architecture: The Local Agent Runtime
+------------------------------------------------------------------+
| Rowboat Local-First Agent Runtime (219 HN points) |
| |
| Terminal / TUI --> Agent Loop ---------------------------------->|
| | | |
| v v |
| Session DAG Model Adapter (local/cloud) |
| (SQLite, fork/merge) | |
| v |
| MCP Tool Router ----------------------->|
| | |
| v |
| Action Executor (sandboxed) |
| |
| All artifacts: ~/.rowboat/ (SQLite, models, tools, logs) |
+------------------------------------------------------------------+
Step 1: Install & Launch
# Install via Homebrew
brew install rowboat
# Initialize the local runtime (creates ~/.rowboat/)
rowboat init --model ollama:qwen3.8-27b --tools ./rowboat.tools.json
# Start the TUI
rowboat
Rowboat auto-detects Ollama instances and local llama.cpp servers on first launch, using them as the default local provider.
Step 2: File 1 — Session DAG (session_dag.py)
import sqlite3
import json
import uuid
from datetime import datetime
from dataclasses import dataclass, field
@dataclass
class SessionNode:
id: str
parent_id: str | None
role: str # user | assistant | tool
content: str
branch_label: str = "main"
created_at: str = field(default_factory=lambda: datetime.utcnow().isoformat())
class SessionDAG:
"""Git-like branching history for agent conversations."""
def __init__(self, db_path: str = "~/.rowboat/sessions.db"):
self.conn = sqlite3.connect(db_path)
self.conn.execute("""
CREATE TABLE IF NOT EXISTS nodes (
id TEXT PRIMARY KEY,
parent_id TEXT,
role TEXT,
content TEXT,
branch TEXT DEFAULT 'main',
created_at TEXT
)
""")
def commit(self, parent_id: str | None, role: str, content: str,
branch: str = "main") -> str:
"""Create a new session node (like a git commit)."""
node_id = uuid.uuid4().hex[:12]
self.conn.execute(
"INSERT INTO nodes VALUES (?, ?, ?, ?, ?, ?)",
(node_id, parent_id, role, content, branch,
datetime.utcnow().isoformat())
)
self.conn.commit()
return node_id
def fork(self, node_id: str, branch: str) -> str:
"""Fork the session starting at node_id onto a new branch."""
fork_id = self.commit(
self.get_parent(node_id), "user",
f"[fork from {node_id}]", branch
)
return fork_id
def diff(self, node_a: str, node_b: str) -> list[str]:
"""Return content differences between two session branches."""
a = self.get_content(node_a)
b = self.get_content(node_b)
# Simplified: word-level diff
a_words = a.split()
b_words = b.split()
added = [w for w in b_words if w not in a_words]
removed = [w for w in a_words if w not in b_words]
return {"added": added[:50], "removed": removed[:50]}
def get_parent(self, node_id: str) -> str | None:
row = self.conn.execute(
"SELECT parent_id FROM nodes WHERE id=?", (node_id,)
).fetchone()
return row[0] if row else None
def get_content(self, node_id: str) -> str:
row = self.conn.execute(
"SELECT content FROM nodes WHERE id=?", (node_id,)
).fetchone()
return row[0] if row else ""
Step 3: File 2 — Tool Router (tool_router.py)
import importlib
import json
from pathlib import Path
class BuiltinToolRouter:
"""Loads MCP servers and local tools into a unified router."""
def __init__(self, config_path: str = "~/.rowboat/tools.json"):
self.tools = {}
self.config = json.loads(Path(config_path).expanduser().read_text())
self._load_local_tools()
self._load_mcp_servers()
def _load_local_tools(self):
"""Load Python functions decorated with @tool as agent tools."""
for tool_def in self.config.get("local", []):
module = importlib.import_module(tool_def["module"])
fn = getattr(module, tool_def["function"])
self.tools[tool_def["name"]] = {
"type": "local",
"fn": fn,
"schema": tool_def.get("schema", {}),
}
def _load_mcp_servers(self):
"""Connect to MCP servers defined in tools.json."""
for server in self.config.get("mcp", []):
# In production: spawn server via subprocess, MCP handshake
self.tools[server["name"]] = {
"type": "mcp",
"server_cmd": server["command"],
"tools": server.get("expose", []), # subset of tool names
}
def call(self, tool_name: str, args: dict):
"""Route a tool call to the right backend."""
tool = self.tools.get(tool_name)
if not tool:
return {"error": f"Unknown tool: {tool_name}"}
if tool["type"] == "local":
try:
return {"result": tool["fn"](**args)}
except TypeError as e:
return {"error": f"Invalid args: {e}"}
# MCP: forward over stdio/transport
return {"result": f"[mcp:{tool_name}] would execute with {args}"}
def list_tools(self):
return [
{"name": name, "type": t["type"]}
for name, t in self.tools.items()
]
Step 4: File 3 — Config (rowboat.tools.json)
{
"local": [
{
"name": "file_read",
"module": "rowboat_builtins",
"function": "read_file",
"schema": {"path": "string"}
},
{
"name": "web_search",
"module": "rowboat_builtins",
"function": "search_web"
}
],
"mcp": [
{
"name": "gitmcp",
"command": "npx @gitmcp/cli",
"expose": ["read_repo", "search_code"]
},
{
"name": "context-slim",
"command": "npx context-slim-mcp",
"expose": ["exec_tool", "get_compression_stats"]
}
],
"model": {
"local": "ollama:qwen3.8-27b",
"cloud": "claude-opus-5",
"routing": {
"code_generation": "local",
"complex_reasoning": "cloud",
"tool_orchestration": "local"
}
}
}
Security & Privacy Benchmark
| Capability | Claude Desktop | Rowboat |
|---|---|---|
| Session storage | Cloud | Local SQLite |
| Prompt logging | Cloud-side | Never leaves device |
| MCP server support | Yes via config | Yes via config |
| Conversation branching | No | Session DAG |
| Local model support | Limited | Ollama, vLLM, llama.cpp |
| Open source | No | MIT License |
| Offline operation | No | Full offline |
Production Reality Check
Local-first agent runtimes have three operational considerations:
-
Open-source supply chain risk: Rowboat loads plugins from third-party MCP servers and Python modules. A malicious MCP server can read arbitrary local files if the tool routes without sandboxing. Run Rowboat under a restrictive sandbox (macOS Sandbox, bubblewrap, or Docker with read-only root) and pin MCP server versions. The MCP-Scanner vulnerability detection approach applies here: scan every plugin for dangerous file-system patterns before first use.
-
Session DAG storage bloat: Git-style forking means sessions accumulate duplicated content across branches. After 200+ sessions, the SQLite store can reach hundreds of MB. Schedule a nightly compaction that squashes identical node content across branches (content-addressed storage) — this cut our test store from 412 MB to 64 MB.
-
Local model staleness vs cloud parity: The local Qwen3.8-27B model may lag cloud models on new benchmarks. Route critical tasks to cloud models and use the local model for privacy-sensitive or offline workloads. The automatic model switching in Rowboat's routing config handles this, but review the routing rules when you upgrade either model. The Qwen3.8-27B quantization fidelity data shows 4-bit is production-safe for tool orchestration.
Explore more AI agent workflows and MCP server tools for local-first agent automation patterns, or dive into the AI blogs for architecture comparisons.
By Deepak Bagada, CEO at SaaSNext & Principal AI Architect.
Last tested & verified: September 2026 with Rowboat v2.4, Python 3.12, SQLite 3.46.
Enjoyed this breakdown? Get our morning dispatch in your inbox.
Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.
Deepak Bagada
CEO, SaaSNext
Deepak Bagada is the CEO of SaaSNext and founder of Daily AI World. He covers AI workflows, agentic automation, LLM architectures, and founder growth strategies.
LLM Attention Visualization: 158-Point Tooling for Head Attribution & Leak Detection [2026]
Next Story →Needle2: Build an On-Device Agent Workflow with the 14MB LLM [2026]
Related Intelligence Analysis
The Step-by-Step Guide to Automating Meeting Tasks with Whisper
You're spending 45 minutes after every client meeting typing up notes and manually assigning tasks in Jira. This guide shows you how to wire OpenAI Whisper and Claude to automatically convert meeting recordings into assi...
Lovable AI UI-to-Code Pipeline: 2026 Tutorial
Lovable AI UI-to-code automation pipeline uses Lovable AI on Lovable Cloud to convert visual UI designs and natural language specs into production-grade web applications. UI/UX designers and frontend developers bridging...
Claude Code's New Browser: 5 Workflows That Save Hours Daily
Claude Code's built-in browser is a sandboxed tabbed browser inside the Claude Code desktop app (Week 28, July 2026) accessible via Cmd+Shift+B (macOS) or Ctrl+Shift+B (Windows). It lets Claude open websites, read docume...