Visitar URL original
GitHub - robmorgan/agtd: ✨ A daemon-backed workspace for coding agents - like tmux, but built for AI-native software development. · GitHub
Skip to content
robmorganPublic

About

✨ A daemon-backed workspace for coding agents - like tmux, but built for AI-native software development.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

agtd logo agtd logo

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.

How It Works

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.

Native Terminal First

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.

Example

Start a task:

agt new fix-tests

This 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-tests

Multiple 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 --all

Inspect 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 --vt

Review 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 stats

Stop a task:

agt kill fix-tests

Remove a session and clean up its artifacts:

agt rm fix-tests

Or use the compatibility form:

agt kill --rm fix-tests

Run 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.

Core Concepts

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.

Attention model

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.

Making agents ask out loud

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/idle with 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 stalled notice 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)"

Architecture

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.db under 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

Build

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"

Install

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 | bash

This 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 sudo when you are not root. On macOS it asks for the Xcode command line tools. Go and Zig are used from PATH when new enough. Otherwise pinned releases are downloaded into ~/.local/share/agtd/toolchain and used for the build only. Without a checkout it clones AGTD_REF (default main) 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 agtd changed (or the daemon still runs a binary replaced earlier), it runs agtd upgrade, so sessions are handed over to the new binary without stopping (AGTD_NO_UPGRADE=1 leaves 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 restart and upgrade still 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=1 skips 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.

Releases

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 VERSION builds each platform, the same way locally as in CI. Linux binaries are linked by zig cc against 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.sh installs releases/latest, which skips prereleases.

Working Directories And Worktrees

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-refactor

A 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.

What A Session Produced

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 NAME prints one unified diff of everything that differs from the base: commits made since, staged and unstaged changes, and untracked files (respecting .gitignore). --stat and --name-only summarise it. The diff applies to a checkout of the base with git apply (binary files are only named in it; the patch artifact carries them).
  • agt status NAME adds 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 NAME lists what can be downloaded, and agt artifact NAME ARTIFACT [-o FILE] downloads it: diff, patch (the commits since the base as an mbox, for git am), history and history.vt (the terminal history, plain or with escape sequences). With -o the 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.

Workspaces

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 mono

Workspaces 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.

Configure Agents

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 = false

The 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 = false

An 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_ID
  • AGTD_CWD
  • AGTD_SESSION_NAME (same as AGTD_SESSION_ID)
  • TERM=xterm-256color and COLORTERM=truecolor when the daemon's environment has no TERM (a daemon started at boot or over ssh) or TERM=dumb: the agent's terminal is agtd's own, whatever started the daemon, and without them agents turn colour off
  • TERM_PROGRAM=ghostty, always (replacing the daemon's own, and dropping its TERM_PROGRAM_VERSION): the terminal is libghostty's, and agents such as pi only write clickable OSC 8 hyperlinks for a terminal they recognise
  • SSH_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.

Remote Access (Preview)

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 enable

This 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 disable

If 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 pair
Pairing 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-laptop

Now 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-refactor

Closing 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-tests

Managing 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 pair on the devbox), or run the agtd remote authorize command from the error there.
  • "is not pairing": no agtd remote pair is 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.key was recreated), run agt host rm and agt host add again. Otherwise, treat it as a possible interception.
  • "timed out connecting": check [remote] listen on the devbox, that the daemon was restarted, and that UDP reaches the port (firewalls often allow TCP only).

Forwarding Your SSH Agent

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 them

From 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.

A Devbox On DigitalOcean

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 devbox

create 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.

Troubleshooting

Try restarting the daemon:

agt daemon info
agt daemon restart
agt daemon upgrade

agt 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 restart

Status And Limitations

Current capabilities include:

  • local agtd daemon over a Unix socket
  • remote sessions over QUIC with agt --host and 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 --follow and surfaced in agt ls, agt status and the TUI

Sessions whose worker dies while no daemon is running are shown as unknown_recovered.

About

✨ A daemon-backed workspace for coding agents - like tmux, but built for AI-native software development.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages