Connect independent AI agents on demand — locally, in your browser.
AgentHub is a lightweight localhost hub for running multiple AI agents (Claude Code, Cursor, Codex, terminal tools) in parallel without permanent coupling. Agents join when they want, leave when they want, and you wire them together from a web UI only when coordination is needed.
Also includes a read-only workspace viewer for markdown and code — so you can read docs without juggling tmux panes.
| Stack | Rust (Axum) + React (Vite) |
| Scope | Localhost only — no cloud, no accounts |
| License | MIT |
Running several agents at once usually means isolated terminals. When two agents need to talk, you copy-paste context or write glue scripts. Reading project docs from tmux is painful.
AgentHub gives you one process on your machine that:
- Registers agents over WebSocket as they connect and disconnect
- Links agents on demand — create a channel between two agents from the UI; tear it down when done
- Routes messages only across active links (or broadcast with
*) - Browses your workspace — file tree + markdown/code viewer in the same UI
Inspired by tools like Maestri, but deliberately simpler: manual connections, no workflow DSL, no orchestration engine.
| Feature | Status |
|---|---|
| WebSocket hub + agent registry | ✅ |
| Message broker with connection edges | ✅ |
| Web UI — agent panel, link agents, live log | ✅ |
agenthub-connect terminal CLI |
✅ |
| Workspace file tree + doc viewer | ✅ |
| Path traversal protection on file API | ✅ |
| Native desktop app (Tauri) + one-line installer | ✅ |
| Canvas: terminal nodes, 8-way resize, pan/zoom-to-cursor, group boxes | ✅ |
| tmux-style prefix shortcuts + direct shortcuts + in-app help | ✅ |
| Terminal panes auto-close on exit, splits collapse cleanly | ✅ |
| Accented/dead-key input fix (Tauri WebKitGTK + WKWebView) | ✅ |
| Desktop window runs on Chromium, not WebKitGTK (see Desktop window engine) | ✅ |
| MCP adapter (Claude Code) | 🔜 |
| Terminal wrapper improvements | 🔜 |
Fastest path — no toolchain needed. Debian/Ubuntu, one line:
curl -fsSL https://raw.githubusercontent.com/NicolasHubner/agenthub/main/install.sh | bashIt pulls the latest .deb from Releases, installs tmux + WebKit runtime deps, ensures a Chromium-based browser is present, and adds AgentHub to your app menu.
On other distros (or to avoid apt), grab the portable .AppImage from the same Releases page — chmod +x and run it.
The desktop app is built with Tauri. The Rust backend is bundled as a sidecar, so there is no separate server to start — launching AgentHub starts the hub and opens its window, and closing the window shuts the hub down.
The window is a Chromium app-mode window (--app, with its own browser profile under ~/.local/share/com.agenthub.desktop/browser-profile), not the Tauri WebKit webview.
WebKitGTK — the only webview Tauri has on Linux — starves requestAnimationFrame while the window is idle: it runs frame callbacks only in response to input. xterm.js schedules every repaint through rAF, so PTY output stayed unpainted until the next keystroke woke the compositor, and the terminal rendered exactly one keystroke behind (typing ssa showed ss). The cursor never blinked either. Swapping the xterm renderer and shimming rAF inside the page both failed to fix it.
AgentHub looks for google-chrome, google-chrome-stable, chromium, chromium-browser, brave-browser, microsoft-edge, then vivaldi-stable on PATH. Override with AGENTHUB_BROWSER=/path/to/browser. If none is found it falls back to the WebKit webview and logs a warning — the app still runs, with the rendering lag above.
Two consequences of running outside the Tauri webview: desktop notifications use the browser Notification API instead of the Tauri plugin, and image paste always goes through the server-side /paste-image route rather than the native clipboard bridge.
You need three things installed: Rust (cargo), Node.js 18+ (npm), and git.
Copy-paste for your OS:
# Linux (Debian/Ubuntu)
sudo apt-get update && sudo apt-get install -y git curl build-essential
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs# macOS (Homebrew)
brew install git node rustup-init && rustup-init -y# Windows (winget)
winget install Git.Git OpenJS.NodeJS Rustlang.RustupAfter installing, restart your shell so cargo and node are on PATH. Verify:
cargo --version && node --version && npm --version && git --versionThe launcher builds the UI + backend if needed, starts the server, and opens your browser.
# Linux / macOS
./scripts/agenthub-start.sh# Windows (PowerShell)
./scripts/agenthub-start.ps1On Windows you can also double-click scripts\agenthub-start.cmd.
This opens AgentHub in your default browser (port 3070 by default, override with AGENTHUB_PORT). The Rust backend must be running for terminals to work; the launcher above keeps it running. For a native window instead of a browser tab, use the desktop app (below).
Prefer a packaged download? See Install (desktop app) above. Maintainers cut a release with
./release.sh [version]— it runs./desktop.shto build the Tauri bundles (.deb+.AppImage) and publishes/updates the matching GitHub Release.
cd ui && npm install && npm run build && cd ..
AGENTHUB_WORKSPACE=. cargo runOpen http://127.0.0.1:8008. The Agents tab is the main view; Files is the doc viewer.
On the canvas, press the prefix Ctrl-b, then a command key:
| Key | Action |
|---|---|
c |
New terminal |
x |
Close focused terminal |
n / p |
Cycle to next / previous terminal |
← ↑ ↓ → |
Move focus to the terminal in that direction |
z |
Zoom (center + fit) the focused terminal |
0–9 |
Jump to terminal N |
Direct (no prefix needed):
| Key | Action |
|---|---|
Ctrl-Shift-w |
Close focused terminal |
Alt-t |
New terminal |
Alt-z |
Zoom focused terminal |
Alt-n / Alt-p |
Next / previous terminal |
Alt-Tab |
Cycle terminals |
Alt + ← ↑ ↓ → |
Move focus |
Other canvas controls: Space + drag to pan, Alt + scroll to zoom. Click the keyboard icon (bottom of the canvas) for an in-app shortcuts reference.
With the hub running:
# Terminal 1 — first agent
cargo run --bin agenthub-connect -- --name claude --tag claude
# Terminal 2 — second agent (default send target: claude)
cargo run --bin agenthub-connect -- --name cursor --tag cursor --to claudeIn the browser: select claude and cursor → click Connect. Type in either terminal; messages flow only when the link exists.
Use --to '*' to broadcast to every connected agent (no edge required).
cargo install --path . --bin agenthub-connect
agenthub-connect --name my-agent --tag terminal| Variable | Default | Description |
|---|---|---|
AGENTHUB_WORKSPACE |
. |
Workspace root for the read-only file viewer |
AGENTHUB_UI_DIR |
ui/dist |
Path to the built frontend |
AGENTHUB_PORT |
8008 |
HTTP listen port (binds 127.0.0.1 only) |
HTTP and WebSocket share the same port. WebSocket endpoint: ws://127.0.0.1:8008/ws.
| Flag | Description |
|---|---|
--name, -n |
Agent display name (default: agent) |
--hub, -u |
Hub WebSocket URL (default: ws://127.0.0.1:8008/ws) |
--to, -t |
Default recipient for stdin lines (default: * = broadcast) |
--tag |
Tag label (repeatable) |
Stdin lines become hub messages; incoming messages print to stdout as [from] content.
JSON messages over /ws. All client messages use a type field.
register— join the hub as an agent (required for message delivery).subscribe— UI clients only; receive state updates and message log.msg— send toto(agent name or*for broadcast). Delivered only if an edge exists (except broadcast).connect/disconnect— add or remove a bidirectional link between two agents.
{ "type": "state", "agents": [...], "edges": [["a","b"]] }
{ "type": "msg", "from": "claude", "to": "cursor", "content": "hello" }
{ "type": "error", "reason": "no route to agent", "to": "cursor" }GET /state returns the same snapshot as the latest state event (REST fallback).
Read-only, scoped to AGENTHUB_WORKSPACE.
| Endpoint | Description |
|---|---|
GET /state |
Current agents and edges |
GET /files |
Sorted list of relative file paths |
GET /file?path=<rel> |
File content + metadata (kind: markdown, code, text) |
Files over 2 MiB or non–UTF-8 content are rejected. Paths escaping the workspace root (.., symlinks, absolute paths) return 403.
┌──────────────────────────────────────────────────┐
│ AgentHub (Rust) │
│ │
│ :8008 HTTP ──► React UI (static) │
│ ├──► GET /state, /files, /file │
│ └──► WS /ws (agents + browser) │
│ │
│ Registry ── agents online (DashMap) │
│ Broker ── per-agent delivery queues │
│ Edges ── manual links between agent pairs │
│ Event log── ring buffer (last 1000 messages) │
└──────────────────────────────────────────────────┘
▲ ▲ ▲
│ WS │ WS │ browser
agenthub-connect agenthub-connect (UI)
| Path | Role |
|---|---|
src/main.rs |
Server entrypoint |
src/hub.rs |
WebSocket hub, registry, broker, edges |
src/protocol.rs |
JSON message types |
src/workspace.rs |
Safe file listing and reading |
src/bin/connect.rs |
Terminal agent CLI |
ui/ |
React frontend |
Full design spec: docs/superpowers/specs/2026-06-16-agenthub-design.md.
# Backend tests
cargo test
# Frontend (separate terminal; hub must be running for API)
cd ui && npm install
npm run dev # Vite on :5173, proxies /files and /file
npm test
npm run build # → ui/dist
# Desktop app bundle (needs the Tauri CLI: cargo install tauri-cli)
./desktop.sh # builds backend + UI, then .deb + .AppImage into src-tauri/target/release/bundleDoc viewer✅WebSocket hub + agent registry✅UI agent panel + manual linking✅Canvas: terminal nodes, pan/zoom, group boxes✅Native desktop app + one-line installer✅- Node graph drag-to-connect edges 🔜
- MCP adapter for Claude Code 🔜
Non-goals: remote access, authentication, durable storage, automatic orchestration, in-browser file editing.
Issues and pull requests are welcome. For larger changes, open an issue first.
- Fork the repo
git checkout -b feat/my-changecargo testandcd ui && npm test- Open a PR with a clear description
MIT — Copyright (c) 2026 Nicolas Hubner

{ "type": "register", "name": "claude", "tags": ["claude"] } { "type": "subscribe" } { "type": "msg", "to": "cursor", "content": "hello" } { "type": "connect", "a": "claude", "b": "cursor" } { "type": "disconnect", "a": "claude", "b": "cursor" }