Persistent workspace for AI coding agents. Run Claude Code (or any long-lived shell process) on a server, close the browser, hop devices — and come back hours later to find your agent, your build, your
vimexactly where you left them. tmux on the inside, a real UI on the outside.
Quick Start • Usage Guide • Deployment • Architecture • Troubleshooting
Desktop — workspaces, tab bar, and a tmux-backed terminal panel.
Mobile — collapsible sidebar, IME-safe input bar, on-screen modifier keys.
I run Claude Code (and other AI coding agents) on a remote dev box. The work is long: multi-step refactors, slow builds, agent loops that chew on a problem for an hour. But every time I closed the browser tab, walked away from my desk, or jumped to my phone, the workflow broke — the SSH session died, the scrollback evaporated, and whatever the agent was in the middle of got yanked out from under it.
So I wrapped tmux in a real browser UX. Mansio gives the agent a persistent home — a workspace that doesn't care whether I'm at my desk, on my phone, or rebooting the laptop. Close the tab. Walk to a meeting. Reopen on a different device. The agent is still running, the scrollback is intact, the prompt is exactly where I left it.
That's the whole pitch: a place your agent can live, not just a session it borrows.
- Self-hosted SSH replacement in the browser — no client install, just a URL.
- Survives everything — close the browser, restart the server, switch networks; your shell, your
vim, your long-runningnpm run buildall stay alive thanks totmux. - Single binary — ~10 MB Go binary with the React frontend embedded. The only runtime dependency is
tmux. - Works on your phone — tap-friendly UI, dedicated mobile input bar, IME-safe (Korean / Chinese / Japanese).
- Two deployment modes — native install for full host access (SSH-equivalent), or Docker for an isolated playground.
git clone https://github.com/Younkyum/Mansio.git
cd Mansio
# Linux
sudo bash deploy/install.sh
# macOS
bash deploy/install.shOpen http://localhost:8080, set a password on the first screen, and you're done.
Want to try it without touching your host? Use Docker instead:
docker compose up -d --build
This section walks through everything you'll do day-to-day. Skip to the part you need.
When you open http://localhost:8080 for the first time, Mansio shows a setup screen asking you to create a password. This is stored as a bcrypt hash on the server and unlocks future logins via an HttpOnly session cookie (7-day expiry).
Native install: the password protects SSH-equivalent access to your host. Use a strong one. Docker mode: the container is isolated, but the password still protects whatever you mount into it.
After login you see the main UI:
┌──────────────┬──────────────────────────────────────────┐
│ Workspaces │ ┌──┬──┬──┬──┐ │
│ ───────── │ │T1│T2│T3│ +│ tab bar │
│ ▸ default │ └──┴──┴──┴──┘ │
│ work │ │
│ server │ $ ls │
│ │ README.md go.mod internal/ │
│ │ $ █ │
│ [☀ ☾ ⚙] │ │
└──────────────┴──────────────────────────────────────────┘
Sidebar Terminal panel
Workspaces are top-level groups (left sidebar). Each workspace holds one or more tabs (top of the terminal panel). Every tab is its own persistent tmux session.
| Action | How |
|---|---|
| Create workspace | Click + at the top of the sidebar |
| Rename / delete workspace | Right-click (long-press on mobile) the workspace name → context menu |
| Switch workspace | Click another workspace in the sidebar — instant, no re-fit, scrollback preserved |
| Create tab | Click + at the right end of the tab bar |
| Rename / delete tab | Right-click (long-press on mobile) the tab → context menu |
| Switch tab | Click another tab in the tab bar |
The sidebar shows each workspace's last-active terminal's CWD as a subtitle, polled every 5 s while the page is visible. Useful for picking the right project at a glance.
Instant switching: every open terminal stays mounted in the background (VS Code-style detach/attach). Switching workspaces is immediate, never re-fits a hidden terminal to 0×0, and never drops scrollback.
Every tab is backed by a tmux session named lt_<id>. That means:
- Close the browser tab → processes keep running in tmux.
- Reopen the browser → reattach with full scrollback restored.
- Server restart (native install) → tmux survives the Go process; reattach as if nothing happened.
- Container restart (Docker) → tmux dies with the container; tabs reload empty (the metadata persists, but the running processes don't).
- Delete a tab →
tmux kill-sessionruns and the processes are terminated.
A long-running build, a vim session, an htop, an interactive REPL — leave them, walk away, come back.
| Input | Effect |
|---|---|
| Shift + Enter | Send a literal newline without submitting (helpful for multi-line input in REPLs and AI CLIs that submit on plain Enter) |
| Mouse wheel | Scroll terminal scrollback (tmux mouse mode is on by default) |
| Click + drag | Select text natively in the terminal |
| Right-click on tab/workspace | Context menu (rename / delete) |
| Drag handle between sidebar and terminal | Resize the sidebar (140–400 px) |
Copy / paste: most browsers honor
Cmd+C/Ctrl+Cfor selected text;Cmd+V/Ctrl+Vpastes at the prompt. If the shell is doing something withCtrl+C(sending SIGINT), select first, then use the system menu to copy.
Drop a file (or several) onto the terminal pane:
- The file is POSTed to
/api/v1/sessions/:id/uploadasmultipart/form-data. - It's saved under
~/uploads/(or the equivalent inside the Docker container) with collision-safe naming. - The resulting absolute path is automatically pasted at your prompt, ready for the next command.
$ █
[drag-drop image.png]
$ /home/mansio/uploads/image.png█
[now type whatever:]
$ python process.py /home/mansio/uploads/image.png
Default cap: 100 MiB per upload. Path traversal and NUL bytes are rejected server-side.
Bottom of the sidebar has three icons:
- ☀ Light — pin to light mode
- ☾ Dark — pin to dark mode
- ⚙ System — follow OS preference (default)
Both UI and xterm.js palettes are tuned for ≥4.5:1 WCAG contrast and verified by theme.test.ts.
On narrow screens (<640 px wide):
- The sidebar collapses behind a hamburger button at the top of the tab bar.
- Tap the hamburger → sidebar slides in from the left with a backdrop overlay.
- A dedicated mobile input bar appears below the terminal. This bypasses xterm.js's hidden textarea so IME (Korean / Chinese / Japanese composition) and keyboard suggestions work correctly.
- All tap targets are at least 44 × 44 px.
- iOS focus-zoom is suppressed (the input uses 16 px font + scale tricks) so the page doesn't zoom in on every focus.
Click the user / power icon in the sidebar (or hit /api/v1/auth/logout directly). The server-side session is invalidated immediately; the cookie is cleared.
Mansio has two deployment modes. Pick one:
| Native Install | Docker | |
|---|---|---|
| Access level | Full host (SSH-equivalent) | Isolated container |
| tmux survives server restart | ✅ Yes | ❌ No (dies with container) |
| Best for | Personal dev box, home server | Sandbox, demos, untrusted use |
| Disk footprint | ~10 MB binary + tmux | ~1 GB image (Ubuntu + Node + Python) |
The web terminal will have the same access as logging into the machine directly — same files, same tools, same environment.
Prerequisites: Go 1.26+, Node.js 20+, npm, tmux, git
git clone https://github.com/Younkyum/Mansio.git
cd Mansio
# Linux
sudo bash deploy/install.sh
# macOS (no sudo on the script itself; it sudo's internally for /usr/local/bin)
bash deploy/install.shThe installer detects the OS, builds from source, installs the binary to /usr/local/bin/mansio, and registers a service.
Installer flags:
| Flag | Description | Default |
|---|---|---|
--host HOST |
Server host | 127.0.0.1 |
--port PORT |
Server port | 8080 |
--data-dir DIR |
SQLite database directory | Linux: /var/lib/mansio, macOS: ~/.local/share/mansio |
--user USER |
System user to run as | current user |
--help |
Show help | — |
# Status / restart / logs
systemctl status mansio@$(whoami)
systemctl restart mansio@$(whoami)
journalctl -u mansio@$(whoami) -f
# Custom host/port/data dir
sudo bash deploy/install.sh --host 127.0.0.1 --port 3000 --data-dir /var/lib/mansio
# Uninstall (keeps data dir)
sudo bash deploy/uninstall.sh
# Wipe data too
sudo rm -rf /var/lib/mansioData dir: /var/lib/mansio · Service unit: /etc/systemd/system/mansio@.service
launchctl list | grep mansio # status
launchctl stop com.mansio.mansio # stop
launchctl start com.mansio.mansio # start
tail -f ~/Library/Logs/mansio/stdout.log # logs
# Uninstall (keeps data dir + logs)
bash deploy/uninstall.shData dir: ~/.local/share/mansio · Logs: ~/Library/Logs/mansio/ · plist: ~/Library/LaunchAgents/com.mansio.mansio.plist
macOS Full Disk Access: macOS sandboxes access to
~/Documents,~/Desktop, etc. On first launch Mansio hits/api/v1/health, and if those directories are unreadable, the web UI shows a full-screen modal with step-by-step instructions: System Settings → Privacy & Security → Full Disk Access → add/usr/local/bin/mansio. The installer also opens System Settings to the right pane automatically.
Works out of the box. Keep Mansio bound to loopback and point your tunnel at http://localhost:8080 — Cloudflare handles HTTPS and WebSocket proxying automatically.
cloudflared tunnel --url http://localhost:8080For a permanent tunnel, follow Cloudflare's named-tunnel docs and route a hostname to http://localhost:8080.
Runs in an isolated Ubuntu 24.04 container preloaded with Node.js 20, Python 3, build-essential, zsh, git, tmux, and CJK fonts. The home directory persists via a Docker volume.
git clone https://github.com/Younkyum/Mansio.git
cd Mansio
docker compose up -d --build
# Open http://localhost:8080The compose file publishes 127.0.0.1:8080 by default. Set MANSIO_PORT=3000 to change the host port.
Persists across container restarts:
/home/mansio→ installed tools, project files, shell configs (volumemansio-home)/data→ workspace/session metadata (volumemansio-data)
Does NOT persist:
- tmux sessions (running processes) — killed when the container restarts
- System packages installed via
apt— bake them into theDockerfileto make them permanent
Common operations:
docker compose logs -f # follow logs
docker compose restart # restart (loses tmux)
docker compose down # stop + remove (keeps volumes)
docker compose down -v # stop + remove + WIPE all data
docker compose exec mansio bash # shell into the container| Flag | Description | Default |
|---|---|---|
--host |
Server host | 127.0.0.1 |
--port |
Server port | 8080 |
--data-dir |
SQLite database directory | ./data |
Run directly:
./mansio --host 127.0.0.1 --port 9000 --data-dir /tmp/mansio-dataUpgrading from earlier builds: the server now binds to
127.0.0.1by default instead of all interfaces. Pass--host 0.0.0.0if you need external access (and front it with HTTPS). Env vars previously prefixedLOCITERM_*are nowMANSIO_*(e.g.MANSIO_PORT,MANSIO_HOST,MANSIO_DATA_DIR).
| Symptom | Cause / Fix |
|---|---|
| Web UI shows "Permission Required" modal (macOS) | Add /usr/local/bin/mansio to System Settings → Privacy & Security → Full Disk Access. Click "I've fixed it — Check again". |
systemctl status mansio@<user> shows failure |
journalctl -u mansio@<user> -e for the actual error. Common: port 8080 already taken — reinstall with --port. |
| Tabs are empty after a Docker restart | Expected — tmux dies with the container. Use native install if you need tmux to survive restarts. |
| Can't paste / clipboard blocked | Browser permission. Some browsers require HTTPS for the Clipboard API; front the server with Cloudflare Tunnel. |
| iOS keyboard zooms in on focus | Already mitigated (16 px font + scale). If you still see it, hard-refresh the page; old build cached. |
| CJK characters render as boxes | Native install: install a CJK font on your OS. Docker: already includes fonts-noto-cjk. |
| "WebSocket connection failed" | Check that your reverse proxy forwards Upgrade / Connection headers. Cloudflare Tunnel does this by default. |
| Forgot password | Native: stop the service, delete the password row from <data-dir>/mansio.db (or just delete the DB and lose all metadata), restart. Docker: docker compose down -v && docker compose up -d --build. |
Browser Go server (single binary)
┌─────────────────────┐ ┌──────────────────────────────────┐
│ React + xterm.js │ │ net/http ServeMux │
│ │ │ │
│ Sidebar ──REST──────────────────> /api/v1/workspaces │
│ TabBar ──REST──────────────────> /api/v1/sessions │
│ Drop ──multipart─────────────> /api/v1/sessions/:id/upload │
│ xterm.js ═══WS══════════════════> /api/v1/ws/terminal/:id │
│ binary frames (I/O) │ │ ├── tmux.Manager │
│ JSON (control) │ │ │ └── tmux sessions (persist)│
│ │ │ └── store (SQLite) │
└──────────────────────┘ └──────────────────────────────────┘
| Layer | Technology |
|---|---|
| Frontend | React 19, TypeScript, xterm.js, Zustand, Vite |
| Backend | Go (stdlib net/http), gorilla/websocket, creack/pty |
| Persistence | tmux (sessions), SQLite via modernc.org/sqlite (metadata) |
| Auth | bcrypt + HttpOnly session cookie (7-day expiry) |
| Deploy | systemd (Linux) · launchd (macOS) · Docker multi-stage build (Ubuntu 24.04) |
1. Tab created → tmux new-session -d -s lt_{id} -c $HOME
2. Browser opens → creack/pty spawns "tmux attach -t lt_{id}"
PTY fd is bridged to WebSocket (binary frames)
3. Browser closes → PTY (attach process) terminates
tmux session keeps running in the background
4. Reconnect → new "tmux attach" → scrollback + processes restored
5. Tab deleted → tmux kill-session -t lt_{id}
The tmux server runs independently from the Go process. Even if the Go server crashes or restarts, tmux sessions survive (native install only — Docker containers lose tmux sessions on restart).
Two frame types over a single connection:
| Direction | Type | Content |
|---|---|---|
| Client → Server | Binary | Terminal stdin (keystrokes) |
| Server → Client | Binary | Terminal stdout (output) |
| Client → Server | Text (JSON) | { type: "resize", cols, rows } |
| Server → Client | Text (JSON) | { type: "attached" }, { type: "pong" } |
Binary frames carry raw terminal I/O with zero encoding overhead.
GET /api/v1/health # Liveness + macOS permission status
POST /api/v1/auth/setup # First-run password setup
POST /api/v1/auth/login # Login
POST /api/v1/auth/logout # Logout
GET /api/v1/auth/check # Check auth state
GET /api/v1/workspaces # List workspaces
POST /api/v1/workspaces # Create workspace
PATCH /api/v1/workspaces/:id # Rename workspace
DELETE /api/v1/workspaces/:id # Delete workspace (cascades sessions + tmux)
GET /api/v1/workspaces/:wid/sessions # List sessions
POST /api/v1/workspaces/:wid/sessions # Create session
PATCH /api/v1/sessions/:id # Rename session
DELETE /api/v1/sessions/:id # Delete session (kills tmux)
POST /api/v1/sessions/:id/upload # multipart/form-data file upload
GET /api/v1/ws/terminal/:sessionId # WebSocket terminal
mansio/
├── cmd/mansio/main.go # Entrypoint, embed.FS, graceful shutdown
├── internal/
│ ├── server/ # HTTP routing, auth middleware, /health
│ ├── api/ # REST handlers (workspace, session, auth, upload)
│ ├── ws/ # WebSocket upgrade + PTY bridge
│ ├── tmux/ # tmux session lifecycle management
│ ├── store/ # SQLite persistence + migrations
│ └── model/ # Data structs
├── frontend/src/
│ ├── components/
│ │ ├── Auth/LoginForm.tsx # Login / setup form
│ │ ├── Sidebar/Sidebar.tsx # Workspace list + theme toggle + context menu
│ │ └── Terminal/ # TabBar, TerminalPanel, TerminalView, MobileInputBar
│ ├── hooks/
│ │ ├── useTerminal.ts # xterm.js + WebSocket lifecycle
│ │ ├── useEffectiveTheme.ts # system/light/dark resolver
│ │ ├── useMediaQuery.ts # Mobile breakpoint detector
│ │ └── shiftEnter.ts # Shift+Enter → literal newline
│ ├── stores/
│ │ ├── appStore.ts # Zustand: workspaces/sessions/active
│ │ └── themeStore.ts # Persisted theme mode
│ ├── api/upload.ts # Multipart upload client
│ └── lib/
│ ├── theme.ts # Light + dark UI palettes & xterm themes
│ └── contrast.ts # WCAG contrast helper (used by tests)
├── deploy/
│ ├── install.sh # Cross-platform installer (Linux+macOS)
│ ├── uninstall.sh # Cross-platform uninstaller
│ └── mansio.service # systemd unit template (Linux)
├── Dockerfile # Multi-stage build (Ubuntu 24.04 runtime)
├── docker-compose.yml # Docker deployment with persistent volumes
└── Makefile
# Tests
make test # Run all tests (Go + frontend)
make test-go # Go tests only
make test-frontend # Frontend tests only
# Dev mode (two terminals)
make dev-backend # Terminal 1: Go server on :8080
make dev-frontend # Terminal 2: Vite dev server with proxy
# Build a single self-contained binary
make build # → ./mansio
# Clean build artifacts
make cleanThe Vite dev server proxies API + WebSocket calls to localhost:8080, so you get hot-reload on the frontend while the Go backend keeps running.
| Decision | Rationale |
|---|---|
Go stdlib net/http |
~14 endpoints. Go 1.22+ ServeMux handles method+path routing natively — no router dependency. |
| modernc.org/sqlite | Pure Go, no CGo. Static binary, easy cross-compilation. |
| tmux for persistence | Sessions survive browser close AND server restart. Independent process. |
| Binary WebSocket frames | Zero encoding overhead. Critical for high-throughput terminal output. |
| HttpOnly session cookie (not JWT) | Simpler and revocable for single-user self-hosting. |
| Per-effective-theme xterm palette | Light/dark themes verified against ≥4.5:1 contrast in theme.test.ts. |
| Ubuntu 24.04 (Docker) | glibc-based for tool compatibility (Node.js, AI CLIs, etc.). |
| Dedicated mobile input bar | xterm.js's hidden textarea breaks IME composition on mobile keyboards; a real <textarea> is the cleanest fix. |
- Native install grants the same access level as SSH — use a strong password.
- Always front the server with HTTPS in production (Cloudflare Tunnel recommended for the easiest path).
- Restrict port access via firewall or VPN whenever possible.
- Docker mode provides isolation — host files outside the mounted volumes are not accessible.
- Uploads are sanitized (no path traversal, no NUL bytes) and capped at 100 MiB per upload.
- Sessions expire after 7 days; logout invalidates immediately.
- Password is bcrypt-hashed at cost 10; the plaintext is never stored or logged.
- Code Review panel (git diff viewer)
- Multi-user support (per-user workspace isolation)
- Tab drag-to-reorder
- Terminal scrollback search (Ctrl+Shift+F)
- Terminal split panes (horizontal / vertical within a tab)
- Custom theme presets
- Built-in HTTPS/TLS support (Let's Encrypt or self-signed)
- OAuth login (GitHub, Google)
- 2FA (TOTP)
See TODO.md for the full backlog.
GPL-3.0-or-later — see LICENSE for the full text.
Mansio is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This is a copyleft license: any fork, redistribution, or modified version you publish must also be licensed under GPL-3.0-or-later and ship its source code. For third-party components bundled with Mansio, see THIRD_PARTY_LICENSES.md.