Profile
Back to NewsBack
GitHub Trending 22 min
Reader Mode
31Carlton7/realm: Local-first agentic development and work environment

31Carlton7/realm: Local-first agentic development and work environment

8 hours ago

Realm

One workspace for every coding agent, on your Mac.

realm.computer  ·  Download  ·  iPhone app (TestFlight)  ·  Changelog

macOS · Apple silicon · in active development

!Realm's window floating beside a lit realm in deep space: every space in the sidebar under Needs you, a session that fixed a crash with the card for the two files it edited, and the file its answer named open in the side panel at the line it named.

Every space in the sidebar, a session and the two files its turn edited, and the file its answer named, open beside it at the line it named.

Local-first agent control plane for macOS — profiles → spaces, every space in one sidebar, split panes for sessions beside one side panel for terminals / browsers / devices / documents, a context pool, and an MCP gateway. See docs/superpowers/specs/2026-08-17-realm-v1-design.md.

Architecture

docs/architecture is the map of how Realm is built: every process and the lines between them, every package and who depends on whom, and the stack layer by layer with its versions. features.md follows each feature through it, one drawing each, and realm-architecture.html is the whole system as a poster you can pan and zoom.

Dev

  • Node ≥ 22.13, pnpm 10, macOS.
  • Hugeicons Pro token in .npmrc (see .npmrc.example), if you have one; see below if you do not.
  • pnpm install && pnpm dev
  • Tests: pnpm test · Types: pnpm typecheck
  • Data lives in ~/Realm/ (override with REALM_HOME).

Building without the Pro icons

The icons come from Hugeicons Pro where there is a token for Hugeicons' registry. Without one there is nothing to set up: pnpm install skips the Pro pack, which is optional, and the app draws the free @hugeicons/core-free-icons pack (MIT) under the same names. At the pinned version those are the same glyphs.

git clone https://github.com/31Carlton7/realm.git && cd realm
pnpm install --frozen-lockfile
pnpm build

The build prints [hugeicons] the Pro pack is not installed; drawing the free pack when it is drawing the free pack. docs/dev/hugeicons.md has the rest.

Website

site/ is the marketing site and docs (Next.js, deployed on Vercel with Root Directory = site). It is deliberately outside the pnpm workspace and carries its own lockfile — see site/README.md for why that matters and how to verify the WebGPU hero shader headlessly.

Agent sessions

  • Claude sessions run on @anthropic-ai/claude-agent-sdk, which drives the claude CLI: install it and log in first (claude auth login). An expired login shows up as an error in the transcript.
- A login that lives in ~/.claude/settings.json (an apiKeyHelper command and/or an env block) survives skills-enabled sessions too: skill isolation runs with settingSources: [], which stops the CLI reading that file, so the adapter carries its auth through the session's environment instead (packages/adapters/src/claude/settings-auth.ts).
  • ACP agents — Cursor, Gemini, OpenCode, GitHub Copilot, goose, Qwen Code, Grok and fx all speak the
Agent Client Protocol, so they share one generic adapter (packages/adapters/src/acp/) and adding another is an AcpAgentSpec entry in apps/server/src/app.ts. Install and sign in to each out of band; Settings → Engines probes what is on the machine and shows the exact install or login command for what is not. Realm never calls ACP's authenticate itself. - Note that ACP has deprecated modes/models in favour of configOptions, and agents have split: Cursor still answers with the old shape, OpenCode answers with only the new one, Copilot sends both. acpSessionConfig (in @realm/contracts) normalizes both and carries the id to write back through — reading one channel and writing on the other is a silent no-op.
  • Offline / UI work: REALM_ENABLE_FAKE_AGENT=1 pnpm dev registers a scripted Fake agent (echoes what you send) in the model picker.
  • MCP gateway — third-party MCP servers are configured in a space's settings, not per-agent: every session gets one Realm gateway endpoint, and credentials or OAuth tokens never reach the agent CLI. Every proxied tool call shows up in the Activity view (Connections ▸ Activity, or "MCP Activity" in the command palette).

Command line (realm)

apps/server/bin/realm runs Realm's server and drives it from a shell, with no window. It talks to the server on a home (--home, else $REALM_HOME, else ~/Realm) through the port and token in that home's daemon.json, so a running app and a script see the same spaces, worktrees and terminals: a command started with realm run shows up in the sidebar as a terminal.

realm serve &                                   # the server alone, headless (ready in under a second)
realm repo add ~/code/app                       # a checkout as a project; idempotent
realm run ~/code/app --worktree fix-login -- claude -p "fix the login bug"
realm ps                                        # every terminal: running, exited N, or killed (signal N)
realm wait --timeout 600                        # exit 0 only if every command exited 0
realm logs <terminal>; realm kill <terminal>
realm ship <checkout> --message "Fix the login bug" --pr   # commit it all, push the branch, open a PR

