Run coding agents like processes. Supervise them like jobs.
Developers are running multiple coding agents in parallel including Claude Code and Codex. But once you run more than one, things get messy:
- terminals everywhere
- scrolling logs
- lost artifacts
- agents needing attention
agtd turns coding agents into durable tasks with state, artifacts, and history. Instead of babysitting terminal
tabs, you supervise work.
tmux multiplexes terminals. agtd supervises agents.
agtd is a daemon runtime for supervising coding agents as durable tasks.
Each task runs inside a managed session with:
- a working directory you choose: a git worktree, a plain checkout, or any directory
- a dedicated PTY
- retained terminal history
- persistent artifacts
This allows developers to supervise agent work without constantly switching between terminal sessions.
agtd works WITH your terminal. Ghostty, iTerm2, Kitty, WezTerm... you chose your terminal for
a reason. agtd doesn't replace it.
To view an agent: open a new tab, run agt attach my-agent. Or use the built-in TUI (agt
with no args) for a quick overview of all sessions.
What agtd manages that your terminal can't:
- Daemon lifecycle: agents keep running after you disconnect
- Attention signals: know when an agent needs you without watching it
- Session metadata: persists across daemon restarts
- Live reattach: reconnect to running sessions from any terminal
- Vendor-neutral: Claude Code, Codex, any TTY-based agent
Native scrollback, native search, native copy/paste. For free.
Start a task:
agt new fix-testsThis creates a session, and starts the agent in an attached PTY.
List running tasks:
agt ls
RUN AGE NAME ACTIVITY CWD ATTENTION
⚠ 4m dep-bump blocked 1m ~/src/app permission: Use Bash?
● 12m fix-tests working ~/src/app
○ 6m docs-readme exited ~/src/docs finished (exit 0)Sessions that need you come first; see Attention model.
You can attach to a running agent to open the underlying PTY session:
agt attach fix-testsMultiple clients can attach to the same running session at once, including the TUI and one or
more agt attach processes. The first one to attach sets the session's size; the others see the
agent at that size without resizing it. To resize the session to your terminal instead, use
ctrl + y then r (Take over size), or attach with agt attach --take-size fix-tests. When the
terminal that sets the size detaches or is closed, the one used most recently takes over; one that
only lost its connection (or rode out a daemon restart) keeps the size if it reconnects within 10
seconds.
When the session ends while you are attached (the agent exits, the overlay's Stop, an
agt kill), agt attach says how, session fix-tests finished (exit 0) or
session fix-tests failed: ..., and exits; it exits with status 1 when the session failed, and so
does agt new when the session it attached to failed.
Detach the local agt attach client using ctrl + \; the screen is cleared, and the session's
output stays in your terminal's scrollback. Switch to the previous attached session
with ctrl + [ and the next one with ctrl + ]. To inspect or manage other attached
clients:
agt attachments fix-tests
agt detach fix-tests --attach attach-1
agt detach fix-tests --allInspect a session's scrollback (live from the session, or from its saved log once it has ended):
agt history fix-tests
agt history fix-tests --vtReview what the agent produced in its git repository: commits, staged and unstaged edits, and new files, all since the session started:
agt diff fix-tests # the whole diff, coloured in a terminal
agt diff fix-tests --stat # or a diffstat, or --name-only
agt status fix-tests # includes branch, base, commits and changed files
agt artifacts fix-tests # what can be downloaded: diff, patch, history, history.vt
agt artifact fix-tests patch -o fix-tests.mbox # the commits, for `git am`See what sessions cost: memory, CPU, threads and open files of a session's worker and agent, and its scrollback; or the daemon and every running session at once:
agt status --stats fix-tests
agt daemon statsStop a task:
agt kill fix-testsRemove a session and clean up its artifacts:
agt rm fix-testsOr use the compatibility form:
agt kill --rm fix-testsRun agt without any arguments to open the inbox: every session on this machine and on every
paired host, the ones that need you first, below the prompt.
agtd 1 need you · 2 working · 1 idle
local ● 3 devbox ● 1 38ms sfo2 ⚠ unauthorized · agt host info sfo2
▌ ⚠ auth-refactor devbox claude blocked 4m Allow Bash: go test ./... 2h
● docs local codex working 62% Updating README 40m
● scratch local claude idle 38m 3h
+ new session
╭──────────────────────────────────────────────────────────────────────────────────╮
│ ❯ Filter, or name a new session │
╰──────────────────────────────────────────────────────────────────────────────────╯
↑↓ choose · enter attach · tab actions · ctrl-n new · esc quit · ? keys
Type to filter (devbox/ narrows to one host), Enter attaches to the selected session on whichever
host runs it (a detach, or the session ending, comes back to the inbox), Tab opens its actions
(attach, remove), Ctrl-N or the + row starts an agent in the
current directory (named by the filter, if any), Ctrl-R asks every host again, and Esc (or Ctrl-C) quits with everything still running.
agt --host NAME opens it on that host alone; for a remote host, Ctrl-N there shows the
agt new -C DIR NAME/SESSION command that starts a session on it.
agtd introduces three core primitives.
- Tasks. A long-running unit of work. A task may spawn one or more agents and has a lifecycle (running, completed, failed).
- Threads. A sequence of reasoning associated with a task. Threads capture prompts, tool calls, intermediate outputs, and final results.
- Artifacts. Outputs produced by agents, such as commits, files, patches, test results, or screenshots.
Multiple agents create an attention problem.
Instead of streaming logs constantly, sessions emit attention signals:
info background update
notice something meaningful happened (an agent went quiet, a session finished)
action user intervention required (a bell, a permission prompt, a failure)
Clients surface sessions based on attention instead of raw output. The signal is the program's
own word: program status reports
(OSC 7501), through which a program says it is working (with progress), blocked on the user
(and why: a permission, a question, a login), done or error, with a one-line message. Claude
Code reports since 2.1.295, and agtd does not guess: a program that does not report simply
shows as running, with no inferred waiting or idleness. Bells and desktop notifications (OSC 9
and OSC 777) are still recorded as info-level events for observability, and the foreground
process is still sampled (a non-reporting foreground command that is silent for half an hour
records a stalled notice), but neither raises attention. Report-driven signals are recorded as
events, and a session needs attention until you look at it: attaching to it (or detaching from
it) acknowledges it.
agt ls
RUN AGE NAME ACTIVITY CWD ATTENTION
⚠ 12m fix-tests blocked 2m ~/src/app permission: Run the tests?
● 40m dep-bump working 47% ~/src/app
● 3m docs idle 5m ~/src/docs Updated the README
○ 1h refactor exited ~/src/app finished (exit 0)Sessions that need you come first, in agt ls, in the inbox (agt) and in the Ctrl-Y
switcher. agt status NAME shows the details: activity, foreground command, terminal title,
elapsed time and what needs attention.
Every session's history of events is kept (the newest 500 per session), along with a record of
the machines that paired, were authorized or revoked, and connected remotely (shown as host):
agt events # the latest events of every session
agt events fix-tests --follow # follow one session
agt events --follow --notify # ring the bell and post a desktop notification when a session needs you
agt events --follow --level action --exec 'say "$AGTD_EVENT_SESSION needs you"'
agt events --json --after 120 # for scripts: everything after event 120--notify writes to your terminal, so it pops a desktop notification wherever your terminal
supports one (iTerm2, Ghostty, WezTerm, kitty and others use OSC 9; GNOME Terminal and other VTE
terminals, foot and urxvt use OSC 777; inside tmux, set -g allow-passthrough on). --exec runs a
command for each event with AGTD_EVENT_ID, _HOST, _SESSION, _KIND, _ATTENTION,
_SUMMARY and _TIME set (_SESSION is empty for a host event). Following reconnects by itself (after a daemon restart, or a lost
connection to a remote host) and resumes after the last event it printed.
The clearest signal is a program status report (OSC 7501): one escape sequence that says what the
program is doing and what it needs. agtd answers the protocol's detection query (ESC ] 7501 ; ? ST) for every session, attached or not, so a program that checks before reporting finds the
protocol supported. Any program can report:
printf '\e]7501;state=blocked:kind=permission:msg=%s\e\\' "$(printf 'Deploy?' | base64)"Status reports are the only attention signal. Typing into the session counts a blocked report
as answered until the program reports again; scrolling or moving the mouse over it does not. Bells and desktop notifications (OSC 9, OSC 777)
are recorded as info-level events — visible in agt events as facts about what the terminal
emitted — but raise no attention and trigger no --notify: agtd stopped inferring "needs
you" from them when the agent harnesses began reporting status themselves.
-
Claude Code reports program status natively since 2.1.295: permission prompts, questions and finished turns surface as
blocked/idlewith their message, with nothing to configure. -
Agents that do not report yet (Codex, older Claude Code, plain tools) show as running, with the foreground command and terminal title as context and a
stallednotice if a non-reporting foreground command is silent for half an hour. Attention arrives when their harnesses adopt the protocol — or today, from anything you wrap:mytool; printf '\e]7501;state=%s:msg=%s\e\\' \ "$([ $? -eq 0 ] && echo done || echo error)" "$(printf 'mytool finished' | base64)"
agtd focuses purely on agent runtime semantics:
- durable PTY-backed agent sessions that outlive the client connection that started them
- session metadata stored in
state.dbunder the resolved runtime root - PTY scrollback held by each session's worker process and saved to
logs/when the session ends - interactive reattach with
agt attach - background PTY input with
agt send-input - git state and artifacts (
agt diff,agt artifacts), read from the session's directory and streamed apart from terminal traffic
agtd is two Go binaries, built from one module at the repository root: the agt CLI and
the agtd daemon.
make build # builds libghostty-vt if needed, then bin/agtd and bin/agt
make agt # just the CLI: pure Go, no cgo or Zig needed
make test # Go tests, with -race
make test-cgocheck # the worker's tests with cgo's pointer checks (libghostty)
make test-linux # the same suite on Linux: directly on a Linux host, else in Docker
make shellcheck # lint every tracked shell script (CI runs this on pull requests)The daemon's session workers link libghostty-vt statically, tracking the ghostty commit pinned in
scripts/build-libghostty.sh (matching the Go bindings' CMakeLists.txt). That commit needs
Zig 0.17 or newer; the script fetches it and builds it under .build/. Point ZIG= at a 0.17
toolchain if the one on PATH is older. Zig is only needed when the library is (re)built; an
up-to-date build under .build/ is reused as is.
libghostty-vt is built ReleaseFast by default (GHOSTTY_OPTIMIZE overrides it). Zig's default
Debug build parses PTY output at roughly 50 KB/s, which is slow enough to throttle a busy agent;
BenchmarkTerminalFeed shows ~640 MB/s with ReleaseFast.
Only libghostty needs cgo; modernc.org/sqlite is pure Go, so cross compiling agtd is zig cc
plus CGO_ENABLED=1 as described in the go-libghostty README. scripts/release-build.sh does
that for the release binaries; see Releases.
To measure what sessions cost on this machine, agtd bench sessions starts a private daemon in a
temporary root (never ~/.agtd), creates the sessions, prints a report, and stops everything it
started, also on Ctrl-C. BENCHMARKS.md has the numbers and what they mean:
bin/agtd bench sessions --count 100 # idle sessions
bin/agtd bench sessions --count 0 --output-heavy 100 --attach 100
bin/agtd bench sessions --count 500 --json-file report.json--output-heavy N adds sessions that write output as fast as they can, --attach K attaches K
clients to one of them (--attach-mode raw|frames|both: the raw output, screen frames, or each in
turn), --duration sets how long CPU, throughput and fan-out are measured, and --fill-lines
sizes the session used for the full-scrollback snapshot. macOS allows 511
pseudo-terminals for all programs together (kern.tty.ptmx_max), so about 500 sessions at most.
For local development, run the freshly built CLI against the freshly built daemon without reinstalling:
make dev-run ARGS="list"On macOS 13+ or Linux (glibc 2.28+: Debian 10, Ubuntu 20.04, RHEL 8 or newer, Arch):
curl -fsSL https://agtd.sh/install.sh | bashThis downloads the latest release's binaries and
checks them against its SHA256SUMS; it needs nothing but curl and tar. AGTD_VERSION=0.2.0
picks a release. The macOS binaries are universal (Apple silicon and Intel), signed with a
Developer ID and notarized.
It builds from source instead when run as ./install.sh from a checkout (it builds that
checkout), when AGTD_REF (a branch, tag or commit) or AGTD_FROM_SOURCE=1 is set, or when no
release fits the machine (a musl Linux, or no release yet). Either way it installs agt and
agtd side by side in ~/.local/bin (AGTD_PREFIX changes the prefix) and can be run again
at any time:
- A source build needs macOS, Debian/Ubuntu or Arch. Missing base packages (git, make, a C
compiler, pkg-config, curl, xz) come from apt or pacman, through
sudowhen you are not root. On macOS it asks for the Xcode command line tools. Go and Zig are used fromPATHwhen new enough. Otherwise pinned releases are downloaded into~/.local/share/agtd/toolchainand used for the build only. Without a checkout it clonesAGTD_REF(defaultmain) into~/.local/share/agtd/src; a rerun fetches and rebuilds only what changed. - Binaries are only replaced when they differ from the installed ones. If a daemon is running and
agtdchanged (or the daemon still runs a binary replaced earlier), it runsagtd upgrade, so sessions are handed over to the new binary without stopping (AGTD_NO_UPGRADE=1leaves the daemon alone). - On Linux with systemd it installs and enables a user unit (
~/.config/systemd/user/agtd.service) and turns on lingering, so the daemon starts at boot without anyone logging in, and a rebooted devbox takes remote connections again. The unit only starts the daemon, from your login shell (sessions start through your login shell too, rc files included);agt daemon restartandupgradestill manage it, and stopping the unit never stops the daemon or a session. A daemon that crashes is not restarted by systemd. If lingering cannot be turned on, the unit is left disabled.AGTD_NO_SERVICE=1skips this.
It never turns on remote access; see agtd remote enable below.
For development, make install installs the binaries from the current tree into $GOBIN (or
$(go env GOPATH)/bin); set BINDIR to choose another directory.
Pushing a tag vX.Y.Z runs .github/workflows/release.yml, which publishes a GitHub release with
agtd-linux-amd64.tar.gz, agtd-linux-arm64.tar.gz, agtd-darwin-universal.tar.gz (each holding
agt, agtd and LICENSE) and SHA256SUMS:
scripts/release-build.sh PLATFORM VERSIONbuilds each platform, the same way locally as in CI. Linux binaries are linked byzig ccagainst glibc 2.28 and libghostty-vt is built for the architecture's baseline CPU, never the build machine's. macOS binaries are universal, for macOS 13+.- Each build is smoke tested by running
agtd bench sessions(a private daemon with real PTY sessions) from the built binary, and the Linux ones again in a Debian 10 container (glibc 2.28). - The macOS binaries are signed (hardened runtime, Developer ID Application) and notarized. The
secrets this needs are listed at the top of the workflow. A tag fails without them; run by hand
(
workflow_dispatch), the workflow is a dry run that skips signing when they are missing and leaves the tarballs as the run's artifacts instead of publishing. - Releases are never marked as prereleases, even for an alpha tag, because
install.shinstallsreleases/latest, which skips prereleases.
A session runs in the directory you give it. agt new defaults to the current directory;
pass --cwd DIR to pick another (it must exist). agtd does not create git worktrees,
branches, or merge anything back. If you want isolation between agents working on the same
repository, create the worktree yourself and point the session at it:
git worktree add -b agent/auth-refactor ../wt/auth-refactor main
agt new --cwd ../wt/auth-refactor auth-refactorA skill or a wrapper script can package this recipe. Two agents started in the same checkout will step on each other; that is your call, not the daemon's.
When a session starts in a git repository, agtd records HEAD's commit and branch as the
session's base. Everything is then measured from there:
agt diff NAMEprints one unified diff of everything that differs from the base: commits made since, staged and unstaged changes, and untracked files (respecting.gitignore).--statand--name-onlysummarise it. The diff applies to a checkout of the base withgit apply(binary files are only named in it; thepatchartifact carries them).agt status NAMEadds a git section: branch, HEAD, base, upstream with ahead/behind, the commits since the base, and the changed files counted by kind with line totals.agt artifacts NAMElists what can be downloaded, andagt artifact NAME ARTIFACT [-o FILE]downloads it:diff,patch(the commits since the base as an mbox, forgit am),historyandhistory.vt(the terminal history, plain or with escape sequences). With-othe file only appears once the artifact has arrived whole.
These work for ended sessions too, as long as their directory is still there. A session started
outside a repository, or before agtd recorded bases, is compared with HEAD instead.
agtd only reads: it runs git in the session's directory without taking locks, and never
writes to the repository, its index or its refs. Untracked files are included by marking them in a
private copy of the index. Lists are capped (200 commits, 1000 files) and say when they are. A
download that you stop reading (say, in a pager) simply waits; nothing piles up in the daemon.
Working directories are resolved on the machine running agtd, not the client. Give the
directories you work in a name, then start sessions by name:
agt workspace add mono '~/src/mono' # or: agt workspace add mono .
agt workspace ls
agt new --workspace mono auth-refactor # runs in ~/src/mono
agt new --workspace mono --cwd services/api api-fix # runs in ~/src/mono/services/api
agt new -w mono -C services/api api-fix # the same, with short flags
agt workspace rm monoWorkspaces are stored by the daemon in state.db, so they take effect immediately and are
managed entirely from the CLI. A quoted ~/ path is expanded by the daemon against its own
user's home; other paths are resolved by the CLI, which today always shares the daemon's
filesystem. The directory must exist: agtd does not clone, create, or sync workspaces.
With --workspace, --cwd is relative to the workspace root and may not leave it. A session
records the workspace it was started in (agt status shows it); removing or re-adding a
workspace does not move sessions that are already running.
This is the addressing a remote client will use: once agt --host exists, a laptop can start
a session in a devbox's checkout without knowing the devbox's paths.
The first agt command on a machine writes <runtime-root>/config.toml with Claude Code
(claude), Codex (codex) and pi (pi) configured. The default agent, the one agt new starts
without --agent, is the first of those three found on PATH at that moment (claude when
none is), so a machine with just one of them installed needs no configuration. Edit the file to
change it; it is never rewritten. For example:
default_agent = "claude"
[agents.claude]
command = "claude"
args = []
[agents.codex]
command = "codex"
args = []config.toml also sets how much scrollback agt attach copies into your terminal (more
below):
[attach]
scrollback_rows = 1000
# The wheel over a fullscreen agent's transcript (remote, under frames) is
# drawn at once and corrected by the agent's screen; false waits for it.
#predict_wheel = true
# A prototype: keep the terminal on its primary screen and push the
# transcript rows a fullscreen agent scrolls away into its scrollback.
#alt_scrollback = falseThe agent chooser's order (in the inbox and the overlay) follows the order of the [agents.*]
tables in this file. default_agent must name one of those configured agents. Without it, the default is claude if configured,
otherwise the first agent listed. With no config.toml at all, the built-in defaults are the
ones written above.
Each agent starts through your login shell, as an interactive login shell on the session's
terminal ($SHELL -l -i -c 'exec "$0" "$@"' claude ..., the way an ssh login starts a program):
the shell reads your profile and rc files, then execs the agent in its place. So an agent sees
what your own shells see, read afresh for every new session: PATH, version managers, and
variables that installers append to ~/.bashrc or ~/.zshrc, such as the base URL and token of
a local credential proxy. Those exports sit below the interactive guard of a stock ~/.bashrc,
which a daemon started at boot never reaches, and a session started earlier keeps the
environment it started with. sh, bash, zsh, dash, ksh and fish are supported; any
other $SHELL, or none, starts the agent directly with the daemon's environment, as
login_shell = false in an agent's table does:
[agents.codex]
command = "codex"
login_shell = falseAn rc file runs for every session, so one that takes over the shell (exec tmux, say) should
skip sessions, which have AGTD_SESSION_ID set. An agent the shell cannot find ends the session
with exit code 127 and a hint about the shell's PATH.
The daemon injects:
AGTD_SESSION_IDAGTD_CWDAGTD_SESSION_NAME(same asAGTD_SESSION_ID)TERM=xterm-256colorandCOLORTERM=truecolorwhen the daemon's environment has noTERM(a daemon started at boot or over ssh) orTERM=dumb: the agent's terminal is agtd's own, whatever started the daemon, and without them agents turn colour offTERM_PROGRAM=ghostty, always (replacing the daemon's own, and dropping itsTERM_PROGRAM_VERSION): the terminal is libghostty's, and agents such as pi only write clickable OSC 8 hyperlinks for a terminal they recogniseSSH_AUTH_SOCK, naming the session's own SSH agent socket (see Forwarding Your SSH Agent), set again after the login shell's rc files so one that points it elsewhere does not undo it
The daemon socket is not passed to agents. They are not sandboxed, though: an agent runs as your
user, so it can still reach the daemon at its usual path. There is no structured event channel for
agents: agtd reads attention from what they print (see Attention model).
Session status, attention, events, and history are the supported runtime surfaces.
Everything lives under one root, ~/.agtd on macOS and Linux alike, unless AGTD_DIR names
another. Without a home directory, XDG_RUNTIME_DIR/agtd, TMPDIR/agtd-<uid> and then
/tmp/agtd-<uid> are used instead; those may not survive a reboot.
The root contains config.toml, state.db, logs/, remote/ (keys for remote access),
hosts.toml, and the runtime files agtd.sock, agtd.lock, agtd.pid, sessions/ (one
socket per live session) and ssh/ (each live session's SSH agent socket). It is created private
to your user (0700), and agtd refuses a root owned by someone else.
Interactive PTY attach is available with agt attach <name>. Detach with Ctrl-\, switch to
the previous running session with Ctrl-[, or switch to the next running session with Ctrl-], or
agt detach <name> --attach <attach_id> for a specific client, or
agt detach <name> --all to disconnect every attached client on the session. Ctrl-Y opens agt's
palette over the session: switch session, new session, session details (which also say whether
the agent has the alternate screen up, as a fullscreen agent does) and stop, with detach always
last. With
[attach] submit_and_switch = true its first command is Submit and switch (so Ctrl-Y, Enter),
which sends Enter to the agent and moves you to the session that needs attention, or stays when
none does. Detaching prints the address to attach again with, detached from HOST/NAME.
Use agt attachments <name> to inspect the current attachment ids, which one sets the session's
size (size-owner), and the size each one has.
Attach clears the visible screen and repaints from the daemon's retained terminal state; if the
restored session was using the alternate screen, replay restores that state naturally. It also
copies the session's most recent 1,000 rows of scrollback into your terminal, so you can scroll
back through them natively; agt history has the rest. From a remote daemon the screen then
arrives as frames rather than the raw output: the worker sends the rows that changed, the client
paints them in place and acknowledges, and a slow link gets fewer, fresher frames instead of
dropped output and a flashing resync; rows that scroll off still land in your terminal's own
scrollback, and the local echo keeps the screen moving behind your typing. An agent that copies
to the clipboard over OSC 52 (pi does on a devbox) still reaches your local clipboard, if your
terminal allows programs to write it (Ghostty's clipboard-write). [attach] mode
chooses: auto (frames from a remote daemon, the raw output locally), frames or raw.
[attach] scrollback_rows in
config.toml changes how many (-1 for all of it, 0 for the screen only). When the client
reconnects by itself, or repaints after the overlay closes or after falling behind, it sends only
the screen, since your terminal already has the scrollback. A client
that falls behind a fast agent (a slow link, a stalled terminal) never slows the agent down: it
skips output it cannot keep up with and is repainted from the current screen once it catches up.
Typing is local. Over a slow link every key would otherwise wait a round trip for the agent to
draw it, so agt attach draws what you type in the agent's own input box at once, in the
agent's own wrapping and pen, and sends the keys as you type them, batched for at most 50 ms
([composer] flush_after in config.toml; enabled = false turns the local drawing off). The
worker finds the input box on the session's screen, so this works for Claude Code, Codex, pi and
a shell prompt alike, and never draws where it cannot be sure: in a menu, a permission prompt or
an editor, keys go raw as before. Text, Backspace, Delete, Left, Right and the Shift-Enter or
Alt-Enter newline are drawn locally, and over frames Enter on a prompt empties the box at once
(not on an empty box, a slash command, an @ mention or a backslash continuation, where Enter may
do something else); Tab, Esc, Ctrl-C, Ctrl-J, Ctrl-Enter and the other keys are never
predicted, so the agent's own menus, completions and history work as they always did, one round
trip late. Mouse input reaches the agent as it is, so in a fullscreen agent
(Claude Code, Codex, pi) the wheel scrolls its transcript while you type; from a remote daemon
each wheel tick is drawn at once from the agent's transcript, which the daemon records while the
agent runs, attached or not, and the agent's screen corrects the guess a round trip later
([attach] predict_wheel). Against an older daemon the transcript is only what scrolled past
since you attached, and rows above it come at the round trip. The agent's rendering replaces the drawing as soon as it arrives; the one thing you may
notice is that the screen behind the box stands still for about a round trip after you pause
typing. ARCHITECTURE.md, "The Input Field And Local Echo", has the details.
The CLI also tells the daemon its terminal's default colours (agt new and agt attach ask for
them once, at startup), and the worker answers an agent's colour queries with them at once. Codex
styles its composer by that answer and waits 250 ms for it, less than a round trip to a distant
host, so without this its composer has no background when attached from afar.
Every session has a UID (agt status shows it) as well as its name. A name can be reused once
its session is removed; the UID never is, so a reconnecting client can tell that the session under
a name is no longer the one it was attached to.
Multiple interactive attachers are allowed per session, and attaching from the inbox (agt) uses
the same shared attach path. Background PTY writes are still available with
agt send-input <name> -- <text>, and agt send-input --compose <name> -- <prompt> sends a whole
prompt the way the agent's terminal asks for input (a paste, bracketed when the agent turned that
on, then Enter as its keyboard mode wants it), for scripts and agt events --exec hooks.
agtd can accept remote clients over QUIC, so sessions on a devbox can be reached from a laptop
over a LAN, Tailscale or WireGuard, without any hosted service. Both sides authenticate with pinned
keys, like SSH host keys and authorized_keys.
On the devbox, remote access is off by default. Turn it on:
agtd remote enableThis finds the devbox's Tailscale address and listens on UDP port 7433 there. It sets
[remote] listen in config.toml, restarts the daemon (sessions keep running), and prints the
agt host add command to run on the laptop, with the daemon's key fingerprint included.
Without Tailscale, it offers the address of the interface holding the default route and asks first. A private LAN address is only reachable on that network and may change with DHCP; a public address is reachable from the whole internet, so it gets a stronger warning. To choose the address yourself (for example a WireGuard one), or to skip the question in a script:
agtd remote enable 10.8.0.2 # port 7433
agtd remote enable 10.8.0.2:9000
agtd remote enable --yes # accept the detected address
agtd remote status
agtd remote disableIf the address does not exist yet when the daemon starts (say, Tailscale is still coming up at
boot), the daemon keeps retrying every few seconds. agtd remote status and agt daemon info
show why it is not listening.
Pair the laptop with a short code, so neither machine's key has to be copied. On the devbox:
agtd remote pairPairing code: 482-193-675
On the machine you connect from, run:
agt host add devbox 100.64.0.5:7433 --code 482-193-675
Waiting until 14:05:12 (Ctrl-C to cancel)...
On the laptop, run that command. The port can be left out: agt host add devbox 100.64.0.5
means port 7433, the default. Or leave out --code: agt host add then shows the key the devbox
presents and asks for the code:
$ agt host add devbox 100.64.0.5:7433
The agtd at 100.64.0.5:7433 presents this key:
SHA256:...
To trust it, run `agtd remote pair` on `devbox` and enter the code it shows.
(Or press Enter to compare the key with `agtd remote id` there instead.)
Pairing code: 482-193-675
✓ Paired with `devbox` (100.64.0.5:7433)
The laptop pins the devbox's key, the devbox authorizes the laptop's key, and agtd remote pair
reports which machine paired. A mistyped code can be entered again. The code works once, expires
after 5 minutes, and closes after 3 wrong attempts. Both outcomes are recorded in agt events, with
the address the attempt came from. Neither end can be fooled into trusting a
machine that does not know the code: the two run a password-authenticated key exchange (CPace),
so someone on the network gets one guess per attempt and cannot test codes offline. No service
outside the two machines is involved.
A host you already added without pairing (one agt hosts shows as unauthorized) can be paired
the same way: run the agt host add command with the same name and address.
Or, without pairing, compare fingerprints. On the laptop, run the command enable printed:
agt host add devbox 100.64.0.5:7433 --fingerprint SHA256:...Or run it without --fingerprint and press Enter at the pairing code prompt: it then asks you to
confirm the key against agtd remote id. Either way it then prints the command that authorizes
the laptop.
Back on the devbox, run that command:
agtd remote authorize SHA256:... my-laptopNow any command can run on the devbox, with --host (or -H) or a host/session address:
agt new --cwd ~/repo devbox/auth-refactor # a path on the devbox (absolute or ~/...)
agt --host devbox workspace add mono ~/src/mono # name a directory on the devbox
agt new -w mono devbox/auth-tests # and start sessions in it
agt --host devbox ls
agt attach devbox/auth-refactor
agt send-input --compose devbox/auth-refactor -- "run the tests" # a whole prompt, then Enter
agt history devbox/auth-refactor
agt diff devbox/auth-refactor --stat
agt --host devbox events --follow --notify # tell me when a devbox session needs me
agt rm devbox/auth-refactorClosing the laptop or losing the network leaves the session running; agt attach again, from any
authorized machine, restores the screen. An attachment that loses its connection (or whose daemon
restarts or is upgraded, locally too) reconnects by itself: the bottom row says it is reconnecting, typing is ignored until it is back, and Ctrl-\
gives up. It keeps trying (every 5 seconds at most) until the devbox answers, so a laptop that slept
picks up where it was when it wakes. It stops if the devbox refuses this machine's key or the
session is gone, or was removed and replaced by a new session of the same name while you were away;
if the session ended meanwhile, it says how it ended instead.
A network change that keeps the connection alive (a new address behind the same NAT) needs no
reconnect at all. Commands whose connection drops after they were sent are sent again once the
devbox answers: lookups always, and new, kill, rm and workspace add|rm because they carry a
token that lets the devbox do them only once and answer the retry with the first result.
send-input is never sent twice. Without --agent, new uses the devbox's default_agent.
With several hosts, agt hosts (or agt host ls) checks every one at once, this machine's
daemon included as local:
NAME ADDRESS STATUS LATENCY SESSIONS CPUS AGENTS VERSION
local (this machine) online 310µs 2/5 10 claude,codex 0.1.0
devbox 100.64.0.5:7433 online 23ms 3/7 32 claude 0.1.0
gpu-01 10.0.0.9:7433 offline - - - - -
SESSIONS is running/total. A host can also be unauthorized (it refuses this machine's key) or
key changed. agt host info devbox shows its machine (OS, CPUs, memory), its agents, daemon
version, capabilities and pinned key. --host local (or local/session) is this machine.
Across every host at once:
agt # the inbox: every host's sessions, the ones that need you first
agt ls --all # one list, with a HOST column, what needs attention first
agt ls --all --json # one object per session, with its address and global id
agt events --all -f # follow every host's events (add --notify for alerts)A session's global id (agt status shows it) is its host's daemon key plus the session's UID.
Unlike host/name, it never changes when a host is renamed and never refers to a newer session
that reused the name.
agt --host auto new lets the CLI choose: among the hosts that are online, have the agent, and
(with --workspace) have that workspace, it picks the one with the fewest running sessions per
CPU, and says which and why. It is a hint for spreading work, not a scheduler. It needs
--workspace or an absolute or ~/ --cwd, which mean the same on every host:
agt --host auto new --workspace mono fix-flaky-testsManaging keys and hosts:
agt remote id # this machine's client key fingerprint
agt host ls # with --no-probe: hosts.toml as is, with pinned keys
agt host info devbox
agt host rm devbox
agtd remote pair # on the devbox: authorize a machine with a one-time code
agtd remote list # on the devbox: authorized clients
agtd remote revoke SHA256:...An authorized client has the same access as you have locally, except that only the devbox itself
can stop its daemon. Authorizing takes effect immediately; revoking a key also disconnects that
client within a few seconds, ending any attachment it has open. agt daemon info works remotely; restart and upgrade only
manage the local daemon.
Troubleshooting:
- "refused this machine's key": the laptop is not authorized; pair it (
agtd remote pairon the devbox), or run theagtd remote authorizecommand from the error there. - "is not pairing": no
agtd remote pairis waiting on the devbox, or its code expired or was closed by wrong attempts; run it again and use the new code. - "the key of host ... has changed": the devbox presented a different key from the pinned one.
If you know why (for example
remote/daemon.keywas recreated), runagt host rmandagt host addagain. Otherwise, treat it as a possible interception. - "timed out connecting": check
[remote] listenon the devbox, that the daemon was restarted, and that UDP reaches the port (firewalls often allow TCP only).
An agent on the devbox that pushes with git, or signs commits, can use the SSH keys on your
laptop, as ssh -A would, with no key copied to the devbox. Turn it on in the laptop's
~/.agtd/config.toml:
[ssh_agent]
forward = true
# agent = "~/.1password/agent.sock" # the agent to lend; $SSH_AUTH_SOCK by default
# keys = ["SHA256:..."] # offer only these keys, as `ssh-add -l` prints themFrom then on every attachment lends your agent to its session: agt attach, agt new, the
inbox, the overlay's switch, and every reconnect. Programs in the session find it at their
SSH_AUTH_SOCK, which every new session has: a socket of the session's own, which answers from
the agent of whichever attachment owns the session's size. Take over the size (agt attach --take-size, or
Take over size in the overlay) and your keys come with it. A connection lost for a moment, or a
daemon restart, does not lose them: a push made meanwhile waits for you to reattach, or for agt to
open its forwarder again, rather than use the devbox's keys. When nobody
driving the session forwards an agent, the socket answers from the agent the devbox's daemon was
started with, or with no keys, and ssh falls back to key files on the devbox. While a forwarding
attachment owns the size, only its keys answer: the devbox's own agent (a deploy key in it) is not
asked, and adding keys is refused.
SSH_AUTH_SOCK is set even when no agent is behind the socket. ssh-add -l then reports that
the agent has no identities (exit status 1) rather than that it cannot connect (2), and ssh-add
cannot load keys into it. A shell setup that starts its own ssh-agent when none answers should
test whether ssh-add -l succeeds, not for status 2 or an empty SSH_AUTH_SOCK; or the devbox
can set session_socket = false.
Your private keys never leave the laptop. Only requests to list keys and to sign cross the
connection; agt refuses anything else (adding, removing or locking keys) and, with keys, any
other key. A mistake in [ssh_agent] stops agt forwarding: a setting it does not know (names
have a case), a table spelled [SSH_Agent] or [ssh-agent], keys under another table, a key
that is not a fingerprint, or a value of the wrong type (forward = "yes"). agt says why before
and after an attach, and the inbox shows it until you press a key; agtd logs it when it starts.
So a misspelled or misplaced keys cannot offer every key, and agtd and agt's other settings
carry on. An agent that confirms each use (1Password, a security key's touch)
keeps doing so. A key you limited with ssh-add -h is refused through agt on another host, as
through ssh -A to a host the limit does not name. agt lends an agent only when it answers and
holds a key it may offer; otherwise the session keeps what it falls back to. Each signature is
recorded in the session's events (agt events) with the key, what it signed (a login, as whom
and, when your agent accepted ssh's binding of it, to where; or git's commit signing) and which
client signed.
What it does not protect: while your attachment owns a session's size, any process running as your
user on the devbox can ask your laptop for signatures, as with ssh -A. It stops the moment you
detach. For work that must push while nobody is attached, use a deploy key or a token on the
devbox. On the devbox itself, ssh -A devbox and then agt attach, with forward = true in the
devbox's own config, lends that login's agent to the session.
[ssh_agent] session_socket = false on the devbox starts new sessions without a socket of their
own; they keep the SSH_AUTH_SOCK the daemon was started with, unless that is another session's
socket (a daemon started from inside a session), which they go without. Sessions started before
agtd had this cannot forward until they are restarted.
scripts/do-devbox.sh does all of the above on a new droplet, with doctl (authenticated) on the
laptop:
scripts/do-devbox.sh create devbox # sgp1, s-2vcpu-4gb; --region, --size, --ref, --local
agt --host devbox new --cwd '~' auth-refactor
scripts/do-devbox.sh ls
scripts/do-devbox.sh destroy devboxcreate puts the droplet behind an agtd cloud firewall (SSH and UDP 7433 in), creates a sudo
user named after you with your SSH keys, runs install.sh there (so the daemon starts at boot),
installs Claude Code, listens on the droplet's public address, and pairs this machine with it.
Sign in to Claude Code in the first session you start there. destroy deletes the droplet and
removes the host; it only touches droplets tagged agtd.
Try restarting the daemon:
agt daemon info
agt daemon restart
agt daemon upgradeagt daemon restart is safe while sessions run: they keep running and reattach to the new
daemon. agt daemon upgrade (or agtd upgrade, run as the new binary) is too: it replaces the
daemon, then hands each running session's worker over to the new binary without stopping the agent
(a live handoff), and lists what happened to each session:
✓ Upgraded daemon
✓ Session fix-tests now runs /usr/local/bin/agtd (live handoff, pid 4242)
! Session old-work keeps running on its previous binary: ...
A session whose handoff cannot be done (its worker predates live handoff, or the new binary does not run) keeps running on the binary it has. Attached clients, local or remote, reattach by themselves after a daemon restart or a handoff and repaint the screen; output the agent writes meanwhile is kept. ARCHITECTURE.md lists exactly what survives each kind of restart and crash.
agt starts the daemon on demand with agtd serve --daemonize, using the agtd binary
installed next to agt. Set AGTD_BIN to use a different daemon binary, for example a local
build:
AGTD_BIN=$PWD/bin/agtd agt daemon restartCurrent capabilities include:
- local
agtddaemon over a Unix socket - remote sessions over QUIC with
agt --hostand pinned keys (preview) - PTY-backed agent processes that outlive client connections
- sessions that survive the daemon stopping, crashing, restarting, or being upgraded: each runs in its own worker process, a new daemon picks it up again, and an upgrade hands the worker itself over to the new binary with the agent still running
- SQLite-backed session metadata
- per-session PTY history held by the session while it runs and saved to
logs/when it ends - git state and artifacts of a session's repository (
agt diff,agt artifacts), locally and remotely, with large transfers on their own streams so they never stall an attachment - SSH agent forwarding: programs in a session use the SSH keys of the machine attached from
(
[ssh_agent] forward), which never leave it - attention and activity read from each session's terminal output, kept as events, followed with
agt events --followand surfaced inagt ls,agt statusand the TUI
Sessions whose worker dies while no daemon is running are shown as unknown_recovered.
