{"slug": "i-love-my-t3-code-development-workflow", "title": "I love my T3 Code development workflow", "summary": "A developer running AI coding agents in home-hosted dev containers replaced the SSH-based herdr workflow with T3 Code 0.1.5, an MIT-licensed \"agent harness control surface\" from pingdotgg that runs a server per container and drives already-installed Claude Code and Codex CLIs. The setup, documented in the developer's public dotfiles mirror, starts each container's server via the dcup script on port 3773 with telemetry disabled, keeps threads and pairings in a /history bind mount that survives rebuilds, and uses socat to forward host ports starting at 37731. The developer cites the in-container built-in browser and mobile clients as the main gains, but names per-container pairing with the desktop app and phone as the biggest drawback and says a single server on the Oren LXC that could reach any container would mean pairing once.", "body_md": "# T3 Code in every dev container\n\nMy agents work in dev containers on a host at home. Each container now runs its own T3 Code server, and I pair my devices with it directly, with no account and no relay.\n\nMy agents work in dev containers on Oren, an LXC at home. I used to reach them over SSH, in\n[herdr](https://herdr.dev), which I described in\n[an agent that configures my machine](https://unbeholden.dev/blog/an-agent-that-configures-my-machine). From my phone that\nmeant SSH into Oren and spawning agents by hand in a terminal, which is clunky. A few days ago I added\n[T3 Code](https://github.com/pingdotgg/t3code) beside herdr to compare the two. herdr is gone now,\nand so are `dcclaude`, `dcpi` and `dccodex`, my scripts for starting an agent in a container.\n\nT3 Code calls itself an “agent harness control surface”. It’s a server with iOS, Android, web and desktop clients, and it drives agent CLIs that are already installed and signed in, in my case Claude Code and Codex. It’s MIT licensed and packaged in nixpkgs, which is where my 0.1.5 comes from. The README calls the project “very very early” and says to expect bugs.\n\nAll of this lives in my dotfiles, which now have a\n[public mirror](https://github.com/unbeholdendev/dotfiles).\n\n## What I like so far\n\nI’m excited about this one, mostly because of how much it shows me in a single UI compared to herdr. I get the git diff, and I can merge from the same UI.\n\nThe built-in browser is the biggest win. It runs inside the container, so I preview changes in T3 Code’s browser instead of exposing a port to the host. I don’t have to think about which ports to expose, or whether two projects want the same one. I’ve already removed the exposed ports from every dev container.\n\nWorking from my phone is nearly as big. The mobile UI is built for it, and I can pick up where an agent left off, or start something new, without setting up a terminal session.\n\n## One server per container\n\nAgents run where the server runs. A single server on Oren would have put them outside the containers, and the containers are the reason I can run agents without permission prompts. So every container gets its own server, and the boundary stays where it was.\n\nIt’s also my biggest complaint so far. I have to pair every container with the desktop app and the phone, one by one. A single server on Oren that could reach into any dev container would mean pairing once. I might fork T3 Code and build that myself.\n\n`dcup`, the script that brings a container up, now starts one:\n\n```\nT3CODE_TELEMETRY_ENABLED=false setsid -f flock -n /history/t3.lock \\\n  t3 start --no-browser --host 0.0.0.0 --port 3773 --base-dir /history/t3 \\\n  </dev/null >/dev/null 2>&1\n```\n\n`/history` is a bind mount from the host, so threads and pairings are kept outside the container and\nsurvive a rebuild. The lock makes running `dcup` again a no-op while the server is alive. The\noutput goes nowhere because it holds pairing details.\n\nEach workspace claims a port on the host, the first free one from 37731, and keeps it. `socat`\nforwards that port to the container, and it looks up the container’s address on every connection,\nbecause a rebuild changes it.\n\nMy clients end up with one server per project. `dcup` writes the project’s folder name to\n`/etc/machine-info` before starting the server, because T3 Code otherwise names it after the\ncontainer’s random hostname.\n\n## Pairing without a third party\n\nT3 Code offers a managed route called T3 Connect, where your devices sign in to the same account and\nfind your machines without any router changes. I don’t want it: my devices can already reach Oren,\nand it would add an account to something that has no need of one. The nixpkgs build doesn’t offer it\nanyway, and `t3 connect` says it’s “unavailable in builds without public configuration”.\n\nThe direct route is a one-time link. `t3 pair` prints one as a QR code, but it runs inside the\ncontainer, so the link carries an address like `http://172.17.0.2:3773` that my phone can’t reach. It\nhas no option to change that. `t3 auth pairing create` does take a base URL, so I added a small\nscript, `dcpair`, that runs on the host:\n\n```\nurl=$(devcontainer exec --workspace-folder \"$folder\" zsh -c \\\n  't3 auth pairing create --base-dir /history/t3 --base-url \"$1\" --json' \\\n  zsh \"http://$(hostname -I | cut -d' ' -f1):${port##*/}\" | jq -r .pairUrl)\n\nprintf %s \"$url\" | qrencode -t ansiutf8\necho \"$url\"\n```\n\nThe URL reaches `qrencode` on stdin, which keeps the token out of the process list. Off my LAN, the\naddress only works when the tailnet routes to that subnet.\n\n## What didn’t work first time\n\n**`t3 serve`.** It looks like the right command for a headless server, and it forces project\nbootstrap off whatever you pass. My flag to add the workspace as a project did nothing, so after\npairing the server showed no projects, and threads ran in a scratch folder under `/history/t3`.\n`t3 start --no-browser` adds the project and doesn’t add a duplicate on the next start.\n\n**Worktrees.** My agents make a git worktree for any work that changes files. One made with\n`git worktree` doesn’t attach to the T3 Code thread, so the diff panel stays empty. Their\ninstructions now say to use T3 Code’s worktree tools instead.\n\n**The browser.** In my containers it stayed blank at first. It’s a prebuilt Chrome, so it can’t\nload libraries from the Nix store, and Chrome won’t start its sandbox as root. The fix has a cost, so\n`dcup` asks once per workspace. If I say yes, it\ninstalls the libraries with the distro’s package manager, and in a root container it turns the\nsandbox off.\n\n## One tool for Forgejo\n\nMy agents reached Forgejo through an MCP server. T3 Code doesn’t use it. Its server runs a forge CLI\nitself, `fj` or `tea`, for the PR status and review notifications in a thread. So I replaced the MCP\nserver with `tea`, and agents and T3 Code now use the same tool.\n\nThe switch has costs. `tea` targets Gitea, and `tea pr reply` gets a 405 from Forgejo, so agents answer review\ncomments with a normal PR comment. An MCP server gave agents typed parameters, where `tea` gives them\nflags and `--help`. The Forgejo token also now sits in `tea`’s config as well as its own file.\n\n## It steers the agent\n\nI expected T3 Code to be a window onto Claude Code. It also adds its own instructions and tools to the session, and some of them disagree with mine. It tells agents to run subagents in the foreground, not to check their own edits in its browser, and to wait for reviews with its PR watch rather than a poll.\n\nNothing decides which instruction wins, so an agent uses its judgement. My prompt now says my rules win. I don’t know yet whether agents will follow that.\n\n## The downsides\n\nThe forward listens on every interface of the host, and the pairing code is the only gate. A token\ngets every permission by default, including a terminal in the container, unless you limit it with\n`--scope`. I don’t limit mine. T3 Code’s docs say to treat pairing links like passwords, and Oren\nhas to stay off public networks.\n\nA paired device can also start an agent that runs without prompts. That’s the same trade I made before: inside the container there’s a token that can push branches and can’t merge them. A shared agent login is in there too, on a volume every container mounts.\n\nWith the browser on in a root container, a page that exploits a Chrome bug gets root in that container, with those tokens. So I only point it at the workspace’s own dev servers.\n\nThe nixpkgs package includes Electron, so it’s about 3.7 GiB. It lands once, on a Nix store volume the containers share.\n\nThe servers and forwards are background processes that `dcup` starts, not services. Nothing brings\nthem back after Oren restarts until I run `dcup` again. Direct pairings have a 30-day default\nlifetime, and I don’t know yet whether they renew, so I may be pairing again every month.\n\n## Summary\n\nT3 Code replaced herdr within a few days. It’s a much better way to work with agents, and I can start or resume their work from my phone with little effort.\n\nEach dev container runs its own server, so agents stay inside the containers. I pair my devices with\neach one through a link `dcpair` prints on Oren, with no account and no relay. The browser runs in the\ncontainer too, which let me remove every exposed port.\n\nThe costs are a pairing per container, a pairing link that grants full access, a 3.7 GiB package and a project that calls itself very early. Pairing every container separately is what I’d change first, maybe in a fork.", "url": "https://wpnews.pro/news/i-love-my-t3-code-development-workflow", "canonical_source": "https://unbeholden.dev/blog/t3-code-in-every-dev-container", "published_at": "2026-11-04 10:37:33+00:00", "updated_at": "2026-11-04 10:50:33.922305+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools"], "entities": ["T3 Code", "pingdotgg", "Claude Code", "Codex", "herdr", "Oren", "nixpkgs", "T3 Connect"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/i-love-my-t3-code-development-workflow", "markdown": "https://wpnews.pro/news/i-love-my-t3-code-development-workflow.md", "text": "https://wpnews.pro/news/i-love-my-t3-code-development-workflow.txt", "jsonld": "https://wpnews.pro/news/i-love-my-t3-code-development-workflow.jsonld"}}