realm ship takes the checkout's id, which realm run --json prints as environmentId and realm worktree list prints first. It stages every change in that checkout, as the Changes pane's Stage all does, and commits it. --push also pushes the branch, setting its upstream when the remote has never seen it, and --pr pushes it and opens a pull request with gh. Where gh is missing or not signed in, it prints the GitHub link that makes the pull request instead. It is the same commit, push and pull request the Changes pane makes, and it lands in the space's History beside theirs. It exits 0 only when every step it was asked for happened, so 1 means nothing to commit (and nothing to push), a push the remote refused, or a pull request it could only link to.

--json makes every command print one JSON document. Every packaged app ships the command at /bin/realm and runs it on the app's own binary as Node, so it needs no Node install and no display: Realm.app/Contents/Resources/bin/realm, squashfs-root/resources/bin/realm in an AppImage extracted with --appimage-extract, /opt/Realm/resources/bin/realm from the deb, and resources\bin\realm.cmd on Windows. From a checkout it uses node (22.13+) on dist/realm.js after pnpm --filter @realm/server build. realm run with --worktree puts each command in its own worktree on realm/, with that worktree's port block in its environment.

A server on another machine

realm serve runs anywhere Node 22.13 runs, Linux included, and keeps agents working with your laptop closed. On a Linux machine, from a checkout: pnpm install, pnpm --filter @realm/server build, then link apps/server/bin/realm onto your PATH (on a Mac, link Realm.app/Contents/Resources/server/bin/realm). It listens on loopback only unless you add --listen. A client on another machine then needs an access token, made on the server:

# on the server
realm token create laptop            # prints the token once; only its hash is kept
realm serve --tailscale              # tailnet only: Tailscale Serve's HTTPS in front of 127.0.0.1:7420

on the laptop

realm remote add vps wss://vps.tail1234.ts.net # paste the token when asked realm --server vps run /home/me/app --worktree fix-login -- claude -p "fix the login bug" realm --server vps wait --timeout 600; realm --server vps logs <terminal>

Three ways to reach it, from the most to the least recommended:

  • Tailscale Serve: realm serve --tailscale [--tailscale-port 443] listens on 127.0.0.1:7420 and
runs tailscale serve --bg --https=443 http://127.0.0.1:7420, so only your tailnet can reach it, over HTTPS with the node's own certificate. It is taken down again when the server stops.
  • An SSH tunnel: realm serve --listen 127.0.0.1:7420 on the server, ssh -N -L 7420:127.0.0.1:7420
server on the laptop, then realm --server ws://127.0.0.1:7420 ….
  • TLS on an open port: realm serve --listen 0.0.0.0:7420 --tls-cert cert.pem --tls-key key.pem
(a renewed certificate is picked up without a restart). A client trusts a self-signed certificate with --ca cert.pem.

What it refuses:

  • Listening on every address (0.0.0.0, ::) or a public address without TLS. No flag allows it.
  • Listening on a private address (a LAN, a tailnet IP) without TLS, unless you pass
--allow-plaintext to say the network encrypts on its own.
  • On the client, sending a token over ws:// to anything but this machine, without --allow-plaintext.
  • Any handshake without a valid access token, including the server's own daemon.json token, and any
handshake from a browser page (one with an Origin header).
  • An address after ten refused handshakes in a minute: it is refused for a minute, valid token or not.
A token is 256 random bits and is shown once. realm token list and realm token revoke manage tokens on the server; a revoked token's open connections close within a few seconds. Tokens can only be made on the server itself, by someone who can read its home, so a stolen token cannot mint a successor. A token holder can do anything you can do in Realm on that machine, which includes running commands as you: treat it like an SSH key, and after a leak, revoke it and check realm phones as well. It cannot register as the server's own app or claim to be sitting at the machine.

Saved remotes live in ~/.config/realm/remotes.json (mode 0600; a file other users can read is refused). --server also takes a URL with --token, or $REALM_SERVER and $REALM_TOKEN. With --server, paths are the server's and must be absolute. Agent CLIs run on the server, so sign them in there once, over SSH.

Pairing an iPhone with a server that has no window

realm pair                           # draws the pairing code in the terminal, then asks before pairing
realm phones; realm phones remove <id|name>

