Chapter VII · Tools

T3 Code

Tested on
T3 Code server 0.1.4-nightly.20261027.3796, Claude Code 2.1.316, Codex CLI 0.166.0, Brigade 0.31.0, Ubuntu 24.04
Last updated
2026-11-02

Run T3 Code as a headless server on a Linux box so Claude Code and Codex sessions live in one place, then wire Brigade underneath so every session gets the same brief, verification, fleet routing, and memory handoff rules. Free to run (the harnesses use your existing subscriptions), but it adds a handful of timers you have to own.

What this is

T3 Code is an app for driving coding agents from multiple providers. It has a desktop client and a server mode. In server mode (t3 serve) it owns the threads, the per-thread git worktrees, and the provider processes (Claude Code, Codex, Cursor, and others), and any client that pairs with it sees the same sessions. This guide covers the headless setup I run, and how Brigade attaches to it: hooks inside each harness, a fleet client that routes work between machines, and handoffs that end up in the memory owner.

T3 is the runtime. It does not know about your policy. Brigade is where policy lives. Keeping that split clear is most of the work.

 clients (desktop app, browser, phone)
            |
            v
 +---------------------------+
 | T3 server (systemd user)  |  threads, worktrees, preview browser,
 | one per machine           |  delegate_task child tasks
 +---------------------------+
            |  spawns per thread
            v
 +---------------------------+
 | provider harnesses        |  Claude Code, Codex app-server, others
 | (one scope per provider)  |
 +---------------------------+
            |  hooks fire inside each harness
            v
 +---------------------------+     +-----------------------------+
 | Brigade                   |<--->| Fleet Hub (routing, claims, |
 | brief, verify, outcomes   |     | policy, seats) + t3-fleet   |
 +---------------------------+     +-----------------------------+
            |
            v
 memory owner (Memory Handoff in the repo inbox, imported later)

Why this way

OptionWhat it costs
One terminal per agent in tmuxNo shared thread list, no worktree isolation, nothing to hand a phone
Each harness’s own remote featureDifferent UI per provider, no cross-provider child tasks, no shared preview browser
T3 server plus Brigade underneath (this guide)Another daemon, a few timers, and a version gate you have to respect

I settled on the T3 server because worktree-per-thread is the default there (defaultThreadEnvMode is worktree in my settings), so two agents on the same repo do not trample each other, and because delegate_task lets a Claude parent hand a bounded child to a Codex model without leaving the app.

Brigade stays outside T3 on purpose. T3’s own tools can launch threads and children, but they do not apply my routing, admission, or verification rules. If policy lived in T3 prompts it would only hold for the harness that read them. Hooks fire in the harness regardless of how the session started, so a thread launched from a phone gets the same rules as one started in a terminal.

Prerequisites

Before / After

Before: agent sessions scattered across terminals and one desktop app. A thread’s worktree lived wherever the agent happened to run. A reboot or a closed laptop ended the session. Each provider had its own idea of what a “brief” or a “verify” was, mostly none.

After: one server per machine, all threads in one list, each thread in its own worktree under ~/.t3/worktrees/<project>/. Clients attach and detach freely. Every harness session starts with the Brigade brief injected, runs checks through brigade work verify run, and cannot stop cleanly after durable work without a handoff. Cross-machine dispatch goes through t3-fleet, which asks Brigade for the machine and seat instead of me picking one.

Implementation

1. The server unit

~/.config/systemd/user/t3-code.service runs t3 serve bound to one interface:

[Service]
Type=simple
Environment=T3CODE_NO_BROWSER=1
EnvironmentFile=%h/.config/<your-secrets-file>.env
ExecStart=/bin/bash -lc 'exec "$HOME/.local/bin/t3" serve --no-browser --host "$(/usr/bin/tailscale ip -4)" --port 3773 --base-dir "$HOME/.t3"'
Restart=on-failure
RestartSec=5
TimeoutStopSec=30

--base-dir is the whole state directory. Under it, userdata/ holds settings, the owner database, logs, and attachments, and worktrees/ holds the per-thread checkouts. Run t3 serve --help for the full flag list. t3 auth pairing mints a one-time token for a new client and t3 pair prints it as a QR code.

Then three drop-ins I would not run without:

# 50-ManagedOOMPreference.conf (created with systemctl set-property)
[Service]
ManagedOOMPreference=avoid

# 70-graceful-runtime.conf
[Service]
KillMode=process
SendSIGKILL=no

KillMode=process plus SendSIGKILL=no means restarting the server signals the server only. Agent processes that are mid-turn keep running in their own scopes. Without it, every restart kills whatever the agents were doing.

2. Why the unit pins a versioned path

My real drop-in replaces ExecStart with a path into ~/.local/lib/t3-code-cli/<version>/t3. The nightly updater (below) stages each release side by side and swaps a pointer, so the unit does not run whatever t3 happens to be on PATH. See Gotcha 1.

3. Loopback proxy for the fleet client

The fleet client talks to 127.0.0.1:3773, but the server only listens on the private interface. A second unit bridges them:

