An AI-native browser. Agents drive. Humans look.
Surface is Chromium with one extra capability: it gives the web page running inside it a real local filesystem. It's built to be opened by an agent, not browsed by a human — you talk to your agent in whatever tool you use (Claude Code, OpenCode, ChatGPT, voice, anything), and the agent uses Surface to put windows on your screen: drafts, dashboards, files, references — pulled from disk or from the web.
You don't navigate Surface. There's no URL bar, no tab strip, no bookmarks, no history. Each window is one rendered thing. Your agent opens them; you read them.
- The runtime — Chromium, framed by Electron, with no chrome.
- The bridge —
window.surfaceplus the standard File System Access API, so any web page running inside Surface can read, write, and watch local files. - The CLI —
bin/surface <path-or-url>so any shell (and any agent's shell tool) can drive Surface. One invocation = one window. Single-instance lock means subsequent invocations join the running process. - The MCP server (
mcp/) — gives any MCP-capable AI agent thesurface_opentool. Register once, your agent has visual hands forever.
- A chat UI.
- An agent.
- A model.
- An opinion about which agent or model you use.
Bring your own.
- Download Surface.app from the latest release.
- Drag to Applications. Right-click → Open the first time (unsigned build — macOS Gatekeeper blocks unsigned apps on first launch).
- Connect your AI agent:
npx -y surface-mcp install— or ask your agent to run it.
That's it. Surface stays running in the background. Your agent opens windows when it wants to show you something.
git clone <this repo>
cd surface/app
npm install
npm startThe intended usage is from a shell — typically your agent's shell tool:
surface ~/Documents/draft.html # render in the right viewer for .html
surface ~/finance.xlsx # spreadsheet viewer (sheet app)
surface https://linear.app/abc-123 # open a URL in a window
surface --edit ~/notes/draft.html # edit-mode viewer; edits flow back to disk
surface --app=raw ~/finance.xlsx # bypass viewer routing, raw render
surface --app=sheet ~/data.txt # force a specific viewer
surface --list-apps # list installed viewers
echo '<h1>hi</h1>' | surface # pipe HTML in — auto-saved + renderedOne call = one window. No tabs. The CLI prints the URL it opened to stdout, so an agent can quote it back in chat.
Symlink the shim onto your PATH once:
ln -s ~/surface/bin/surface ~/.local/bin/surfaceSurface runs as a background daemon — one Electron process per machine, started by a LaunchAgent at login, with an embedded HTTP server on port 7878. Two Surface daemons on the same tailnet (Tailscale, plain LAN, anything that lets them reach each other) become a mesh. surface X on one machine opens a window on the other machine, with the file streamed live over HTTP — no copies, no sync.
~/.config/surface/config.json:
{
"port": 7878,
"bind": "0.0.0.0",
"self": "http://this-host:7878",
"target": "http://that-host:7878",
"rootsExposed": ["/Users/you", "/tmp"],
"peers": ["http://that-host:7878"]
}target— where this Surface sends windows by default."self"opens locally; a peer URL sends windows there.rootsExposed— paths the embedded HTTP server will serve. Anything outside is403. Defaults:~,/tmp.self— this machine's reachable URL, used when constructing the URL the target's renderer will fetch.
LaunchAgent at ~/Library/LaunchAgents/com.maxwraae.surface.plist runs surface --daemon at login with KeepAlive=true. Logs land in ~/Library/Logs/surface/. The endpoints are: GET/HEAD/PUT /<abs-path> for files, POST /_/open {url} for cross-Surface RPC, GET /_/health for liveness, GET /_/peers to list the mesh.
Auth model is "trust the tailnet" — the embedded server only serves paths under rootsExposed, but it serves them to any peer that can reach the port. If your tailnet is shared with untrusted users, this is not for you yet.
The mcp/ directory ships an MCP server (surface-mcp) that exposes one tool, surface, to any MCP-capable agent host. Register it once and every agent on your machine has visual hands — Claude Code, Claude Desktop, Cursor, OpenCode, anything that speaks MCP.
npx -y surface-mcp installRestart your agent host. The agent can now call surface(content) and a window appears. The tool dispatches on what you pass:
- A URL —
surface("https://linear.app/abc-123")opens the page. - A file path —
surface("~/Documents/draft.html")opens the file. - Raw HTML —
surface("<h1>Hello</h1>...")writes it to a temp file and renders it.
The third case is the magic one: the agent writes HTML, the user sees it. Charts, drafts, dashboards, sequence diagrams, working prototypes — anything renderable in Chromium. Multiple calls open multiple windows in the same Surface process. Temp files persist for 24 hours then auto-clean.
Full registration details for other hosts (Claude Desktop, Cursor, OpenCode) are in mcp/README.md.
Surface picks a viewer per file extension. A viewer is a folder with a surface-app.json manifest and an entry HTML page. Built-in viewers ship with Surface; users can drop new ones in ~/.surface/apps/<key>/ and they appear in surface --list-apps immediately.
~/.surface/apps/my-genbank/
├── surface-app.json # { name, extensions, preferredFor, capabilities, ... }
└── index.html # entry, reads ?file=<url>, fetches/edits via plain HTTPDiscovery order (first-match-wins per key):
~/.surface/apps/<key>/— user, XDG-style~/Library/Application Support/surface/apps/<key>/— user, macOS standard<repo>/apps/<key>/— built-in
Resolution order for which viewer renders a given file:
--app=<key>flag (per-call)~/.config/surface/config.jsonuser override (persistent)- Each app's manifest
preferredFor: [".xlsx", ...](auto-registered at boot)
--app=raw is reserved: it bypasses viewer routing and renders the file URL straight in Chromium. Useful for charts, PDFs Chromium handles natively, or whenever you want the source not the editor.
doc—.html,.htm. Generic HTML editor. Loads from a URL via plain HTTP, edits PUT back.sheet—.xlsx,.csv,.tsv. SheetJS + Handsontable. Edits round-trip to disk.
The daemon also exposes the registry over HTTP:
GET /_/apps— list installed appsGET /_/apps/by-ext/.xlsx— which app for this extension?GET /_/apps/<key>— manifest detail
A viewer is just a small web app. It receives the file URL as ?file=<url> and is expected to:
GETthe URL to loadPUTthe URL to save (debounced)HEAD-poll for external changes (1s cadence is a fine default)
The patched apps/doc/index.html and apps/sheet/index.html are reference implementations; both speak the same plain-HTTP contract, no Surface bridge required. That means a viewer also runs in a normal browser if you want to develop or test it outside Surface.
Any web app can detect Surface and use it for local files. The full contract is in docs/surface-api.md. The 10-second version:
if (window.surface?.isSurface) {
// Running in Surface — persistent grants, push-based watch, conflict-detected writes.
const handle = await window.surface.pickFile();
await handle.write('hello');
} else if (window.showOpenFilePicker) {
// Stock Chromium — works too, but the user picks each time.
}Apps already written against the File System Access API run in Surface unchanged. Surface-aware apps get the extras: persistent grants, byMe flag on watch events, mtime-based conflict detection.
apps/doc/ in this repo is a small example — a generic HTML editor that uses the bridge. It also accepts ?file=http(s)://... URLs: when the file param is an HTTP URL, doc-app fetches/PUTs/HEAD-polls over the network instead of going through the bridge. Combined with the daemon's embedded server, surface --edit foo.html on host A pops a doc-app window on host B; edits land back in foo.html on A within a second.
Two independent gates, both required for every file operation:
- Origin grant — does this origin (or, for
file://, this exact document path) have the bridge at all? - Path grant — has the user picked this specific path for this origin? Only a picker (
pickFile/pickFolder/showOpenFilePicker/showDirectoryPicker) can record one.
Folder grants are recursive; file grants are exact-match. Grants persist across restarts in ~/Library/Application Support/surface/permissions.json (a proper UI is on the roadmap).
app/ the Surface binary — Electron main, bridge, preload
bridge/ permissions, handlers, watch, default-app routing
preload.js the renderer-facing window.surface + FSA-API
main.js window management, IPC handlers, app lifecycle
server.js the embedded HTTP server — file serving, /_/open RPC, /_/apps registry
apps.js viewer discovery — scans install dirs, reads manifests
config.js ~/.config/surface/config.json loader
hello.html dev page — renders a static HTML file
playground.html dev page — exercise the bridge by hand
bin/surface the CLI — daemon launcher and HTTP dispatcher
mcp/ the MCP server — agent-facing tool (surface_open)
apps/ built-in viewer apps (doc, sheet, ...). Each has surface-app.json
docs/surface-api.md the public bridge contract
archive/ earlier prototype and personal vision drafts
pitch.html the pitch — what Surface is, for users and devs
v0.4. Downloadable, MCP-installable, end-to-end working:
window.surface+ the FSA-API subset documented indocs/surface-api.md.- Two-gate permissions, persistent across restarts.
- Watch with
byMe(self-write suppression via SHA-1 ring buffer). - Conflict detection on writes (
baseMtime→ConflictError:). - Default-app routing by extension.
bin/surface <path-or-url>CLI with single-instance lock and HTTP dispatch.surface(content)MCP tool — one universal tool: URL, file path, or raw HTML. Registerable in any MCP-capable agent host.- Daemon mode (
surface --daemon) with embedded HTTP server + LaunchAgent template — Surface always-on per machine, instant first call. - Cross-host mesh: two daemons on a tailnet form a one-verb surface where
surface Xon host A puts a window on host B. - Viewer ecosystem: per-extension routing, user-installable viewers in
~/.surface/apps/, manifests,--app=<key>override,--app=rawto bypass. Ships withdoc(html) andsheet(xlsx/csv/tsv). - Temp-file machinery for agent-generated HTML, with 24-hour GC on launch.
What's still missing for "really works as a browser":
- Signed + notarized DMG (currently shipping unsigned).
- An in-app permissions UI (revoke, inspect).
- Recursive folder watch, async directory iterators,
removeEntry/move. - Per-peer auth tokens on the embedded server (today: trust the tailnet).
- Auto-discovery of peers (today: explicit
peersin config).
See docs/surface-api.md's roadmap section for the full list.