realm pair works on the server or with --server. A server started by realm serve has no Realm app to unlock the Keychain, so it keeps its phone identity sealed with its own key, in /keys/remote.key (0600), or in the file $REALM_REMOTE_KEY_FILE names, such as a systemd credential. Anyone who can read that home can read the key. The app opens what a headless server paired. A headless server never replaces an identity the app sealed with the Keychain: it says to open Realm on that Mac once.

Running it as a service

realm service install --tailscale    # or any realm serve options; status, uninstall

On Linux this writes ~/.config/systemd/user/realm.service and enables it. Run loginctl enable-linger $USER to keep it running when you are logged out. On macOS it writes a launchd agent, ~/Library/LaunchAgents/co.charmtechnologies.realm.server.plist, which logs to /logs/service.log. The file runs the exact Node and realm.js you installed it from, so install again after moving either.

Code graphs (Graphify)

Graphify extracts a queryable knowledge graph from a checkout. It is a Python CLI, installed out of band like the agent CLIs — Realm probes for it and never installs it.

  • Install it with the mcp extra: uv tool install "graphifyy[mcp]". A plain
uv tool install graphifyy still puts a graphify-mcp on PATH, but it dies on ModuleNotFoundError: No module named 'mcp' the first time the hub dials it.
  • graphify.probe / graphify.update are Realm's own RPC methods, not agent kinds — graphify
has no models, no login and no transcript, so it stays off AgentKind. graphify update runs graphify update . in the space's primary checkout and needs no LLM and no API key.
  • As an MCP server, add it in a space's Connections: transport stdio, command graphify-mcp,
args the absolute path of graphify-out/graph.json. Its ten graph tools then reach every session in that space through the one Realm gateway endpoint.
  • The rendered graphify-out/graph.html opens in the documents pane. graphify points its only
script tag at unpkg with an SRI hash; the guide CSP is default-src 'none', so the preview server rewrites that tag to a vendored vis-network and drops the hash, which no longer describes the bytes being served (localizeVisNetwork in apps/server/src/documents/preview.ts).
  • Proof: pnpm --filter @realm/server exec tsx scripts/live-graphify-check.ts drives the real CLI,
the real hub and the real preview server, and skips rather than passes when graphify is absent.

Saved sign-ins (browser panes)

An agent driving a browser pane can sign you in to a site without ever seeing the password.

  • You enroll your own passwords in Settings → Sign-ins. That is the only path for a value you
supply. No tool, no RPC method, no file import and no chat message can hand a password to the store — which is what makes the origin check below worth anything, since an agent that could enroll one could enroll a password you use elsewhere against whatever page it is standing on.
  • An agent can ask Realm to GENERATE one, with browser_fill_credential's generate argument
instead of a credentialId: Realm mints a random password in the main process, saves it to the same store, and fills it under the same three gates. The agent never receives the value and cannot read it back, so this is the way to set a password on a sign-up form — strictly better for secret hygiene than an agent printing one into the transcript for you to copy. The origin is not the agent's to choose: it is read off the pane, shown on the card, and checked again against the live page before anything is typed. What it costs you is stated on that card — nothing can read the value back afterwards, so the site's own reset is the way in if you ever need the password outside Realm. Those rows are marked "Generated by Realm" in Settings, and the audit log records them as generated.
  • Values live in the OS Keychain, encrypted by Electron's safeStorage
(apps/desktop/src/main/secret-store.ts), decrypted only inside Realm's main process, and never readable back — not by you, not over IPC, not through the gateway. MCP OAuth tokens share the same store, so realm.db no longer holds them in the clear. (Per-server MCP env/headers still do; MCP_SECRET_STORAGE_NOTE says so.)
  • Every fill is gated three ways: the pane's current origin, read from CDP, must exactly equal the
origin the credential belongs to (no subdomains, no lookalikes); you approve that specific fill on a card naming the origin, username and label; and Touch ID confirms you are there. A generated fill runs the identical three, in the identical order, through the same executor. The card appears in every permission mode including bypassPermissions, is never batched, and answering "always" licenses nothing. Fills are logged to ~/Realm/logs/credential-audit.log — timestamp, origin, credential id, outcome, never the value.
  • Typing into a password field with browser_act is still refused, in every mode.
browser_fill_credential is a separate op, not a way around it.
  • Two-factor is not automated, and will not be. A Duo/Okta push cannot be driven from here, and a
TOTP prompt is out of scope. Realm fills the username and password and stops; an SSO + 2FA sign-in stays partly manual, and you finish it in the pane. If TOTP support is ever added it would be a separately enrolled secret under the same rules — never a code the model sees.
  • A Mac without a Touch ID sensor can save sign-ins but cannot fill them: promptTouchID is
biometrics-only, with no password fallback. Settings says so on the tab.