[Unit]
After=t3-code.service
Requires=t3-code.service
PartOf=t3-code.service

[Service]
ExecStart=/usr/bin/ncat --listen --keep-open 127.0.0.1 3773 --sh-exec "/usr/bin/ncat $(/usr/bin/tailscale ip -4) 3773"
Restart=on-failure
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=read-only
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
LockPersonality=yes
MemoryDenyWriteExecute=yes

PartOf= stops and restarts it with the server. The sandboxing flags are cheap because the unit only forwards bytes.

4. The timers around it

All are systemd user timers. This is the set I run on the control host:

TimerCadenceWhat it does
t3-nightly-update03:30 daily (explicit time zone)Resolves the npm nightly tag, refuses it unless the installed t3-fleet version gate accepts it, verifies release digests, stages side by side, requires an idle T3 database, backs up databases and pointers, swaps the pointer, restarts, then confirms the new version with a live t3-fleet doctor. A failed confirmation restores the old pointer and restarts once. Writes a receipt per run
t3-project-normalize30 minRenames T3 project titles from each repo’s GitHub remote and sets some project icons, so the sidebar shows owner/repo instead of a directory name
t3-fleet-settle-sweep10 minSettles threads of fleet requests that finished long ago, so the sidebar does not fill with dead rows
t3-codex-oom-avoid5 sReapplies OOM protection to the shared Codex app-server scope (Gotcha 4)
disk-janitor05:15 daily, 10 min jitterPrunes old scratch under temp dirs and removes idle clean T3 worktrees (Gotcha 8)

5. How worktrees get made

With defaultThreadEnvMode set to worktree, each new thread gets a git worktree named t3-<8 hex> under ~/.t3/worktrees/<project>/ on a new branch (mine look like t3/<short-topic>). The repository’s own git worktree list shows them next to your normal checkouts:

~/repos/<project>                          8886d2e [some-branch]
~/.t3/worktrees/<project>/t3-1a2b3c4d      a2eef96 [t3/short-topic]

The canonical checkout is never the thread’s working directory, so parallel writers on one repo do not collide. Branches survive worktree removal, so cleanup does not lose commits.

6. Brigade hooks in each harness

For Claude Code, the work-loop hooks live in ~/.claude/settings.json. The events I have wired to the Brigade hook script:

EventMatcherPurpose
SessionStartallInject the brigade work brief for the target repo
UserPromptSubmitallBrigade work-loop handler
PreToolUseBash, Edit|Write|NotebookEditBlock raw test, lint, and build runs and require the verify wrapper
PostToolUse, PostToolUseFailureedits and BashBrigade work-loop handler (failures only for Bash)
PreCompactallBrigade work-loop handler
StopallRefuse a clean stop after durable work with no handoff (a small gate script, once per stop attempt)
SessionEndallBrigade work-loop handler
SubagentStart, SubagentStopallCarry the same rules into subagents

Checks go through the wrapper so they leave a receipt:

brigade work verify run --target . --command "npm test" --capture brigade-work

brigade work verify plan shows what would run without running it. A raw npm test still works in a plain shell, but the hook blocks it inside a wired session, and a raw run produces no Brigade evidence.

7. Fleet routing and delegate_task

Two different ways to start child work, with different rules:

Brigade-wired substantial implementation still goes through brigade run. T3’s app tools are a transport. They do not bypass roster, admission, or privacy rules.

Fleet claims are execution leases. brigade fleet claims lists them. An empty list is not proof nobody owns the work, because durable ownership, reservations, and native T3 presence are tracked separately.

8. Memory handoffs

Each repo names a memory owner. Sessions on any harness write a Memory Handoff into the repo’s inbox (for Claude Code, .claude/memory-handoffs/), and the owner imports it later. The Stop gate triggers the handoff without anyone remembering. T3 holds session context and nothing durable.

9. Planned, not shipped yet

These are open Brigade issues and are not features yet. Do not build on them.

Verification

Server and proxy are up, bound where you expect:

systemctl --user is-active t3-code.service t3-code-loopback-proxy.service
ss -ltn | awk '/:3773/{print $4}'
active
active
192.0.2.10:3773
127.0.0.1:3773

Which binary is the server actually running (not the one on PATH):

~/.local/lib/t3-code-cli/<version>/t3 --version
t3 v0.1.4-nightly.20261027.3796

Timers are scheduled:

systemctl --user list-timers 't3-*' 'disk-janitor*' --no-pager --no-legend | awk '{print $NF}'
t3-codex-oom-avoid.service
t3-fleet-settle-sweep.service
t3-project-normalize.service
t3-nightly-update.service
disk-janitor.service

The fleet client sees the machines (host names scrubbed):

t3-fleet doctor
{"action":"doctor","hosts":[
 {"active":8,"host":"control-host","limit":16,"server_version":"0.1.4-nightly.20261027.3796","state":"eligible"},
 {"active":2,"host":"linux-workstation-2","server_version":null,"state":"blocked_offline"},
 {"active":1,"host":"windows-desktop","limit":12,"server_version":"0.1.4-nightly.20261027.3796","state":"eligible"}],
 "schema":"t3-fleet.result.v1","state":"blocked_offline"}

