T3 Code is a minimal GUI for coding agents. A Node WebSocket server wraps provider CLIs and agents (Codex, Claude Code, Cursor, Grok, OpenCode, Antigravity) and serves web, desktop, and mobile clients.
You can think of T3 Code as an open source "bring-your-own-subscription" alternative to apps like Claude Desktop, Codex App, Cursor Glass and Conductor.
We have over 400,000 users who love T3 Code. It's important we maintain the things they love as we continue to iterate on the product. Here's a brief list of the things we can never compromise on.
T3 Code is truly open. We share our roadmap, we share how we think about things, and of course we share all our code. A large number of our users run forks. We work in the open, and should strive to stay that way.
Lots of apps have gotten bogged down with bad tech decisions and "slop". We have not, and we're proud of the performance of T3 Code. We regularly audit for performance regressions, often caused by sending too much data over websockets, css animations causing gpu spikes, lists being hard to render, and more. Make sure all changes are considerate of performance impact.
The architecture of T3 Code's websocket layer (npx t3) enables a lot of awesome remote features. These have become core to the product. Whether users are connecting directly over their local network, using Tailscale, or leaning in fully with T3 Connect (our tunnel solution, also in this repo), we need to make sure new features are properly supported.
T3 Code has 3 key app surfaces: web, desktop, and mobile.
Web is kind of two surfaces, as we have the public facing "app.t3.codes" as well as locally hosting the web app through the npx t3 command. Both need to be supported by all new features where reasonable.
Desktop is the main surface most users install first. It's a full Electron app that bundles the server runner as well. The desktop app can also be used as the host server, allowing remote connections from app.t3.codes or the mobile app.
Mobile is a React Native app for both iOS and Android, available on the App Store and Google Play. The mobile app allows for connecting to any T3 Code server to control work remotely.
I like ambitious ideas, simple systems, and software that feels obvious. Do not preserve complexity just because it already exists. Do not introduce machinery because it looks architecturally impressive. Understand the real constraint, then fight for the smallest model that makes the correct behavior unsurprising.
Channel both "measure twice, cut once" and "yagni". Fight scope creep. Try to honor the dev's intent in both a minimal and realistic fashion.
The rest of this document is meant to help you navigate the codebase and make changes effectively. Think of these instructions less as "hard rules", more as "good defaults". The developer's preferences should be able to override anything here.
Of note: Most T3 Code contributions will come from T3 Code itself, often controlled remotely. This means you should be careful about accessing data, killing dev servers, and other things that may damage the T3 Code instance that the contributor is using.
We need to be on the same page with terminology. When communicating, use this language:
pkill -f, pgrep | kill, or kill a PID you found by matching a name, path, or worktree string. Your own agent process has this worktree's path in its argv, and this machine runs several other dev servers at once. Kill only a PID you captured at spawn, or the owner of your port from ss -H -ltnp after confirming /proc/<pid>/cwd is your worktree.~/.t3/userdata is the developer's real T3 Code database, in use while you work. Reading it and copying from it are fine, and a good way to get real test data (see Test data). Never start a server against it, never open it read-write, never clean it up.VITE_HTTP_URL or VITE_WS_URL for dev. Dev is single-origin and Vite proxies /api, /ws, /oauth, and /.well-known. Setting them bakes localhost into the bundle and silently breaks every remote browser.The most common defect in this repo is a change that works on the path you tested and is missing everywhere else. Before calling frontend work done, walk this list and say which entries applied:
packages/client-runtimepackages/contracts. Change the schema and the server, web, mobile, and desktop all follow.vp i installs. Worktrees get this from the t3.json setup script; if module resolution looks broken, it probably did not run.vp run dev starts server and web. In a worktree, state defaults to that worktree's gitignored .t3, which deliberately outranks an ambient T3CODE_HOME so you cannot land on shared state by accident. An explicit --home-dir still wins.[dev-runner] line since occupied ports shift.vp run dev --share in the background, wait for the pairingUrl: line in its output, then give that full URL to an unpaired browser. Do not wire up tailscale serve by hand, open the URL yourself, or consume the user's pairing link. A browser with the reusable dev cookie can use the bare origin. If a normal one-time token was consumed, mint a fresh one with node apps/server/src/bin.ts pair. It carries standard scopes, while the startup URL carries admin scopes needed for Connections settings.T3CODE_DEV_AUTH_TOKEN in the main checkout's gitignored .env. The t3.json setup links that file into worktrees. Never commit or publish the token or a startup URL. See Reusable dev credential.An empty database is a bad test. Seed your worktree's .t3 with a copy of real data instead of pointing at live state:
vp run migrate-dev-db with your dev server stopped. It rebuilds <worktree>/.t3/userdata/statev2.sqlite from a read-only snapshot of ~/.t3/userdata/statev2.sqlite, the developer's real data. It keeps recent projects and their stopped threads, and drops scheduled tasks, pending work, and auth sessions, so your dev server never runs the developer's agents. Raise --projects and --threads-per-project for more data.statev2.sqlite, not state.sqlite. The server copies the V1 state.sqlite only when statev2.sqlite is missing.secrets and settings.json only if the flow under test needs them.vp test run <files> for the tests you touched, targeted lint and typecheck for the scope you changed.vp check, no vp run -r test, no vp run -r typecheck unless I ask. CI owns the full suite.OrchestrationEffectWorkerV2.drain) or await the specific persisted event or Deferred that marks the milestone. Never wait on sleeps or polling. A test that needs a timeout to pass is wrong.test-t3-app for web, test-t3-mobile for mobile. The primary agent does this once after integrating. Subagents do not launch their own dev servers. Ask permission before doing computer use or spinning up browsers.For authorized mobile verification, a missing or outdated native client is a build step, not a blocker. Run node scripts/mobile-native-client.ts ensure <ios|android> <device-id> on the simulator host before starting Metro. It checks the local Expo fingerprint and builds/installs when needed. See test-t3-mobile for the full workflow.
fix(web): new threads no longer spike CPU..github/pr-assets/.Most code changes do not need an internal documentation change. Agents can read the code.
docs/internals/ is for architectural decisions and their reasons, constraints that span components, and implementation traps that are hard to discover from the source. Before adding a paragraph, ask what a maintainer would get wrong without it. If reading the relevant code answers the question, leave it out.docs/user/ helps users accomplish tasks. Give each major feature a concise section explaining what it does, how to start, and anything unintuitive. A settings path is useful; descriptions of visible buttons, icons, layouts, animations, or every UI state are not. Before adding text, ask what task or decision it helps the user with.docs/operations/ holds maintainer setup, release, and debugging procedures. Keep instructions for operating an installed T3 Code server in the user guides..plans/ is gitignored only as a safety net for legacy tooling.CONTRIBUTING.md and belong in Ideas discussions.Clients send typed WebSocket requests. The server turns them into commands. The orchestrator (apps/server/src/orchestration-v2/Orchestrator.ts) serializes commands and decides events without doing any I/O. The event sink commits those events, the projections the UI reads, the command receipt, and outbox effects in one transaction. The effect worker then runs the effects, such as starting a provider turn or capturing a checkpoint, and feeds results back as commands. Provider CLIs run as subprocesses; per-provider adapters translate their native protocols into orchestration events. Each turn ends with a checkpoint, a hidden git ref, so the app can diff and restore.
Architecture and its constraints: docs/internals/overview.md. Glossary: docs/internals/glossary.md
apps/server - WebSocket, orchestration, providers, checkpointing. Effect-heavy: read Effect services before adding server code, and .repos/effect-smol/LLMS.md for the Effect library itself.apps/web - React/Vite UI. apps/desktop wraps it, apps/mobile is React Native, apps/marketing is the site.packages/contracts - Effect/Schema contracts plus small derived helpers. No heavy runtime logic.packages/shared - shared runtime utils, subpath exports, no barrel.packages/client-runtime - client code shared by web and mobile..repos/ - vendored read-only references. Prefer their patterns over invented ones. Never edit or import from them. Sync with vpr sync:repos when bumping the matching dependency.createEnvironmentRpcCommand. Add protected methods to CLIENT_GUARDED_RPC_SCOPES in contracts and use the command's permissionAtom for UI availability. Grants are checked at execution against the destination environment; the server remains authoritative. Keep raw RPC clients inside rpc/, and extend the permission behavior tests when adding a protected method.ws.ts RPC handler, HTTP route, or MCP tool decodes input, calls one service method, and maps errors. See Effect services.apps/web/src/components/ui exports own their look. Pick a variant or size; do not restyle one with className. If none fits and the look is a generic concept, add a variant to the component; a look that belongs to one feature stays in that feature's own component, not in components/ui. Layout classes (width, flex, margin, position) belong on the parent. shadcn/no-restyle fails lint on violations. See Web UI.any is the enemy.