CrewCode is moving toward a headless server that can be controlled by the same React application used by the Electron desktop app.
The renderer must access privileged features through the typed CrewCode client contract. Components and hooks must not add new transport-specific HTTP, WebSocket, or Electron IPC calls.
shared React renderer
-> Electron client -> Electron IPC -> backend services
-> Brain-attached desktop -> Electron IPC -> owner-local Brain RPC/events
-> Web client -> authenticated HTTP/WebSocket -> Brain services
Filesystem, Git, PTY, agent, SSH, plugin, and credential operations remain on the server. Provider keys and other permanent secrets must never be returned to a browser client.
The network protocol starts at version 1. Request/response envelopes and server
capabilities live in src/shared/remote-access-types.ts. Browser startup
negotiates a compatible protocol, exchanges a URL-fragment pairing credential,
removes that credential from browser history, validates the resulting device
session, and only then installs the privileged client adapter.
- Bind to loopback by default.
- Require an explicit host option for LAN or private-network exposure.
- Never expose Electron IPC handlers directly as an unauthenticated network API.
- Use short-lived, single-use pairing tokens.
- Exchange pairing tokens for revocable device sessions.
- Validate request payloads and workspace paths server-side.
- Use HTTP for bounded RPC and WebSockets for PTY/agent/event streams.
- Enforce origin checks, request-size limits, rate limits, and session expiry.
- Recommend Tailscale or another trusted private network for access between devices.
Pairing credentials remain memory-only, short-lived, and single-use. Device sessions
are persisted as SHA-256 digests in an owner-only atomic store, survive restarts,
expire after 30 days or 7 idle days, and can be listed/revoked through authenticated
RPC. HTTP and WebSocket browser requests enforce exact same-origin checks, with
repeatable --public-origin exceptions for explicitly configured reverse proxies.
Pairing and invalid-session attempts have bounded per-peer fixed-window limits.
User-facing crewcode auth commands, general authenticated-RPC traffic limits, and
turnkey LAN/Tailscale deployment guidance remain incomplete. Keep the server on
loopback or a trusted private network unless its proxy, TLS, and public origin are
configured deliberately.
CrewCode supports two distinct deployment modes. They must share the typed client contract, but must not share credentials or silently fall back from one trust model to the other.
crewcode serve is a single-machine direct server: the browser connects to the
same process that owns the workspaces, terminals, agents, transcripts, and provider
credentials. Hub relay is a multi-machine access architecture: the browser signs in
to a separate crewcode hub, chooses an enrolled machine, and reaches that machine's
outbound-connected crewcode brain through an end-to-end encrypted tunnel. The Hub
is identity, discovery, ticketing, and routing infrastructure; it is not the Brain
and does not execute workspace operations.
| Concern | crewcode serve |
Self-hosted Hub relay |
|---|---|---|
| Processes | One direct server | One crewcode hub plus crewcode brain on each enrolled machine. crewcode hub --local-brain is a supervisor: Hub stays the parent process and spawns a sibling Brain for that host only |
| Browser route | Browser connects directly to the Brain | Browser connects to Hub; Hub routes encrypted frames to the selected Brain |
| Network reachability | The Brain must be reachable from the browser | Brains connect outbound; only the Hub needs a browser-reachable endpoint |
| Authentication | One-time pairing URL exchanged for a revocable Brain-local device session | Passkey Hub session, machine selection, and a short-lived one-shot connection ticket |
| Authorization | The direct server enforces its configured workspace roots | Hub identity cannot grant execution; the Brain independently enforces explicit roots and workspace:read, workspace:write, terminal, and agent scopes |
| Encryption | Direct HTTP/WebSocket; use Tailscale HTTPS or a deliberately configured TLS reverse proxy outside loopback | HTTPS to the Hub plus end-to-end encrypted application frames between browser and Brain; the Hub cannot read RPC, source, terminal, or agent plaintext |
| Machine discovery | None; each direct server has its own URL | Enrolled machines and presence appear in one owner dashboard |
| Browser disconnect | The Brain process can remain alive, but direct mode has no Hub-owned detached-event claim protocol | The Brain retains bounded detached events and supports same-owner bridge claiming and persisted latest-reply recovery while Brain custody remains valid |
| Default data | ~/.crewcode |
Hub identity in ~/.crewcode/hub; Brain credentials/policy in ~/.crewcode/brain and runtime state in its runtime/ child |
| Operational cost | One process and one pairing flow | Hub TLS/passkeys, machine enrollment, Brain policy, connection tickets, presence, and relay lifecycle |
Choose direct mode for one machine over loopback, a trusted LAN, or a trusted tailnet. Choose Hub relay when users need one authenticated dashboard for multiple machines, phone access, passkey login, or machines that cannot accept inbound connections. Hub relay provides a stronger multi-machine topology, not a blanket security upgrade: it has more moving parts and remains a preview with the limitations listed below.
An enrolled Electron install can enable Settings → Desktop & Web → Background Brain. Electron starts an optional detached Brain, attaches its shared renderer to the Brain's loopback backend, and leaves that Brain running when the desktop window or ordinary app process closes. Desktop and Hub web clients then use the same machine- authoritative workspace, transcript, replay/resume, terminal, and agent backend. Files never move to the Hub.
The first enable seeds only missing Brain runtime files from Electron data; existing
Brain state wins. It also aliases existing desktop thread: replay shards into the
Brain web: namespace and preserves provider-specific native resume IDs. Catalogue
state hydrates before the app mounts and is mirrored as bounded allowlisted patches.
Rich transcript writes merge divergent client snapshots so a stale save cannot erase
an observed turn.
Both clients may prompt concurrently. One conversation is serialized FIFO by the
Brain, while different conversations remain concurrent. Stop Brain or Quit and
stop Brain explicitly withdraws availability and terminates Brain-owned resources;
normal window/app close does not. See docs/desktop-web-continuity.md for the source-
of-truth, lifecycle, security, and first-release limitations.
Example direct server from a built source checkout:
node bin/crewcode-server.mjs serve \
--host 127.0.0.1 \
--workspace-root /path/to/projectsExample Hub deployment:
# On the persistent Hub host (VPS, NAS, or always-on desktop):
# Identity/relay only — recommended when this box should not execute agents:
node bin/crewcode-server.mjs hub --host 0.0.0.0 --public-origin https://your-hub.example
# Same host should also appear as a machine (sibling Brain, not one process):
node bin/crewcode-server.mjs hub --local-brain \
--host 0.0.0.0 \
--public-origin https://your-hub.example \
--workspace-root /path/to/projects \
--allow-scope workspace:read \
--allow-scope workspace:write \
--allow-scope terminal \
--allow-scope agent
# On each additional development machine, enroll once:
node bin/crewcode-server.mjs enroll --hub https://your-hub.example
# Then run the outbound Brain with explicit local authority:
node bin/crewcode-server.mjs brain \
--workspace-root /path/to/projects \
--allow-scope workspace:read \
--allow-scope workspace:write \
--allow-scope terminal \
--allow-scope agent--local-brain does not merge Hub identity with Brain execution. After the owner passkey exists, the Hub supervisor enrolls this host only and spawns crewcode brain as a sibling. Extra laptops/desktops still use enroll then brain against the Hub origin. Omit --local-brain on a public VPS that should only route; a co-located Brain executes against that host's workspace roots.
The brain serves the React application and API itself. A browser opens a one-time pairing URL, exchanges it for a brain-local session, and talks directly to that brain. This mode is for loopback, LAN, or a trusted tailnet. It requires a reachable address and does not provide account login or machine discovery.
The separate crewcode hub process now provides durable local identity storage,
first-owner passkey bootstrap, passkey sign-in, revocable browser sessions, machine
enrollment/presence, short-lived one-shot connection tickets, a bounded outbound
relay, and an end-to-end encrypted browser-to-Brain transport. The Hub dashboard can
open the shared renderer for an online machine. Workspace, terminal, and agent RPC
use the existing typed web-client adapter through the encrypted tunnel.
After passkey authentication, Hub visits at the renderer's mobile breakpoint
(innerWidth <= 768) enter /app?hub=mobile and show MobileDashboard before a
machine is selected. This home reads only the owner name and enrolled-machine
presence from the Hub control plane; it does not install a Brain client, request a
connection ticket, or invent agent/worktree activity. Selecting an online machine
opens /app?hub=mobile&machine=<id> first. That selected-machine overview obtains a
short-lived ticket and a disposable end-to-end encrypted tunnel requesting only
workspace:read and agent. It reads real workspace/worktree counts and the
same-owner Brain execution registry; a denied value is shown as unavailable rather
than replaced with zero or mock data. The recent-thread RPC returns at most five
metadata summaries (opaque scope id, file timestamp, and a 240-character first-user
title seed), never assistant replies or complete transcript bodies. The Hub still
sees only relay metadata and ciphertext. When an older, already-running Brain returns
UNSUPPORTED for that method, the renderer uses the existing transcripts.mtimes
index to show timestamped untitled rows; it never falls back to transcripts.loadAll.
The overview adapts the Brain's transcript timestamp index plus live execution registry
into Mission-agent status records and runs the same deriveMissionStats aggregation
used by desktop mc-stats; completed solo turns remain idle, while only completed
crew-lane lifecycles count as done. A recent row includes a bounded workspace id, tab
id, scope id, label, and optional provider hint in the full-app URL. After authoritative
transcript hydration, App validates that the workspace owns the tab and the tab owns
the scope, restores the exact session id when necessary, and focuses it. The generic
full-app action omits that descriptor. Either navigation closes the overview tunnel
before the full runtime requests its own ticket. /?hub-admin=1
deliberately returns a phone to Hub device/account administration without triggering
the mobile redirect. Desktop Hub visits and direct crewcode serve browsers keep
their existing startup behavior.
This is still a preview: recovery codes, live dashboard updates, persisted remote crash-durable execution custody, attachment tunneling, cross-device chat discovery, and durable bandwidth accounting remain incomplete. Per-connection frame and byte token buckets are enforced. Browser disconnect now detaches Brain-owned agents and terminals; a fresh encrypted connection explicitly reclaims known stable resource ids and replays only bounded observed events, never interrupted RPC requests or prompts. The browser discovers same-owner execution routes before the shared App mounts and buffers reclaimed events until chat subscribers are ready. If an earlier connection already consumed the detached event window, a completed execution can recover its latest assistant reply from the Brain-local conversation store over an owner-checked, agent-scoped encrypted RPC. The browser persists only the opaque chat/resource route needed to request that recovery across a full page close; it contains no credential or added authority. Recovery events use stable ids and are idempotent in chat. The Brain dashboard reports running/completed/blocked/failed/interrupted executions. This survives browser/network loss while the Brain process and its persistent Hub relay remain alive. A Brain/VPS restart still interrupts active execution and does not replay the unobserved prompt. After reconnect, CrewCode can recover the latest persisted assistant reply and an explicit new user prompt idempotently reasserts the stable bridge, creating a replacement provider process only when the Brain's process-local execution registry is gone.
Cross-thread context handoff also remains Brain-local. Browser chat scopes are
namespaced as web:<session> and persisted in per-session conversation shards; the
browser never receives the replay store. An authenticated browser may hand a source
chat into a destination bridge it owns while the Brain-local agent grant remains
valid. The Brain performs bounded disposable summarization, updates the destination
shard, clears its native resume id, and replays that combined history on the next
prompt. Handoff refuses a running destination and reports missing history or summary
failure explicitly.
A user runs one always-on CrewCode Hub on a Linux desktop, headless server, NAS, or other trusted host. The Hub serves the React application, local sign-in, machine registry, and relay. Every CrewCode brain makes an outbound-only persistent connection to that Hub, so enrolled machines can appear in one dashboard without opening a separate inbound port for every machine.
The Hub URL is deployment-specific. CJ's personal deployment uses
https://crewcode.logixhub.icu; this is not a CrewCode-operated SaaS endpoint and
must never be hardcoded as the application default. Other users provide their own
LAN address, Tailscale HTTPS name, or user-controlled domain when configuring their
Hub and enrolling brains.
browser
-> HTTPS local sign-in + machine list -> self-hosted CrewCode Hub
-> authenticated encrypted tunnel -> Hub relay <- outbound tunnel <- CrewCode brain
Hub control plane: local users, machine keys, enrollment, presence, revocation
Hub relay: connection routing, backpressure, short-lived ticket enforcement
brain: final authorization, workspace sandbox, RPC execution, secrets
The Hub relay is not a replacement for the brain's authorization boundary. The brain must validate the user, machine audience, expiry, and session identity on every new tunnel before installing a privileged client session.
A managed CrewCode-hosted Hub may be added later, but it must implement the same protocol and must never be required for self-hosted operation.
- First launch creates no default password. It prints a short-lived, single-use owner setup URL whose credential remains memory-only. The first owner registers a user-verifying passkey. Recovery codes are still planned and must be implemented before passkeys are presented as recoverable.
- Subsequent browser sessions authenticate to the Hub with WebAuthn/passkeys. An optional external OIDC provider may be configured by the Hub owner, but is not required.
- Successful Hub sign-in creates a revocable 30-day trusted-browser session. Its HttpOnly cookie persists across phone/browser restarts, while only its digest is stored in Hub SQLite. Clearing site data, signing out, expiry, or server-side revocation requires passkey sign-in again; none of those events enrolls or re-enrolls a machine.
- The browser uses secure, HttpOnly, SameSite cookies for the Hub session; Hub bearer
tokens must not be stored in
localStorage. - State-changing Hub routes require CSRF protection and exact checks against the configured public origin. The Hub refuses ambiguous forwarded-host/protocol headers unless the reverse proxy is explicitly trusted.
- Recovery must not silently restore access to revoked machines. Recovering Hub ownership and trusting a machine are separate events.
The first release may be single-owner, but authorization must still use stable local user ids so multi-user access can be added without changing machine identity.
crewcode enroll --hub <url>creates or loads a machine identity key and prints a short-lived device authorization URL/code. Use an OS keystore or TPM when available, with an owner-only file fallback for headless systems.- The user signs in to their Hub, confirms the machine name and fingerprint, and assigns the machine to an allowed local user.
- The brain exchanges the approved device code for a revocable machine credential. Only a digest/encrypted form is persisted, with owner-only filesystem permissions.
- The Hub stores the machine public key, owner, display name, created time, last-seen time, reversible disabled state, and revocation state. It never receives provider keys, workspace credentials, source files, transcripts, or terminal output as control-plane data.
- Re-enrollment and ownership transfer require explicit confirmation. A revoked machine credential cannot be refreshed.
Enrollment codes are single-use, short-lived, rate-limited, and bound to the machine key. A copied code alone must not be enough to impersonate a machine.
An enrolled brain opens an outbound wss:// connection to its configured Hub,
proves possession of its machine key with a Hub nonce, and sends a bounded
capability/presence record. The machine list exposes only metadata such as:
- stable opaque machine id and user-selected name;
- online, offline, disabled, connecting, or revoked status;
- platform, CrewCode version, protocol version, and coarse capabilities;
- last seen time and an optional user-selected location label.
Workspace paths, repository names, active prompts, and provider identities are not
presence metadata. Presence expires when heartbeats stop; silence is offline,
never evidence that a command or agent turn completed.
- The signed-in browser selects a machine.
- The Hub issues a very short-lived, single-use connection ticket bound to the local user, browser session, machine id, requested protocol, and random nonce.
- The browser presents the opaque ticket once to the Hub relay. The Hub consumes it, revalidates machine ownership/disabled/revocation state, and sends immutable user/session/ scope claims over the machine-authenticated outbound channel. Expired, replayed, wrong-machine, offline, disabled, or revoked tickets are rejected. Tickets are memory-only, not self-contained bearer claims or durable signed tokens.
- The browser and brain perform an authenticated end-to-end handshake using the enrolled machine public key and a browser ephemeral key before privileged RPC is enabled.
- HTTP-style RPC and PTY/agent events are multiplexed as bounded tunnel frames. The existing versioned request/response envelopes remain the application protocol.
- Disconnecting marks in-flight outcomes
interruptedunless the brain observed and persisted a terminal result. Reconnect never infers success from silence.
The Hub adapter belongs behind crewcode-client.ts. Components and hooks must not
know whether frames use direct HTTP/WebSocket or the Hub relay.
When the shared renderer is opened through an authenticated Hub, Settings → Hub
Machines lists every machine enrolled to that owner. Disable is a reversible
authority suspension, not a visual preference: the Hub records disabled_at, closes
the machine relay and its browser tunnels, refuses heartbeats and new connection
tickets, and preserves the enrollment credential for a later explicit Enable.
Enable clears the suspension and reports the machine offline until its Brain
reconnects and a new heartbeat is observed. The Settings adapter uses the Hub's
same-origin HttpOnly session plus a freshly observed CSRF value; it never routes this
owner control through the selected Brain or exposes a Hub credential to renderer
storage. Direct-server and Electron-only Settings do not show this Hub-owner surface.
The Hub binds to loopback by default and requires explicit network configuration. Supported deployment profiles are:
- LAN: bind the Hub to a private interface and use trusted local DNS/TLS. Access works only from that network.
- Tailnet (recommended): keep the Hub private and publish HTTPS through Tailscale. Browsers and brains join the tailnet; no public ingress is required.
- User-controlled public origin: use the owner's own domain and place Caddy,
nginx, or another HTTPS reverse proxy in front of the Hub. For example, CJ uses
https://crewcode.logixhub.icufor his deployment. The owner is responsible for DNS, firewall configuration, and TLS renewal. - Reverse tunnel: keep the Hub local and publish a user-controlled domain through Cloudflare Tunnel or an equivalent service. This makes the service internet reachable, and that provider becomes part of the network threat model.
CrewCode must not automatically enable public exposure, edit firewall rules, or create a third-party tunnel. Setup should print explicit commands and warnings for the profile selected by the owner.
TLS protects each network hop, but hop-by-hop TLS alone lets a reverse proxy, tunnel provider, or Hub relay inspect source and terminal traffic. Application-layer end-to-end encryption between browser and brain is required for public/reverse-tunnel deployments and should be used in every profile; the relay routes opaque frames. Metadata needed for abuse prevention (local user id, machine id, connection id, frame size, timestamps, and close reason) may be logged with an owner-configurable retention period.
The relay must enforce per-user/machine connection limits, frame-size limits, idle and absolute connection expiry, bandwidth backpressure, replay protection, and rate limits before forwarding traffic. It must never accept arbitrary destination hosts or become a general-purpose TCP proxy.
- Owners can inspect enrolled machines and reversibly enable/disable them from Hub web Settings. Permanent revocation remains a separate Hub administration action.
- Disabling a machine immediately refuses new authority and closes its active relay sessions. The local Brain may retain local execution custody and retry its outbound connection; disabling remote access is not evidence that local work completed or stopped.
- A brain periodically revalidates machine status and immediately closes new and active tunnels when disablement or revocation is observed. Revocation remains terminal: its credential cannot be enabled again.
- If identity, scope, relay continuity, or session authority becomes unknown, the
brain refuses new privileged actions and applies the execution-custody rules in
docs/execution-custody.md. - Relay loss does not kill an agent blindly if doing so could corrupt work, but the run must be contained, recorded as interrupted/unknown where its result was not observed, and require the documented reauthorization path.
- Audit events record bootstrap, enrollment, connection, rejection, revocation, and authority changes. They must not include prompts, source content, provider secrets, or raw terminal streams.
The Hub runs as a separate headless process rather than inside the Electron renderer
or main process. It may ship from this repository as crewcode hub, but its storage
and network lifecycle remain independent from any one brain. crewcode hub --local-brain
is a supervisor on the Hub host: it must spawn a sibling Brain process, keep
~/.crewcode/hub and ~/.crewcode/brain separate, and wait for owner passkey
setup before in-process local enrollment. It must not fold Brain RPC, workspace
roots, or provider secrets into the Hub SQLite store. Extra machines never use
--local-brain; they enroll remotely. This repository owns
the shared protocol, Hub service, brain connector, CLI enrollment flow, and browser
adapter. No identity, proxy, or database vendor SDK may leak into renderer components
or backend workspace services.
Minimum Hub data model:
LocalUser(id, credential, role, created_at, revoked_at)
Machine(id, owner_user_id, public_key, name, status, created_at, last_seen_at, disabled_at, revoked_at)
BrowserSession(id, user_id, created_at, expires_at, revoked_at)
ConnectionTicket(id, user_id, machine_id, browser_session_id, expires_at, used_at)
AuditEvent(id, user_id?, machine_id?, browser_session_id?, type, created_at, metadata)
- Introduce the transport-neutral client boundary and versioned protocol types. Complete.
- Extract main-process IPC logic into reusable backend services. Workspace and core filesystem operations complete; Git, PTY, and agents follow with their server transports.
- Add a loopback-only headless server and a minimal browser connection screen. Core server, handshake, one-time pairing, authenticated RPC, and connection screen complete; CLI packaging remains.
- Add authenticated workspace/filesystem operations. Browser adapter, pairing exchange, locally persisted device session, workspace listing, text editing, and saving complete.
- Add PTY and agent streaming over WebSockets. PTY and core agent lifecycle services, authenticated event transport, browser chat/terminal controls, workspace-root enforcement, native resume IDs, local transcript fallback, compaction RPC, and permission responses complete. The full desktop shell is not mounted in browsers yet.
- Harden direct mode: persistent expiring sessions, authenticated inspection/revocation RPC, exact origin checks, and authentication rate limits are complete. User-facing auth CLI commands, general request-rate policy, and LAN/Tailscale guidance remain.
- Implement the self-hosted
crewcode hubprocess, local owner bootstrap, passkey sessions, machine registry, audit events, and signed single-use tickets. Process/CLI, SQLite identity schema, passkey bootstrap/sign-in, browser sessions, audit storage, and machine registry are complete. Recovery and signed connection tickets remain. - Implement
crewcode enroll, persistent machine identity, outbound presence, and explicit machine revocation. Enrollment, owner-only machine credentials, outbound heartbeat presence, dashboard status, reversible Settings disable/enable, and revocation are complete. Machine logout/credential rotation remain. - Implement the bounded Hub relay and a transport-neutral multiplexed tunnel with authenticated end-to-end browser-to-brain encryption. Preview complete: one-shot 60-second tickets, outbound authenticated WebSocket relay, P-256 ephemeral ECDH, enrolled Ed25519 Brain authentication, HKDF/AES-256-GCM ordered frames, 30-minute idle and 8-hour absolute connection expiry, backpressure/frame bounds, per-connection frame/byte token buckets, and typed RPC/event multiplexing are implemented. Explicit fresh-ticket browser reconnect preserves UI state without replaying interrupted operations and reclaims known Brain-owned terminal/agent ids. Stable web bridge ids allow the same remote thread to reattach after a page reload. Cross-device thread discovery and Brain-process restart recovery remain.
- Replace the direct-only browser connection screen with local Hub sign-in, machine list/status, machine selection, reconnect, and revocation UI while retaining an explicit direct-pairing route. Initial machine selection and shared renderer launch are complete; automatic reconnect and live status remain.
- Persist remote execution custody and test disconnect, restart, revocation, replay, cross-user isolation, relay compromise, and backpressure behavior.
- Move the desktop application onto the same backend contract. First continuity slice complete: enrolled Electron can start and attach to an optional background Brain; desktop and web share machine-authoritative workspaces, transcripts, replay/resume state, terminals, agents, and a bounded chat/workspace catalogue. Remaining desktop-only orchestration surfaces and live cross-client navigation updates still need convergence.
Implemented direct-server commands:
npm run serve
npm run serve -- --host 127.0.0.1
npm run serve -- --host 0.0.0.0 --public-origin https://your-hub.exampleThese commands currently run from a source checkout. CrewCode is not distributed as an npm package.
Implemented self-hosted Hub and mobile QR commands:
node bin/crewcode-server.mjs hub
node bin/crewcode-server.mjs hub --local-brain --workspace-root ~/developing --allow-scope agent
node bin/crewcode-server.mjs hub mobile --tailscale
node bin/crewcode-server.mjs hub mobile --public-origin https://your-hub.example
node bin/crewcode-server.mjs hub --host 0.0.0.0 --public-origin https://your-hub.exampleFrom a source checkout, equivalent npm conveniences build the current checkout first:
npm run hub:mobile
npm run enroll -- --hub https://your-tailnet-host.example.ts.net
npm run brainnpm run hub:mobile configures Tailscale Serve without replacing an unrelated
existing Serve configuration. After inspecting tailscale serve status, replacement
requires the explicit npm run hub:mobile -- --tailscale-replace opt-in. npm run brain is the foreground/headless Brain command; do not run it against the default
Brain data directory while Electron Background Brain is enabled. Electron already
owns that detached Brain lifecycle.
hub mobile --tailscale requires a connected Tailscale client, MagicDNS, and HTTPS
certificates enabled for the tailnet. It derives the exact https://<node>.<tailnet>
origin, refuses to overwrite an existing Serve configuration unless
--tailscale-replace is explicitly supplied, proxies HTTPS to the loopback Hub,
and prints a terminal QR. On first startup, a distinctly labeled setup QR contains
the same short-lived, single-use 10-minute bootstrap fragment as the printed owner
setup link; this is necessary to create the first passkey and must not be shared.
After owner creation, terminal and authenticated-dashboard QR payloads contain only
the stable Hub URL—no session, enrollment credential, or Brain ticket. The phone
must belong to the tailnet and still signs in normally.
Users without Tailscale provide their own trusted HTTPS reverse proxy/domain with
hub mobile --public-origin. The proxy must forward HTTP and WebSocket upgrades to
the loopback Hub. A QR code is address transfer, not a tunnel; plain LAN HTTP and
self-signed certificates are intentionally not treated as safe iPhone deployment.
Passkeys are bound to the exact hostname. Changing an already-configured Hub from
localhost or another domain to a Tailscale/domain origin requires registering the
owner credential for that final origin (for an early test install, use a separate Hub
data directory and re-enroll Brain). Once selected, keep the HTTPS origin stable.
The Hub defaults to 127.0.0.1:3774, stores state in ~/.crewcode/hub/hub.sqlite,
and prints a ten-minute single-use owner setup URL on first launch. Interactive
terminals receive an OSC 8 clickable setup link plus the raw URL as a copy fallback.
Browsers normally treat http://localhost as a secure context, but some Linux
browser/passkey-provider combinations reject it with InsecureLocalhostNotAllowed;
use a current Chrome/Chromium build for local testing or the final HTTPS Hub origin.
Do not weaken the Hub CSP for extension-injected scripts or styles. Wildcard binds
require an explicit final public origin; non-loopback origins require HTTPS because
the origin is cryptographically bound to passkeys. Put a TLS reverse proxy or
Tailscale HTTPS in front of the HTTP listener for network deployment.
--local-brain waits for that owner passkey, enrolls the Hub host without the
phone comparison-code dance, and spawns a sibling Brain. The local credential is
still owner-only on disk; Hub identity still cannot widen Brain scopes.
After signing in on the phone, run this on every additional machine:
node bin/crewcode-server.mjs enroll --hub https://your-hub.exampleThe PC generates its Ed25519 identity locally, prints a short XXXX-XXXX comparison
code and public-key fingerprint, and polls with a separate 256-bit private request
secret. The authenticated phone dashboard automatically shows the pending machine.
Verify the code/fingerprint, then tap Approve or Reject. The short code is
identification only and cannot retrieve a credential; approval returns the one-time
machine bearer credential exclusively to the polling PC. Requests expire after ten
minutes, are memory-only, rate/bound limited, and disappear on Hub restart. The
legacy --token path remains for controlled automation but is no longer the default.
Then start the relay:
node bin/crewcode-server.mjs brainEnrollment creates an Ed25519 machine identity plus a random bearer credential in
~/.crewcode/brain/hub-machine.json, written with owner-only permissions. The Hub
stores the public key and only a SHA-256 digest of the bearer secret. crewcode brain
then maintains an authenticated outbound WebSocket relay and sends HTTPS heartbeats
every 30 seconds; the dashboard marks a machine offline after 90 seconds without a
successful heartbeat. Disabling it from Settings → Hub Machines closes active
relay sessions and rejects heartbeats/tickets until it is explicitly enabled again;
the preserved Brain process can then reconnect with the same credential. Revoking it
closes active relay sessions and permanently rejects later heartbeats. Enrollment
tokens are never written
to the Hub database and are invalidated by Hub restart, expiry, first successful use,
or a failed guess against their id.
That credential belongs to the machine, not to a particular Brain process. Electron
Background Brain and the foreground/headless crewcode brain command both reuse the
same default ~/.crewcode/brain/hub-machine.json; stop one before starting the other,
but do not enroll again when switching modes. A custom --data-dir is a deliberately
separate Brain identity and therefore does not reuse the default credential. The
phone's persisted Hub browser session is likewise Hub-owned rather than Brain-owned,
so restarting or changing Brain mode does not forget the phone.
Remote authority is disabled by default. Enable only explicit Brain-local roots and scopes, for example:
node bin/crewcode-server.mjs brain \
--workspace-root ~/developing \
--allow-scope workspace:read \
--allow-scope workspace:write \
--allow-scope terminal \
--allow-scope agentThe first Brain start seeds an owner-only persisted policy from these flags. After that, the web Settings → Brain Access section manages Brain-local roots and scopes through the E2EE tunnel without restarting Brain. Reductions apply immediately and stop affected agents/terminals; additions renew the encrypted tunnel with a fresh ticket. Hub sign-in and ticket scope requests cannot widen these grants. Every RPC method is classified again at the Brain and filesystem/PTY/agent operations retain live workspace enforcement. The enrolled Ed25519 identity signs each ephemeral P-256 handshake; HKDF-derived AES-256-GCM keys encrypt ordered application frames so the Hub routes ciphertext rather than source, terminal, prompt, or response content.
Planned direct-auth and remaining Hub commands:
crewcode pair
crewcode auth sessions
crewcode auth revoke <session-id>
crewcode hub machines
crewcode hub revoke <machine-id>
crewcode brain logoutThe initial CLI implementation is available from a source checkout through
npm run serve or node bin/crewcode-server.mjs <command> after npm run build.
It builds/serves the shared renderer, defaults to loopback, prints a single-use
pairing URL, resolves installed provider CLIs without Electron, and shuts down
cleanly on SIGINT/SIGTERM. The direct-auth CLI and remaining machine-management
commands above remain planned. Enrollment, dashboard revocation, Settings machine
enable/disable, machine selection,
and shared CrewCode workspace-client launch through the encrypted Hub relay are
implemented.
WorkspaceService owns persisted workspace listing and mutations, project
creation, cloning, and remote workspace registration without importing Electron.
FilesystemService owns sandboxed directory listing, text reads/writes, mkdir,
rename, delete, copy, and file discovery, including the existing SSH routing for
reads. Network filesystem RPC also rejects roots absent from the server workspace
store, preventing a browser from substituting / or another arbitrary host path.
workspaceStore.ts and fs.ts are now Electron transport adapters for those
operations. Browser file-tree copy/paste uses fs.copyFile (same-dir duplicate
when destDirRel is omitted, otherwise copy into that folder, with '' meaning
the workspace root). Cut/paste uses fs.move into the destination folder.
Native folder pickers remain in the Electron adapter. SSH roots still refuse
copy/move/mkdir/rename/delete over web access; Electron SSH uses the remote
copy and move paths.
Hub-relayed attachment tunneling uses ordered 256 KiB chunks inside the existing
browser-to-Brain encrypted RPC tunnel. The Hub sees only bounded ciphertext frames.
Brain requires workspace:write, restricts destinations to registered workspace
roots, rejects symlink escapes and files over 25 MiB, verifies a final SHA-256
digest, and removes canceled, failed, idle, or shutdown-temporary uploads. The
browser can list the Brain-owned MCP registry and select entries by opaque id;
bridge.start resolves
those ids server-side and never accepts executable MCP command or environment
definitions from the browser.
PtyService now owns process lifecycle independently of Electron. Both Electron
IPC and the remote server adapt that service. Browser terminal creation is
restricted to registered workspace roots, commands use authenticated HTTP RPC,
and output/exit events use an authenticated WebSocket endpoint. Core agent bridge start/prompt/abort/stop/mode and permission-response operations
now use AgentBridgeService, with normalized events delivered on the authenticated
WebSocket. The server resolves binaries and API keys itself and discards browser-
supplied secrets, environment variables, external directory grants, and plugin
providers. The reusable service now persists provider resume IDs and normalized local
user/assistant transcript fallback, and exposes provider compaction. Complex
cross-provider handoff summaries and every desktop-only surface still live in the
Electron application; browser chat intentionally uses the same bridge contract
without pretending unsupported desktop controls are available. Prompt acceptance is
not treated as turn completion: the browser keeps the Stop control active until an
authoritative terminal bridge event arrives. Provider model discovery uses the same
authenticated RPC and keeps curated fallback choices visible while that asynchronous
discovery is pending or unavailable. The GitHub sidebar can read Brain-local gh
status, pull requests, workflow runs, and issues, and can create/merge/approve pull
requests inside registered workspaces without exposing the Brain's GitHub token.
Browser voice supports Brain-configured OpenAI/xAI realtime client secrets,
dictation, and speech; permanent keys remain server-side, remote key mutation is
denied, and remote audio is bounded to 8 MiB. Browser editor formatting is routed to
workspace-local Prettier through the sandboxed filesystem service.
Browser delegation now mints a Brain-loopback, per-parent bearer endpoint and correlates its thread operations to the authenticated browser that owns the parent. The renderer still authoritatively enforces depth one and parent/child ownership; the Brain additionally enforces token-leak refusal, request limits, mode policy, and concurrency caps before forwarding a request.
Approved plugin panels load in sandboxed iframes from short-lived, asset-only capability URLs. Those URLs grant access only to files under one approved plugin folder; they do not contain the browser session credential. Capability calls still flow through the trusted renderer and the existing manifest permission gate, with workspace roots revalidated against the Brain registry. Approval, enablement, and installation remain Brain-local administration operations.
Open-file polling and TypeScript language-server framing run on the Brain. Watch
events and LSP messages are delivered only to the authenticated browser session
that owns the watch/handle, and handle send/stop operations reject cross-session
ownership. GitHub device login output and codes stream from Brain-owned gh; PR
operations and repository publishing are confined to registered workspace roots,
and the browser never receives GitHub credentials. Remote GitHub logout remains
disabled so a browser cannot revoke the Brain's host credential unexpectedly.
Brain-local voice sidecars remain incomplete browser work.