T3 Code can run as a headless server while its coding agents, projects, terminals, and thread history stay on a Freestyle VM. Your browser, desktop app, or phone becomes the remote control for that environment.
You can expose the same T3 Code server in more than one way:
| Connection | What it gives you |
|---|---|
| T3 Connect | The shortest path from several devices. It uses a T3 Connect account and needs no public Freestyle ingress. |
| Freestyle HTTPS | A direct browser URL without T3 Connect. It uses a one-time pairing link on a public style.dev hostname. |
| Freestyle VPC | A network-private browser route. It uses a one-time pairing link and a WireGuard config. |
T3 Connect and direct pairing can coexist. Provider credentials still live on the VM because that is where the agent processes run.
Install the Freestyle SDK
Install the SDK in the Node.js project you will use to manage the VM:
npm install freestyle@latestpnpm add freestyle@latestbun add freestyle@latestyarn add freestyle@latest Create an API key in the Freestyle dashboard and export it locally:
export FREESTYLE_API_KEY="your-api-key"
The examples below use npx tsx to run TypeScript files. They call the Freestyle SDK, not the Freestyle CLI.
Create the VM
This script creates a persistent Ubuntu VM, installs T3 Code, and keeps its web server running as the VM’s ubuntu user. Set T3_PRIVATE=1 before the first run if you want the private VPC option later. A VPC interface must be attached when the VM is created.
import { Freestyle } from "freestyle";
const freestyle = new Freestyle();
const privateAccess = process.env.T3_PRIVATE === "1";
let vpcId: string | undefined;
if (privateAccess) {
({ vpcId } = await freestyle.vpc.create({
slug: "t3-code-vpc",
cidr: "10.88.42.0/24",
firewall: {
rules: [{ action: "allow", source: {}, destination: {} }],
},
}));
}
const { vm, vmId } = await freestyle.vms.create({
slug: "t3-code",
displayName: "T3 Code",
idleTimeoutSeconds: null,
firewall: {
rules: [
{ action: "allow", source: {}, destination: { public: true } },
],
},
...(vpcId
? { networks: [{ vpc: vpcId, ipv4: "10.88.42.10" }] }
: {}),
});
await vm.exec({
command: "curl -fsSL https://t3.codes/install.sh | sh",
timeoutMs: 300_000,
});
await vm.exec("mkdir -p ~/.config/systemd/user");
await vm.fs.writeTextFile(
"/home/ubuntu/.config/systemd/user/t3code.service",
`[Unit]
Description=T3 Code web server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
Environment=HOME=/home/ubuntu
WorkingDirectory=/home/ubuntu
ExecStart=/usr/bin/bash -lc 'exec "$HOME/.local/bin/t3" serve --mode web --host 0.0.0.0 --port 3773 --no-browser "$HOME"'
Restart=always
RestartSec=2
[Install]
WantedBy=default.target
`,
);
await vm.linuxUser("root").exec(
"chown ubuntu:ubuntu /home/ubuntu/.config/systemd/user/t3code.service && " +
"loginctl enable-linger ubuntu",
);
await vm.exec(
"export XDG_RUNTIME_DIR=/run/user/$(id -u); " +
"systemctl --user daemon-reload && " +
"systemctl --user enable --now t3code.service",
);
const health = await vm.exec(
"curl -fsS http://127.0.0.1:3773/.well-known/t3/environment",
);
console.log({ vmId, vpcId, health: health.stdout });
Run it with one of these commands:
npx tsx setup-t3.tsT3_PRIVATE=1 npx tsx setup-t3.ts The VPC-ready VM can still use T3 Connect or a public HTTPS domain. The VPC only gives you an additional private route.
Open an Interactive VM Shell
Provider login and T3 Connect both need an interactive terminal. This small SDK client connects your local terminal to a PTY on the VM:
import { Freestyle } from "freestyle";
const freestyle = new Freestyle();
const vm = freestyle.vms.ref("t3-code");
const session = await vm.pty.open({
exec: "/bin/bash -l",
cols: process.stdout.columns ?? 120,
rows: process.stdout.rows ?? 30,
onData: (bytes) => process.stdout.write(Buffer.from(bytes)),
onExit: (code) => {
process.stdin.setRawMode?.(false);
process.exit(code);
},
onError: (error) => console.error(error),
});
process.stdin.setRawMode?.(true);
process.stdin.resume();
process.stdin.on("data", (bytes) => session.write(bytes));
process.stdout.on("resize", () =>
session.resize({
cols: process.stdout.columns ?? 120,
rows: process.stdout.rows ?? 30,
}),
);
Run it whenever this guide asks you to work in the VM shell:
npx tsx t3-shell.ts
The PTY uses the VM’s default ubuntu user. Exit the remote shell with exit.
Authenticate a Coding Provider
T3 Code needs at least one supported provider installed and authenticated on the VM. Open the SDK shell and follow T3 Code’s provider setup. For example:
codex login
# or
claude auth login
The provider login belongs on the VM. GitHub login is separate. You only need GitHub credentials if you clone a private GitHub repository or use T3 Code features that talk to GitHub.
If T3 Code cannot find a provider, run command -v <provider-command> in the SDK shell. The custom service starts through a login shell so it can see provider CLIs installed by the VM’s Node environment. You can also set an absolute binary path in T3 Code’s provider settings.
Add a Project
T3 Code’s remote clients cannot currently add projects through the GUI. Clone or copy a project onto the VM, then register it through the SDK:
import { Freestyle } from "freestyle";
const vm = new Freestyle().vms.ref("t3-code");
await vm.exec("mkdir -p ~/projects");
await vm.exec(
"git clone https://example.com/your/repository.git ~/projects/my-project",
);
await vm.exec(
'~/.local/bin/t3 project add ~/projects/my-project --title "my-project"',
);
Run the script once for each project you want to open remotely. Use your repository’s real clone URL in place of the example.
Connect with T3 Connect
T3 Connect is the simplest route when you want to use the hosted web app, desktop app, or phone without publishing a Freestyle domain.
Open the SDK shell and run:
~/.local/bin/t3 connect
The command prints a browser link and a short code. Open the link on any device, confirm the code, and approve the environment. T3 Connect requires a T3 account. GitHub is not part of the Freestyle setup, so choose another sign-in method if T3 currently presents one. If you do not want a T3 account at all, use one of the direct pairing paths below.
This guide already installed a persistent service, so decline T3’s offer to install another one. After the authorization finishes, restart the existing service so it loads the saved T3 Connect link:
systemctl --user restart t3code.service
~/.local/bin/t3 connect status
Open app.t3.codes or a T3 Code app, sign in to the same T3 Connect account, and choose the VM environment. The VM initiates the managed connection outbound, so this path does not need a Freestyle TLS rule or an inbound public port.
Saving the T3 Connect login alone does not make the VM reachable. The t3code.service process must stay running.
Connect Through a Public HTTPS Domain
Use this path when you want a direct browser URL and do not want to use a T3 Connect account. Freestyle terminates HTTPS at a public style.dev hostname and forwards requests to port 3773 on the VM.
Choose an unused one-label hostname, then run this script:
import { Freestyle } from "freestyle";
const freestyle = new Freestyle();
const vm = freestyle.vms.ref("t3-code");
const domain = "my-t3-code.style.dev";
const rule = await freestyle.tls.rules.create({
action: "allow",
domain,
source: { public: true },
destination: { vmId: "t3-code", port: 3773 },
});
const pairing = await vm.exec(
`~/.local/bin/t3 auth pairing create ` +
`--ttl 15m ` +
`--label "Laptop browser" ` +
`--base-url https://${domain}`,
);
console.log(`TLS rule: ${rule.id}`);
console.log(pairing.stdout);
Open the printed https://my-t3-code.style.dev/pair#token=... URL. T3 Code exchanges that one-time token for a browser session. You can return to the bare domain later without pairing that browser again.
Create a fresh link for each additional browser or device. Treat every pairing URL like a password. Do not put it in screenshots, logs, tickets, or source control.
The hostname is reachable from the internet, but T3 Code requires an authorized client session before it exposes the environment. Delete the TLS rule when you no longer want public ingress.
Connect Privately Through a VPC
Use this path when your browser should reach T3 Code only through a private network. The VM must have been created with T3_PRIVATE=1 in the setup step.
Create a WireGuard tunnel for your computer and attach it to the VM’s VPC:
import { Freestyle } from "freestyle";
import { writeFile } from "node:fs/promises";
const freestyle = new Freestyle();
const vm = freestyle.vms.ref("t3-code");
const tunnel = await freestyle.tunnels.create({
slug: "t3-code-laptop",
vpcs: [{ vpc: "t3-code-vpc", ipv4: "10.88.42.2" }],
});
await writeFile("t3-code-vpc.conf", tunnel.clientConfig, { mode: 0o600 });
const pairing = await vm.exec(
`~/.local/bin/t3 auth pairing create ` +
`--ttl 15m ` +
`--label "Private laptop browser" ` +
`--base-url http://10.88.42.10:3773`,
);
console.log(pairing.stdout);
Install WireGuard on your computer, then bring up the generated tunnel:
sudo wg-quick up ./t3-code-vpc.conf
Open the printed http://10.88.42.10:3773/pair#token=... URL directly while the tunnel is active. When you finish, disconnect the local interface without deleting the Freestyle tunnel:
sudo wg-quick down ./t3-code-vpc.conf
Do not paste a plain HTTP private address into https://app.t3.codes. Browsers block the hosted HTTPS page from connecting to an HTTP backend. Open the private pairing URL directly or add it through the T3 desktop app.
The WireGuard config contains a private key. Store it like a credential and do not commit it.
Other T3 Connection Modes
If both devices already use Tailscale, T3 Code can publish its server through Tailscale HTTPS. Install and authenticate Tailscale on the VM, then run ~/.local/bin/t3 pair --tailscale in the SDK shell. T3’s remote-access guide covers the persistent HTTPS mapping and cleanup. Use the Freestyle VPC route above when you want Freestyle to own the private network instead.
T3 Code’s desktop app can also launch or reuse a remote server over an ordinary SSH host or alias. This guide uses Freestyle’s authenticated SDK and PTY instead, so it does not publish and maintain a separate SSH endpoint only for T3 Code.
Manage or Revoke Access
Run administrative commands through the SDK without opening a shell:
import { Freestyle } from "freestyle";
const freestyle = new Freestyle();
const vm = freestyle.vms.ref("t3-code");
console.log((await vm.exec("~/.local/bin/t3 connect status")).stdout);
console.log((await vm.exec("~/.local/bin/t3 auth --help")).stdout);
Use t3 connect unlink to disable T3 Connect exposure while keeping the saved account login. Use t3 connect logout to unlink the environment and clear that login. Neither command stops the local T3 Code service. T3 Code client sessions and unused pairing links can be revoked separately through t3 auth --help or the Connections settings.
To update T3 Code after active agent work has finished:
await vm.exec({ command: "~/.local/bin/t3 update --yes", timeoutMs: 300_000 });
await vm.exec("systemctl --user restart t3code.service");
Restarting the service interrupts active turns, terminals, and connected clients.
Delete a public TLS rule with the ID printed by pair-public.ts:
await freestyle.tls.rules.delete("tls-rule-id");
Delete the VM only when you no longer need its projects, T3 Code history, or provider credentials:
await freestyle.vms.ref("t3-code").delete();