Note the top-level state is blocked_offline while two hosts are eligible. That is expected with one host down (Gotcha 9).

Brigade claims and the brief inside a T3 worktree:

brigade fleet claims
brigade work brief --target .
target  node  conductor  harness  session  acquired  expires
(no active fleet claims)
work brief: ~/.t3/worktrees/<project>/t3-<id>
session_close:
  - [ ] Verify captured through Brigade? (`brigade work verify run --target . --command "..." --capture brigade-work`)
  - [ ] Outcome recorded? (...)
  - [ ] Memory Handoff written when durable knowledge was produced?
branch: t3/<topic>
fleet_routing: impl=sol61 review=opus55 chef=chef research=luna6 security=daybreak scout=luna6
fleet_routing_source: hub revision 292

From inside a Claude Code thread, orchestrator_capabilities reports the child-task features the server supports:

{"features":{"appOwnedSubagents":true,"asyncPolling":true,"cancellation":true,
 "batchThreadCreation":true,"threadManagement":true,"scheduledTasks":true,"maxBatchThreads":20}}

If brigade work brief works but the hooks never inject it, check the hook wiring in Gotcha 6.

Gotchas

  1. t3 --version on PATH is not the server’s version. My t3 on PATH still reports 0.0.46-nightly while the service runs 0.1.4-nightly from a versioned directory. Anything that shells out to bare t3 (project scripts, the normalizer) hits the old binary. Check the version of the binary named in the unit’s ExecStart, and keep t3 update and the nightly pointer in agreement.

  2. The preview browser reports success on failure. I audited this on a 0.0.38 nightly. Four rules that held up:

    • Always pass open: true to preview_open. The open: false mode gives a half-dead tab where preview_snapshot errors and preview_click returns {} as if it worked while dispatching no click.
    • Bind dev servers to 0.0.0.0, not 127.0.0.1 only. The server rewrites explicit loopback URLs to its own bound host, so a loopback-only dev server fails to load while preview_navigate returns success with a plausible URL and title and the tab sits on a browser error page.
    • Verify with preview_evaluate after every navigate or click, for example ({href: location.href, title: document.title}). It is the only preview tool that reports accurately in every mode. A missing element and a broken surface produce the same generic failure message.
    • There is no preview_close. Tabs opened with reuseExistingTab: false leak until the server restarts, so keep the default and stay on one tab.
  3. Tool names change between T3 builds. The 0.1.3 release folded many tools into action tools (t3_queue with an action, preview_act, and so on). Older servers still advertise the old names. Read the attached tool catalog and its argument schema instead of replaying names from an older guide. A missing name alone does not mean the feature is gone.

  4. A killed Codex app-server does not come back. On a 0.1.1 nightly, when systemd-oomd killed the shared Codex app-server, T3 did not respawn it and every later Codex turn failed with read-process-exit-status. ManagedOOMPreference=avoid was not enough: it only sorts the scope last, and systemd-oomd still picked it first one day. The timer now sets ManagedOOMPreference=omit plus MemoryLow=12G on t3code-codex-*.scope and lowers oom_score_adj on its processes. MemoryLow caps how much memory is protected and reserves nothing. The timer reapplies this every 5 seconds so new scopes are covered.

  5. The fleet client needs loopback, the server binds to a private interface. Without the proxy unit, t3-fleet cannot reach the local server. Keep the proxy PartOf= the server unit so a restart does not leave a listener forwarding to nothing.

  6. brigade work hooks status can say “not installed” when hooks are working. My settings file has Brigade’s managed entries interleaved with other handlers, and the status command counts those as foreign. The script version reports current. Trust a real session: the brief shows up at session start and subagent start. If it does not, run brigade work hooks status --scope user (the default scope reads the project file, not user settings).

  7. delegate_task children start blind. No parent history is copied. Put the goal, file ownership, constraints, verification command, and terminal state in the prompt. Children inherit the caller’s checkout, so give writers disjoint files or serialize them. A cd in the prompt does not change the thread’s workspace binding.

  8. Worktrees fill the disk. Agent runs leave worktrees and scratch behind, and /tmp and /var/tmp lived on the same volume as my home directory. It hit 0 bytes free once. The janitor removes a T3 worktree only if it has been idle more than two days, no process has it as its working directory, and plain git worktree remove (no --force) succeeds, which git refuses for dirty or untracked trees. The branch ref stays.

  9. An aggregate blocked_offline hides eligible hosts. One offline machine makes t3-fleet doctor and capacity return blocked_offline at the top level. Scope the check with --host <name> for the machine you are about to use.

  10. The brief still prints chef=chef. The routing contract has not been renamed yet (see the planned section). Anything parsing the brief should accept both names for now.

  11. A restart is not a clean cut. With KillMode=process, restarting the server leaves running agent scopes alive instead of killing them. That is why the nightly updater refuses to swap unless the T3 database is idle: I do not want a version change landing mid-turn.

Templates