Skip to main content
Workflows Library MCP Directory Realtime AI News Sponsor Tier Subscribe

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

Deepak Bagada

CEO, SaaSNext

Sep 09, 2026 Published
|
Sep 09, 2026 Updated
|
7 Minutes Reading Time
Core Takeaways for Founders & Builders
  • 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:

  1. 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.

  2. 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.

  3. 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.

Executive Briefing

Enjoyed this breakdown? Get our morning dispatch in your inbox.

Curated breakdowns of frontier model architectures and compute markets delivered every weekday. Zero fluff.

🎉 Thank You for Subscribing!

Frequently Asked Questions
No. Rowboat is local-first by default: all sessions, prompts, and tool calls are stored in a local SQLite database. Cloud connectivity is only used if you configure a remote model provider, and even then only the request payload is transmitted — never the session store.
A session DAG is a directed acyclic graph where each turn is a node with a parent, supporting Git-like operations: fork a conversation onto a new branch, diff two branches, or merge them back. Linear chat history forces you to re-type or scroll; the DAG lets you explore alternative agent paths without losing the original.
Local providers include Ollama, llama.cpp, vLLM, and MLX. Remote providers include Claude, GPT, Gemini, and any OpenAI-compatible API. The routing config lets you assign model classes to task types — e.g., local model for tool orchestration, cloud model for complex reasoning.
MCP servers are declared in a tools.json config with an explicit expose list (only listed tools are exposed to the agent). Rowboat recommends running under an OS sandbox or Docker with read-only root, and pinning server versions to avoid supply-chain drift. Unknown MCP servers are blocked until explicitly approved.
Deepak Bagada
Author Profile

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.

Related Intelligence Analysis

Research Breakdown AI Workflows

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...

Deepak Bagada Deepak Bagada
9m read
Research Breakdown AI Workflows

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...

Deepak Bagada Deepak Bagada
8m read
Breaking AI Workflows

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...

Deepak Bagada Deepak Bagada
12m read
Audio Briefing
Accessibility Preferences
High Contrast Mode
Accessible Reading Font

Keyboard Shortcuts

Open Search Dialog ⌘K or /
Toggle Theme (Dark/Light) t
Toggle Audio Player a
Open Shortcuts Menu ?
Close Active Dialog Esc