Packaging

  • pnpm dist — full build + DMG and zip in apps/desktop/release/ (pnpm dist:dir stops at an
unpacked Realm.app for fast iteration; pnpm app:update runs it and swaps the result into /Applications/Realm.app, quitting a running copy first). Under the hood: root pnpm build, then apps/desktop/scripts/stage-pack.mjs stages .pack-stage/ (a pnpm deploy of realm-server with its production node_modules, the bundled skills/, the ScrollPhase helper, the icon), then electron-builder (apps/desktop/electron-builder.yml) packs it — server and skills as real files under Contents/Resources/, never inside the asar, because node-pty's native prebuilds and a spawnable server entry can't load from an archive.
  • pnpm app:update — the fast local update loop: builds the unpacked app, gracefully quits the
installed /Applications/Realm.app, replaces it with rollback protection, and relaunches it. Set REALM_APP_PATH=/another/location/Realm.app to target a nonstandard install. This is for locally built unsigned copies; published builds continue to use the signed updater below.
  • No system Node needed: the packaged app runs realm-server under its own binary with
ELECTRON_RUN_AS_NODE. Launched from Finder (launchd's minimal PATH), main adopts the login shell's PATH at startup (login-shell-path.ts) before anything spawns, so agent CLIs and mac resolve; if the login shell can't be asked (exotic shell, timeout), it falls back to the inherited PATH plus /opt/homebrew/bin:/usr/local/bin. Terminals spawn login shells (-l).
  • Proof: node apps/desktop/scripts/packaged-smoke.cjs launches the packaged binary with a
scratch REALM_HOME and a stripped PATH=/usr/bin:/bin and asserts boot, agents.probe finding claude, bundled skills, and a terminal resolving claude.
  • Unsigned by default: with no signing credentials in the env, pnpm dist builds an unsigned,
un-notarized app (scripts/pack.mjs passes -c.mac.identity=null and says so). A copy downloaded to another Mac will be quarantined: first launch needs right-click → Open, and on Apple Silicon Gatekeeper may report the app "damaged" — clear it with xattr -cr /Applications/Realm.app. Locally built copies launch normally. Signing and notarization are fully wired and env-activated (CSC_LINK/CSC_KEY_PASSWORD + APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID): with those set, the same pnpm dist signs, notarizes and staples with zero code changes — exact steps in docs/dev/signing.md.

Releasing

  • pnpm release — bumps the app version (patch; --minor / --major for more), prepends a
changelog stub to CHANGELOG.md from merged PR titles since the last tag (via gh when it answers, else git log's squash-merge subjects; offline it degrades to plain commit subjects), builds the dmg + zip + latest-mac.yml through the normal pnpm dist, commits, and creates the vX.Y.Z tag locally. It never pushes and never publishes — it ends by printing the exact manual next steps (review the stub, push branch + tag, gh release create with the artifacts). --dry-run shows the whole plan without touching anything.

Updates

  • Signed packaged builds check the public GitHub release feed on launch, download a newer version
in the background, and ask before restarting to install it. Settings → General → Updates also supports manual checks and installing a downloaded update.
  • Public releases must carry the dmg, zip, and latest-mac.yml artifacts produced by pnpm release.
The updater never embeds a GitHub token.
  • The hard gate in apps/desktop/src/main/updater.ts still disables updates in development and in
unsigned builds: macOS cannot apply an unsigned Squirrel update. Configure signing and notarization as described in docs/dev/signing.md; pnpm app:update handles local unsigned builds.

Importing from the agent CLIs

Settings → Import brings what Claude Code, Codex and Cursor already have on disk into Realm: transcripts, the Claude memory tool's per-project fact folders, and user-level skills.

  • The agents' directories are read-only. ~/.claude, ~/.codex, ~/.cursor, ~/.agents and
~/.gemini are copied from — never written, moved or cleaned up. Everything the import produces lands in Realm's database or under ~/Realm/.
  • import.scan writes nothing. It opens files, matches candidates to spaces and answers; no
space, session or environment is created by looking. Only import.apply writes, and only for the keys it is handed — so the preview you approve is the work that happens.
  • Space matching is most-specific-location-wins (apps/server/src/import/match.ts): walking the
cwd and its parent, asking in turn for an environment, a project root, a space folder, and a directory named after a space. The walk is bounded (MATCH_MAX_HOPS) because one broadly-registered ancestor would otherwise capture every session on the machine. Anything unmatched falls to a profile's Imported space, and every row shows the rule that placed it so a wrong guess is visible.
  • Imported sessions keep their provider id when the recorded cwd still exists, so sending a
message resumes the real CLI conversation. Where the directory is gone the link is left off and the session imports as searchable history. Re-target rows in the preview: sessions.moveToSpace refuses once a session has events, and an imported session has a transcript from the moment it exists.
  • Memory is not flattened into the space doc. Fact files are copied to
~/Realm/memory/imported/// and the index goes into the space's memory document between markers (replaced on re-import, never duplicated). The largest folder here was 712k characters against a 100k doc cap; inlining would have dropped most of it and called that an import.
  • Skills are copied, never symlinked or overwritten, and land unscoped — visible in every space,
the honest translation of "installed for my user".

One transcript is not one file. Codex rewrites a whole thread into a new rollout file every time it is resumed — on one machine here, 241 files were 71 conversations, with 158 of them replays of a single Stora thread. The scan keeps the fullest copy of each and counts the rest, so the panel offers conversations rather than files.

Two scripts, from apps/server:

  • tsx scripts/live-import-check.ts — prints what an import would do against this machine's real CLI
directories, reading a VACUUM INTO copy of the database so it can reason over your actual spaces without being able to write to them.
  • tsx scripts/undo-import.ts --all-imported --sweep-environments — takes an import back out. Only
rows whose dispatch origin is import are touched, and the agents' own directories are never read or written. Worth knowing about before a big import: an imported session cannot be re-targeted afterwards, and the environment rows an import leaves behind will out-match everything on the next run if they are not swept with it.

Screens

Real captures of a real space, taken against the built app by site/scripts/capture-product.mjs, then framed as the site frames them and laid on an opaque ground by site/scripts/capture-brand.mjs — the window is made of translucent material, so a capture laid straight onto GitHub's white theme washes its sidebar out.

Every space, in one sidebar. Each space is a section of one list, and Needs you heads it with every session waiting on a permission or a question, from every space and every profile.

!The sidebar: Needs you lists two questions, one from another profile, above the Realm, Dashboard, Site and School spaces and their sessions.

Two questions waiting on you, one from another profile, above every space's sessions.

Bring the agent you already use. Claude Code, Codex, Cursor, Gemini, OpenCode, GitHub Copilot, goose, Qwen Code and Grok all run here, each keeping its own login, models and permission modes.

!The model picker open on a new session: Claude's and Codex's models grouped by harness, Opus 5.5 picked with its description, context and price, and the effort track at Max, lit, with fast mode on.

Every installed agent's models in one list: Opus 5.5 picked, at Max, with fast mode on.

Your tools, without your credentials. Connections belong to the space rather than to an agent, so a session is handed the tools and never the token — it calls Realm, and Realm calls the server.

!The Connections page, showing Linear, Notion, Slack, GitHub, Jira & Confluence and Figma as cards, each with what it grants and a Connect button.

Connecting an app to a space. One click each, and every session in the space can use them.

Confine what an agent can touch. A space can put its agents and terminals behind a macOS Seatbelt policy. It ships off, per space — Seatbelt is not a container, and the network stays open.

!The Sandbox settings page for a space, showing its three postures — Workspace write, Read only and No sandbox — with No sandbox selected.

The three postures a space can take, on the one it ships with.

Skills

skills/ holds skills Realm ships, one folder per skill, laid out exactly like the library at ~/Realm/skills/ (spec §7) so installing one is a copy. SkillSync — per-profile enablement and the symlink into each session's .claude/skills/ — is not built yet, so until it is, enable a bundled skill by hand:

ln -s "$PWD/skills/mac" ~/.claude/skills/mac
  • study-guide / lecture-notes — the school workflow (Plan 22): how to write a self-contained
interactive HTML guide the Documents pane renders (quizzes, step-throughs, flashcards, KaTeX; progress in a sidecar), and how to answer questions during a lecture and wrap one up afterwards. Both lean on the realm-docs gateway tools (docs_search, docs_list, docs_open, docs_progress), and the palette's New lecture… / Wrap up a lecture… / Import recording from Plynn… entries drive the loop.
  • mac — the mac-cli binary: Calendar, Reminders, Contacts, Mail,
Messages, Notes, Music, TV, Shortcuts, Finder, and iWork from the shell. Realm spawns agents and terminals with its own environment inherited, so mac is already on a session's PATH whenever it is on the PATH Realm was launched from — the skill exists to make it discoverable, not reachable. Note that a Realm launched from Finder rather than a terminal inherits launchd's minimal PATH, which has neither mac nor claude/codex/node on it; that is a packaging problem for all of them, not a mac-cli one.

License

Realm is released under the MIT License. A few bundled parts (file-type icons, the iOS device runner, the Inter and JetBrains Mono fonts) keep their own licenses; THIRD-PARTY-NOTICES.md lists them.

Chat with me