IDE → ADE: Agentic Development Environment. Kotgent organizes development around coding agents: you define the work, coordinate agent sessions, review results, and step in when needed. Tasks, plans, and persistent terminals share one workspace you can access across devices.
kotgent is a local-first control plane for running Claude, Codex, Junie, or a login shell inside
tmux and supervising those sessions from a terminal, a desktop browser, or an installed PWA.
It is for developers who want long-running coding work to continue after the IDE or browser closes, and who want to move between a desktop, tablet, and phone without moving the process itself. Kotgent provides:
- durable agent processes in an isolated
tmuxserver; - one live session list and terminal shared by every client;
- attention notifications when an agent needs a human response, plus early weekly quota-reset notices;
- shared Claude and Codex usage meters in the sidebar;
- restart-safe session state and a project task backlog stored locally on the host.
Kotgent does not replace a provider's conversation storage. Claude, Codex, and Junie still own their transcripts and resume behavior; kotgent records enough state to reconnect the right process and client.
There are two distinct kinds of durability, and kotgent leans on both instead of trying to make tmux
immortal:
- Close the IDE / reload the browser — the agent keeps running in
tmux(a client detached, the process did not die). - Reboot the machine — the process is gone, but the conversation is preserved on disk by the provider
itself and is restored with
resume. See the agent guides for recovery behavior and limits.
- What kotgent is
- Install with Homebrew
- Install on Linux
- Cloudflare Tunnel
- Install the PWA in Safari
- Architecture
- Requirements
- Agents
- Build & test
- The CLI — the task backlog, access & auth, Web UI
- Troubleshooting
- How a session stays available
- Status & limitations
- Contributing
- License
Kotgent supports macOS on Apple Silicon. Install it from the Homebrew tap; the formula also installs
tmux:
brew install Heapy/tap/kotgentRun the setup from a normal login shell. kotgent install creates and starts a per-user launchd agent,
capturing that shell's PATH and UTF-8 locale. Start a plain shell for the provider-free first run, then
open the Web UI:
kotgent install
kotgent start shell
kotgent webTo run a coding agent instead, its CLI must already be on that login shell's PATH:
kotgent start claude # or: codex, juniekotgent list shows every session and its state. kotgent attach <id> opens its terminal, and closing a
terminal or browser only detaches that client—the process keeps running in tmux.
Upgrades are brew upgrade kotgent, followed by kotgent install again: the plist records the
binary's real (version-qualified) Cellar path, which a new release invalidates.
To build from source instead, see Build & test.
Linux x64 and ARM64 releases support glibc 2.35 or newer, with Ubuntu 22.04 as the tested baseline. Releases are published on GitHub Releases. Alpine/musl is not supported.
On Ubuntu, install runtime dependencies:
sudo apt-get install tmux curl libcurl4 libsqlite3-0 perl openssl ca-certificatesDownload, verify and unpack a release, then link it onto your PATH. Set version to the release and
arch to linux-x64 or linux-arm64:
version=0.10.0 arch=linux-x64
name="kotgent-$version-$arch"
base="https://github.com/Heapy/kotgent/releases/download/v$version"
cd "$(mktemp -d)"
curl -fLO "$base/$name.tar.gz"
curl -fLO "$base/$name.tar.gz.sha256"
sha256sum -c "$name.tar.gz.sha256"
mkdir -p ~/.local/opt ~/.local/bin
tar -xzf "$name.tar.gz" -C ~/.local/opt
ln -sfn ~/.local/opt/"$name"/kotgent ~/.local/bin/kotgentThe daemon finds the Web UI next to the executable's real path, so link kotgent, never copy it: a copy
without its adjacent resources/webui directory cannot serve the Web UI, and /auth answers 503.
~/.local/bin must be on your PATH; the default Debian and Ubuntu ~/.profile adds it when the
directory exists at login. Run from your normal login shell:
kotgent install
kotgent start shell
kotgent webinstall writes kotgent.service under ${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user, captures your
PATH and UTF-8 locale, and enables and starts the service. Re-run it after upgrading or moving the
executable. Restart with systemctl --user restart kotgent.service; inspect logs with
journalctl --user -u kotgent.service. Stopping, restarting, or uninstalling the service preserves
existing tmux sessions.
To upgrade, run the same download block with the new version, then kotgent install again. The service
records the executable's real, versioned path, so it keeps running the old release until then. Remove the
old ~/.local/opt/kotgent-<version>-<arch> directory only after that.
For an unattended server, optionally enable the user manager after logout with
loginctl enable-linger "$USER". Installation does not change lingering. Without a systemd user
manager, run kotgent daemon in the foreground. kotgent web prints the sign-in form URL and code, then
launches a browser through xdg-open from the xdg-utils package when DISPLAY, WAYLAND_DISPLAY or
BROWSER is set.
Remote browser and PWA access is optional. The native daemon deliberately binds only to
127.0.0.1:27508 and does not terminate TLS, so never publish that port directly. Use a named
Cloudflare Tunnel with a
Cloudflare Access
policy in front of it. This boundary matters: the Web UI exposes a terminal that can run arbitrary
commands on the host.
-
Put a domain on Cloudflare and install
cloudflaredon the Mac:brew install cloudflared
-
In Cloudflare Dashboard → Networking → Tunnels, create a named tunnel. Use the connector command for your host OS shown by the dashboard so
cloudflaredruns as a service, then add one Published application route:Setting Value Public hostname kotgent.example.comService http://127.0.0.1:27508 -
In Zero Trust → Access controls → Applications, protect that exact hostname with a self-hosted application and an Allow policy restricted to your identity. Do not use an
EveryoneorBypasspolicy. -
Tell kotgent which public origin it may accept, then restart the daemon so the allowlist takes effect:
kotgent config set public-url https://kotgent.example.com # macOS: launchctl kickstart -k "gui/$(id -u)/io.kotgent.daemon" # Linux: systemctl --user restart kotgent.service
Use your own hostname in both places. A stable named tunnel is required; a temporary trycloudflare.com
URL cannot be configured as the durable PWA origin. Only the browser surface crosses the tunnel—CLI and
provider-hook credentials remain loopback-only. Finally, open the public hostname in a private Safari
window and verify that Cloudflare Access appears before kotgent's sign-in form; if it does not, fix the
Access policy before installing the PWA.
Open Kotgent in Safari before installing it:
- on the host, use
http://127.0.0.1:27508/auth; - on another Mac, an iPad, or an iPhone, use
https://kotgent.example.com/authand complete Cloudflare Access first.
Install the app before redeeming a kotgent sign-in code. Safari and an installed web app have separate cookie storage, so a login completed in the browser does not reliably sign in the installed app.
Safari web apps require macOS Sonoma 14 or later. In Safari, choose File → Add to Dock (or Share → Add to Dock), choose the name, and click Add. Launch Kotgent from the Dock or Spotlight. See Apple's Safari web-app guide.
In Safari, tap More → Share (or the Share button, depending on the tab layout), choose Add to Home Screen, enable Open as Web App, and tap Add. Launch Kotgent from its Home Screen icon. See Apple's iPhone guide.
In Safari, tap Share → More → Add to Home Screen, enable Open as Web App, and tap Add. Launch Kotgent from its Home Screen icon. See Apple's iPad guide.
After launching the installed app, complete Cloudflare Access again if prompted. Generate a fresh,
single-use kotgent code with kotgent web, or with the phone button in an already signed-in desktop Web
UI opened on the host, and type that code into the installed app. On iPhone and iPad, server-sent Web
Push requires iOS or iPadOS 16.4 or later and notifications must be enabled from the installed app.
IDE terminal / CLI ────────────────────────────┐
Local browser / Safari web app ────────────────┼──▶ kotgent daemon ──▶ tmux ──▶ agent | shell
Remote Safari / PWA ─▶ Access ─▶ Tunnel ───────┘ │ │
│ └──▶ Web Push service
├──▶ SQLite event log + task backlog
└──▶ provider adapters + hooks
The daemon is the control plane, while tmux owns the live process and each provider owns its
conversation history. Adapters normalize provider-specific hooks into canonical events; a pure reducer
folds the append-only event log into the current session projection. One upstream tmux attach per
session is fanned out to every terminal client.
| Layer | Responsibility |
|---|---|
core/ |
Host-free events, session reducer/projection, quota rules, and notification models. |
adapter/ |
Claude, Codex, Junie, and shell launch/resume behavior plus event normalization. |
daemon/ |
Session lifecycle, reconciliation, provider-id capture, and task coordination. |
tmux/, pty/ |
Isolated process hosting and the single-upstream terminal fan-out. |
store/, task/ |
SQLite session/usage history, notification inbox, and project backlog. |
transport/ |
Ktor REST, WebSocket, authentication, and static PWA endpoints. |
push/ |
Attention tracking, early-reset inbox projection, subscriptions, VAPID signing, and Web Push delivery. |
service/, platform launchd/ and systemd/ |
Per-user daemon installation and environment capture. |
State is local and restart-safe; remote access publishes only the authenticated browser surface. See docs/INTENT.md for product boundaries, CLAUDE.md for architecture invariants, and docs/TESTING.md for the verification strategy.
- macOS on Apple Silicon, or Linux x64/ARM64 with glibc 2.35+. Targets are
macosArm64,linuxX64, andlinuxArm64. Linux service installation uses a systemd user manager. - Source builds only: JetBrains Kotlin Toolchain — invoked through the bundled
./kotlinwrapper committed in the repo. You do not need a separate install or Gradle; the wrapper provisions the toolchain (0.13.0) on first run. A JDK is required for the toolchain, for the build-time SQLDelight codegen plugin, and for the JVM-side browser tier (webuitest), whose first run additionally downloads Playwright's browser bundle — see Build & test. - Source builds and tests: Node v24 or newer and npm. Vite builds the Web UI from the npm project in
webui/, and Node's built-in runner (node --test) tests its TypeScript sources through type stripping. Runnpm ci --prefix webuito install the pinned dependencies. A missingnodeor missing dependencies fails the test tier rather than skipping it. An installed kotgent needs neither Node nor npm. tmux— sessions live on a dedicated server socket (tmux -L kotgent), isolated from your normaltmuxand from your~/.tmux.conf: kotgent passes-f /dev/nullon every invocation, so none of your config is loaded into an agent's pane — not your prefix key, bindings, plugins,status-formatordefault-terminal. This is deliberate. A~/.tmux.confis written for a terminal you drive, and one line of it (set -g destroy-unattached on) would kill the agent every time the last viewer detaches. In its place kotgent forces its own small set:destroy-unattached off,default-terminal tmux-256color,mouse on,status off,history-limit 10000,escape-time 10.mouse onis what makes the wheel scroll an agent's transcript — that scrollback lives in the tmux pane, so it is the only way a browser tab that joined an existing session can see anything above the current screen. kotgent arms mouse reporting for every viewer as it joins, but the single upstream uses the last-resizing viewer's geometry; only that viewer has a fully live wheel, and a larger tab's lower/right area may not scroll. Two other things to know: selecting text in the web terminal needs Option-drag on macOS (Shift-drag elsewhere), because a mouse-reporting terminal otherwise sends the drag to the app; and a wheel scroll puts the pane into tmux copy-mode, which every viewer shares — kotgent leaves copy-mode before programmatic Interrupt/REST input. Interrupt returns only after tmux verifies delivery; REST input reports when full PTY write completion was not observed. A PTY error may have written a prefix, so inspect the terminal before resending to avoid duplicated input.focus-eventsstays off: with one tmux client fanned out to many viewers, "is the terminal focused" has no single answer. Developed against tmux 3.7b.- An agent CLI, installed and available on the login shell's
PATHwhen you runkotgent install. Only the agent you want to use is required; a plain shell needs no additional CLI. See Agents. /usr/bin/openssland a platform HTTPS engine (Web Push). kotgent lazily uses/usr/bin/opensslto generate and sign its VAPID P-256 credential. HTTPS uses Darwin/NSURLSession on macOS and libcurl on Linux, with certificate verification against the system trust store. If VAPID setup fails, the daemon and in-tab notifications keep working; only server-sent push is unavailable.
These user guides describe how each integration currently works: setup, launch, resume and import, configuration, session status, usage meters, and known limitations.
| Session type | Start command | User guide |
|---|---|---|
| Claude Code | kotgent start claude |
Claude Code |
| Codex | kotgent start codex |
Codex |
| Junie | kotgent start junie |
Junie |
| Login shell | kotgent start shell |
Shell |
Sessions of any type serialize shared work, such as builds, with mutexes.
Structured plans attach browser review, questions and edit history to backlog tasks,
then coordinate workers and verified findings through execution. The Kotgent plugin's make-plan and
exec-plan skills guide authoring, review, worker worktrees and final human review.
SQLDelight generation comes from ktc-sqldelight, installed
with the committed ktc-plugins 0.3.0 wrappers. Its sources are vendored under plugins/sqldelight, so
a fresh checkout already contains the generator. The lockfile pins the upstream commit and exported
runtime catalog entries, whose version.ref shares the producer's SQLDelight version.
Run ./ktc-plugins verify to check them; update with
./ktc-plugins update sqldelight --commit <full-sha> and commit the resulting source/catalog/lock changes.
Make generator changes upstream rather than editing the installed copy.
# macOS (use linuxX64 instead of macosArm64 on an x64 Linux build host):
npm ci --prefix webui
npm run typecheck --prefix webui
npm run build --prefix webui
./kotlin build -p macosArm64 -p jvm
./kotlin do kexePath
./kotlin test -p macosArm64 -p jvmThe Web UI sources are strict TypeScript/TSX in webui/. Vite writes the served files to the gitignored
resources/webui/; it strips types, so typecheck is a separate gate. The main application and /auth
are separate Preact entry points in the same build. Sign-in loads only the auth component, Preact and its
styles; it starts no application API reads or WebSockets before login. The server injects the exchange
path, code length and lifetime into the built auth shell and retains ownership of ticket redemption and
session cookies. Both entry points require the built assets. Content hashes give changed assets
new URLs and make cache invalidation automatic: hashed files under assets/ are immutable; source maps
revalidate because their names follow the chunk, not their own bytes. All other static files revalidate,
and the daemon needs no asset revision bookkeeping. The build uses Vite 8's default browser target,
Baseline widely available. CI's webui job type-checks and builds this tree once, and the platform
jobs download it.
./kotlin test -p <host-target> -p jvm runs every tier: the native suite (test/), the browser tier
(webuitest/, a real Chromium driven through Playwright), the browser-independent TypeScript tier
(webuitest/js/ under node --test, spawned by WebUiLogicTest), JVM tests for the build-info
plugin, real-PTY tests under test/pty/ and harness self-checks in webuicheck/test/. The
two module tasks — :kotgent:testMacosArm64Debug and :webuitest:testJvm — are the fast local loops;
neither replaces the aggregate. The counts are deliberately not written here: they move with every
change, and the run itself is the only source of truth that cannot go stale (AGENTS.md carries the
current baseline for the one purpose a number serves — noticing that a change moved it by more than it
meant to).
Build the Web UI before ./kotlin build, then run ./kotlin test. The test task never links a main
binary, and every browser test execs webuicheck, so a missing webuicheck reddens the whole browser tier
— explicitly, rather than passing quietly. See Status & limitations for why it
is a separate binary at all.
The first test run downloads browsers, and needs the network for it. Playwright provisions its
browser bundle into ~/Library/Caches/ms-playwright on macOS or ~/.cache/ms-playwright on Linux, because
Playwright.create() installs Chromium, the headless shell, ffmpeg, Firefox and WebKit as one set even
though every test here asks for Chromium. The Java artifact supplies its Node driver; the served Web UI
requires the Vite build above. CI uses the matching, pinned Playwright CLI through npm exec
to install Chromium only, then disables Java's automatic browser download. It caches browsers under
an OS/architecture/version key. When changing playwright in gradle/libs.versions.toml, update both
the CLI version and cache key in .github/workflows/ci.yml.
Shared runtime and tests stay in the root library; thin kotgent-macos and kotgent-linux modules link
executables under build/. Use ./kotlin do kexePath after a host build (releaseKexePath for a release
build). For a cross build set KOTGENT_TARGET_PLATFORM=linuxArm64 on the path command. Unsupported or
missing targets fail instead of selecting another architecture's binary. kotgent below refers to that binary. The command reads
what build left behind rather than triggering it — the toolchain has no way for a plugin task to
depend on the native link — so run it after a successful build, or it fails saying so.
It prints the path and also writes it to build/kexe-path, which is what a script should read: a task
action's stdout reaches you through the build log, so it is prefixed, interleaved with the log's own
lines, and silenced by --log-level before the surrounding noise is. The file follows --build-dir
along with everything else, and a failed lookup deletes it rather than leaving a stale answer behind.
npm run build --prefix webui
./kotlin build -p macosArm64 -p jvm # use linuxX64 on Linux
./kotlin do kexePath
kexe=$(cat build/kexe-path)To install the current checkout as the kotgent on your PATH, stage its binary and Web UI together,
and restart the daemon service, use the repository's installer (with ~/.local/bin on your PATH):
./install-local.shPass --no-daemon when you want to stage the source build without replacing the running daemon. Pass
--webui-only after a TypeScript, TSX, CSS or HTML change: it rebuilds webui/ before replacing the
installed Web UI files, without restarting the daemon, and a browser reload picks them up. It needs a
full install first, and the installed binary must already serve every API the new Web UI calls.
For Web UI work, run ./run-daemon.sh: it installs dependencies when needed, builds the Web UI, and
keeps it rebuilt on save while the daemon serves the checkout's resources/webui/. No second terminal
is needed; reload the browser after a completed build in build/webui-watch.log. Press r to rebuild
and restart only the Kotlin daemon. The script stops its Web UI watcher when it exits.
Linux source builds use Ubuntu 22.04 and need libsqlite3-dev, libstdc++-11-dev, and
zlib1g-dev in addition to the runtime dependencies. The build links against Ubuntu’s glibc
and GCC 11 runtime libraries; this fixes the release baseline at glibc 2.35. Native Linux tests also use
Python 3 and systemd-analyze; the browser tier needs Chromium's system libraries. CI lists those
packages explicitly.
Kotlin/Native cannot run its compiler on a Linux ARM64 host. On a disposable Ubuntu 22.04 x64 build
machine, scripts/install-linux-build-deps.sh linuxArm64 installs multiarch dependencies (and adjusts
APT sources), then ./kotlin build -p linuxArm64 cross-compiles. Run the resulting executables on ARM64.
scripts/bundle-arm64-checks.sh packages native test binaries for that runner; the serving tests among
them read the built Web UI from resources/webui in the checkout they run in, so copy that directory
there as well (CI downloads the webui artifact into it). macOS source builds
remain native. Run Kotlin invocations and aggregate tests serially.
kotgent <command> [args]
daemon [--port N] run the control-plane server (default port 27508; the service entry point)
install | uninstall (un)install the per-user launchd or systemd service
start <agent> [cwd] start a session (agent: 'claude' | 'codex' | 'junie' | 'shell'; cwd defaults to the current dir)
[--name N] [--tag T] [--task R]
[--prompt-file F] [--parent ID] [--read-only]
import <agent> <session-id> register a session started outside kotgent, then resume it
[--cwd D] [--name N] [--tag T] [--no-start]
list | ls list sessions and their states
stop <id> stop a session
resume <id> resume a stopped/crashed/resumable session (never a lost one)
interrupt <id> send Ctrl-C to un-stick a session
attach <id> attach a raw terminal to a session
session done <id> stop and archive a session
session rename <id> <name> rename a session (an empty name restores the automatic label)
The task backlog (JSON on stdout — written for an agent to parse). Every subcommand that
resolves a session takes [--session S] to name it instead of the calling tmux pane.
task add <title> create a task [--body B] [--project P]
task list the project's backlog, in rank order [--project P]
task show [<ref>] one task in full
task next take the next eligible task [--project P] (exit 3: none)
task claim <ref> link this session to a task
task comment [<ref>] -m TEXT add a comment ('-m -' reads the text from stdin)
task review [<ref>] [-m TEXT] move the task to review
task done [<ref>] [-m TEXT] close the task and unlink every session holding it
task unlink [<ref>] drop this session's link; the task's state is untouched
task move <ref> --top | --bottom | --before <ref> | --after <ref>
task dep add|rm <ref> --on R add or remove "<ref> depends on R"
task delete <ref> remove the task, its dependencies and its feed
project list the live projects, or the deleted ones [--archived]
project init [<path>] write .kotgent.json for a project [--name N]
project delete <uuid> hide a project everywhere; its file, tasks and sessions stay
project restore <uuid> bring a deleted project and its backlog back
web [--print] open the Web UI in a browser (or print the login URL)
token rotate re-mint the master token (old key stops authenticating)
config get | set public-url <url> read / set the public URL published behind the tunnel
--version | --help
-
daemonbinds127.0.0.1:27508by default (27508=0x6b74= ASCII "kt"). Override the daemon's listen port with--port. The$KOTGENT_PORTenvironment variable does not change the daemon's port — it tells the CLI client (list/start/stop/attach/…) which port to reach a running daemon on. This is the process the per-user service runs on login. -
install/uninstallmanage a per-user service: a LaunchAgent on macOS or a systemd user unit on Linux. The service starts on login and restarts after a crash.installsnapshots your shell'sPATHandLANG, so the daemon can findclaude/codex/junieand render a UTF-8 TUI. Re-run it from a full shell whenever either goes stale. An agent missing from the daemon'sPATHfails with an error pointing atkotgent install. Both service definitions set the soft open-file limit to 1024 for viewers, attached sessions and hook bursts. -
startcreates atmuxsessionkt-<id>, launches the requested agent or login shell in it, and records the session.--prompt-file Fgives a new Claude or Codex conversation its first instruction (at most 256 KiB);--read-onlylaunches it read-only on every start and resume;--parent IDrecords the session as a child of another one, for example a worker of an orchestrating agent. Prompt and read-only are fixed at start and fail for agents that do not support them; see the agent guides. -
importbrings a conversation you started outside kotgent under its control, with its history intact. The import first verifies the provider's on-disk record and registers aresumableentry, then resumes it intmux;--no-startleaves it registered for later. The project directory is discovered from the provider's record; pass--cwdif discovery fails or picks the wrong directory. See the agent guides for finding a provider session id and provider-specific import limits.Importing an id kotgent already tracks fails with the existing session's id and the right next move (
kotgent resume <id>, or Restore in the Web UI if that session is archived). The Web UI's new-session dialog has a matching Import mode, including the register-only checkbox. One caveat: kotgent cannot detect that the conversation is still live in the original terminal — resuming it there and under kotgent at once runs two CLI copies of the same conversation. -
attachis not a directtmux attach. It is a raw-terminal passthrough over the daemon's terminal WebSocket (tty put in raw mode viatermios, stdin → WS, WS → stdout,SIGWINCH→ resize, terminal restored on exit). Detaching an attach only drops a client; the agent stays alive.
kotgent tracks sessions; the backlog tracks work. Each project gets an ordered, dependency-aware list
of tasks that you groom on the Web UI's /tasks board and an agent inside a session reads from and writes
to. A session's purpose stops being something you remember and becomes something the daemon records — the
sidebar shows which task each session is on, and a task's card shows every session linked to it.
- A project is a committed file, not a path.
.kotgent.jsonat the checkout root holds a uuid and a name, so one backlog survives agit worktree, a move, a rename and a clone. It appears the first time a task is created somewhere that has no project (or when you runkotgent project init), it is written to be committed, and the daemon never commits it for you. Nothing is written until then — and a directory whose project was deleted writes nothing either:task addrefuses there instead. - Deleting a project is a tombstone, not a cascade.
kotgent project delete <uuid>(or Delete project in the Web UI's command palette) takes the project out of every selector and stops it being a source of new work —task addandtask nextrefuse in its directory, and akotgent startthere is left unstamped. Everything else is kept exactly as it was: the tasks, their order, their dependencies, their comments, and the sessions linked to them..kotgent.jsonis never touched, which is why a delete needs no confirmation beyond the dialog and why the directory keeps resolving to that project — delete or move the file if you want the directory to stop doing so.kotgent project restore <uuid>, or simply adopting the directory again withkotgent project init, brings everything back;kotgent project list --archivedis how you find the uuid of a checkout that no longer exists. - The states are
todo → in_progress → review → done, plus a position you drag on the board and dependencies that mark a taskblockeduntil what it waits on is closed.kotgent task nexthands out the first unblockedtodoin rank order and exits3when there is nothing eligible. - The whole
task/projectfamily prints JSON and only JSON — stdout is the answer, stderr is one{"error":…,"status":…}object. Inside a kotgent pane the commands need no task id:kotgent task show,task comment -m "…"andtask review -m "…"resolve the calling pane's own session. Outside one, pass--session <id>. - kotgent does not enforce one worker per task. You can open a second terminal it never hears about, so
an exclusive claim would be a guarantee it cannot keep; instead a task may be linked from any number of
sessions and the board shows all of them.
task nextwill not hand the same task to two agents in a row, which is the part that actually matters. - The interface an out-of-repo Agent Skill is written against is
docs/agent-task-skill.md.
The daemon binds 127.0.0.1 only. Two distinct keys guard it:
- The master token — the machine key. Stored at
~/.kotgent/token(mode0600, inside a0700 ~/.kotgent; generated from 32 bytes of/dev/urandom). It authenticates the CLI (as aBearerheader), the provider hooks (as their own header), and the issuing of browser tickets.kotgent token rotatere-mints it; the old key stops authenticating new requests immediately (already-open WebSockets survive until they reconnect, since auth is computed once at handshake). - A session cookie — the browser key. A stateless
HttpOnly; SameSite=Strict; Path=/cookie of the formv1.<issuedAt>.<hmac>wherehmac = HMAC-SHA256(master-token, "v1|" + issuedAt). There is no session table — the cookie verifies by recomputing the HMAC, so "sign out every device" is justkotgent token rotate(every HMAC dies at once).
~/.kotgent also holds the generated hook settings, the optional config.json (public URL), and
kotgent.db (including device push subscriptions). Web Push lazily creates the VAPID private key at
~/.kotgent/vapid.pem with mode 0600; deleting or replacing it invalidates existing browser
subscriptions, so notifications must be re-enabled on each device afterwards.
kotgent web # open the sign-in form and print a code to type into it
kotgent web --print # print a credentialed login URL for scripting or copyingNo master token is copied into a URL. kotgent web issues one single-use, 8-character Crockford
Base32 code (40 bits, held in memory for five minutes). It prints the bare
http://127.0.0.1:<port>/auth form URL, the public /auth form URL when public-url is set, and the
still-valid code, and then opens the local form in a browser. Type the code into that form or an
already-installed PWA. On Linux without DISPLAY, WAYLAND_DISPLAY or BROWSER, it launches no browser:
xdg-open would otherwise fall back to a terminal browser that hangs. The exchange is protected by a
daemon-wide rolling budget of ten failed attempts per minute in addition to the short lifetime and
single-use rule.
kotgent web --print is the non-interactive form: stdout contains exactly
http://127.0.0.1:<port>/auth#ticket=…, while the equivalent grouped code and human hint go to stderr,
so piping the URL remains safe. The fragment and typed code are two representations of the same
credential; spending either invalidates the other. A browser opening the fragment reads it locally,
POSTs it to /api/v1/auth/exchange, and then uses location.replace("/"), so neither the server's initial
GET /auth nor browser history receives the live fragment.
Programmatic auth calls use /api/v1/auth/{ticket,exchange,rotate}. The former root paths remain aliases for
older clients, just as /hooks/* remains an alias for hook scripts generated before hooks moved to
/api/v1/hooks/*; newly generated scripts and all bundled clients use the versioned paths.
The UI shows the session list with live state badges and a "Needs attention" queue (fed by the events
WebSocket; its header collapses the queue to a count, remembered per browser), and renders a session's
terminal with xterm.js over the terminal WebSocket (byte rendering,
keyboard input, resize). Its installable PWA layout adds a mobile sidebar drawer, safe-area handling,
terminal sizing from visualViewport, and a phone-only row for Esc, Tab, Shift-Tab, arrows, Ctrl, and
Ctrl-C. Terminal taps focus the software keyboard without Safari zooming the helper textarea, and a
terminal socket lost — to the app being backgrounded, or to the daemon restarting under it — is reattached
without a reload: on returning to the app, and on the events socket reconnecting, which is the only signal
that a restarted daemon is back. Each attempt checks daemon liveness under a deadline first; a daemon that
is merely unreachable leaves the attempt available for the next one, while a daemon that answers that this
session is gone ends it rather than retrying forever.
The UI is a small router over four paths: / and /s/{id} for the session view, and /tasks and
/tasks/{ref} for the task backlog's kanban board — four columns you drag cards
between on a desktop, one column and a switcher on a phone. Each card shows its blocked marker, its
dependency count and every session linked to it, and every change (a drag, a state move, a link, a
deletion) reaches a second tab over the same events WebSocket without a reload. Sessions carry a badge
linking to their task. For a selected live session with no task yet, ⌘K l opens a searchable picker of
that session's project's todo, in_progress, and review tasks; choosing one writes the same
non-exclusive link as kotgent task claim, so a todo starts and another session may still choose the
same task. The command palette opens the board with ⌘K o and its create form with ⌘K w. Those bare
paths are deep-linkable and installable, which is why the client-facing API lives under /api/v1.
Each session has a workspace of tabs under its header, and each tab holds side-by-side columns: the
terminal, linked task, structured plan, and Git Diff. The task editor is the same one the board opens.
+ adds a tab, a column's ⊞ adds the next column type the tab does not hold yet, choosing a type the tab already holds
swaps the two columns, and the divider between columns drags or moves with the arrow keys. A tab without
the terminal keeps it attached in the background, so switching back shows the same screen without
reconnecting. A phone shows one column at a time with a switcher, and a desktop drops columns that would be
narrower than about 320 px until there is room again. Layouts are remembered per browser, not per daemon.
Open changes in the command palette (session menu → Commands) or task opens/reuses Diff. Choose any worktree in that Git repository, including one without a session, without changing the terminal attachment. Review All, Staged or Unstaged local changes, individual commits (including merge parents), or a branch against a selected base. Unified/Split, filtering, Viewed and explicit Refresh help read one patch at a time. Phone navigation uses Files → Patch → Back to files. Reading state stays in page memory; the viewer is read-only and never fetches. See the Diff guide for comparison semantics, budgets and limitations.
One action changes daemon state at a time, and the palette says so rather than queueing. While a start, an import, a lifecycle action, a rename, a preferences save, a task link, or a project delete or restore is in flight, the palette's session commands are disabled and name the flow holding the lock; a form submitted anyway is refused with "Another action is still in progress — try again in a moment." The wait is bounded by the request timeout (60 s), and a mutation holds the lock through its own follow-up read, so two links to the same session cannot overwrite each other. Reads that are not part of a mutation — the board, the project list, the terminal — are never blocked by it.
The sidebar footer identifies the running daemon: local source builds show the release version plus their
embedded short Git hash (for example 0.10.0+81c37fe), while published Homebrew builds show the release
version alone (0.10.0).
The usage strip stays pinned above that footer while sessions and projects scroll. Available quota windows appear as progress bars showing usage, shared across sessions, with a current-time marker relative to the expected reset. Window labels follow the reported duration. Hover, focus with the keyboard, or tap a bar to see the percentage used, current time, time remaining, reset time and last observation; missing windows stay hidden. A provider line dims when its newest observation is more than ten minutes old.
Kotgent captures existing provider output without polling and assumes one account per provider on this host. Observation time describes capture activity and does not guarantee a fresh provider lookup. The agent guides explain each provider's available windows, capture timing and reset limitations.
A session row also carries an unread pill — how many events have arrived since you last looked at that
session. Looking at it clears it: the browser posts the cursor it has displayed, so the count is
server-side (it clears on the phone and the desktop together, and a second browser sees it clear with no
reload) and persistent (restarting the daemon does not resurrect a cleared badge). Reading a session does
not count as activity, so kotgent list's ordering is unaffected.
ADHD mode reduces the sidebar to what you pinned. The pin on a session row marks that session; the pin on a group header marks that directory path, and every session shown under that header is listed while the mode is on, including sessions started there later. A pinned folder that the current grouping does not draw stays pinned but has no effect until it is drawn again. What is pinned is daemon-wide, so a phone and a laptop agree on it; whether a given screen is reduced is that browser's own setting and survives a reload. The mode hides the "needs attention" section altogether, even for pinned sessions; a pinned session still shows its state on its own row, and notifications still fire. The session you have selected keeps its row whatever its pin says, and the toggle never hides on the sessions screen. A session you start or import while the mode is on is pinned on its own, so it stays listed after you select another one. Turning the mode on with nothing pinned says so and offers the way back. A session listed through a pinned folder shows a dimmed pin that names the folder; clicking it pins the session on its own, so it stays listed after the folder is unpinned. The Done list ignores the mode entirely.
A session started with --parent is drawn indented under its parent behind a ▸ N workers toggle,
collapsed by default; whether a tree is expanded is this browser's own setting. Workers stay in their
parent's folder whatever their own working directory. While collapsed, the toggle counts workers that
need attention; they are not repeated in the "needs attention" section. In ADHD mode a worker is listed
when it is pinned, selected or under a listed parent; a listed worker whose parent is hidden is drawn on
its own and names the parent. A worker whose parent is done says "orchestrator finished". A child session
never holds its task open: marking it done archives it without closing the task.
The per-browser notifications toggle registers /sw.js and the browser's Web Push subscription. A
false → true attention transition or an early weekly quota reset sends a payload-less push. The service
worker fetches the authenticated /api/v1/notifications inbox under a ten-second deadline and shows
current session-attention items plus reset notices from the past hour. Attention notices open or focus
their session; quota notices open Kotgent's main view and name the provider, percentage used before the
reset, and when that value was seen. A failed or stalled fetch still produces a generic Kotgent notice.
Scheduled resets and resets of shorter windows do not notify. Early weekly resets remain recorded in the local journal and inbox even when push is disabled. If Web Push is unsupported, denied, or unavailable on the daemon, the live tab falls back to ordinary in-tab attention notifications; usage-reset notices require Web Push.
Most real-world breakage traces back to the daemon's service environment, which is minimal by design — so the first question is almost always "does the plist still match my shell?".
startfails withagent '…' not found on the daemon's PATH. The daemon'sPATHis a snapshot taken atkotgent install, not your live shell's. If an agent CLI moved because of a version manager or a new Homebrew prefix, re-runkotgent installfrom a full login shell. kotgent fails fast here on purpose: the error names the fix instead of leaving a phantomrunningrow.- The TUI renders as a wall of underscores. The tmux client decided it may not emit UTF-8, which
happens when the daemon runs without a UTF-8
LANG— again a stale plist. Re-runkotgent installfrom a shell wherelocalereports a UTF-8 setting. - After
brew upgrade kotgentthe daemon does not come back. The plist records the binary's version-qualified Cellar path, which the upgrade invalidates. Re-runkotgent install. - The port is bound but nothing answers (a rebind fails with
EADDRINUSE, or a client connects and then hangs). Current builds close every spawn path against descriptor inheritance, so this should only come from a long-livedtmuxserver started by an older kotgent, which is still holding the listening socket the daemon that spawned it left behind.tmux -L kotgent kill-serverreleases it — note that this also stops every agent running under that server. - I created a project in the wrong folder.
kotgent project delete <uuid>removes it from every selector without touching anything it owns (find the uuid withkotgent project list). The delete does not remove.kotgent.json, so that directory still resolves to the deleted project:task addthere refuses and names all three ways out — restore it, file elsewhere with--project, or delete or move that.kotgent.json— whilekotgent startthere leaves the session unstamped and says nothing. Committing the file and then deleting the row is the one combination that keeps coming back.kotgent project restore <uuid>undoes the delete with the whole backlog. - My tmux settings do nothing inside a kotgent pane. Expected: kotgent runs every tmux command with
-f /dev/null, so~/.tmux.confis never loaded on its socket (see Requirements for what it forces instead). Your owntmuxon the default socket is untouched. There is no user-facing override — the option set lives insrc/tmux/TmuxOptions.kt. Note the flag only affects the command that starts a server: if something else already started one on-L kotgent, that server has your config until it is restarted (tmux -L kotgent kill-server, which also stops every agent on it). - I can't select text in the browser terminal / the wheel scrolls tmux instead of my terminal. Both
are
mouse on, which kotgent forces so the wheel reaches the pane's own history (10 000 lines, and the only scrollback a newly attached viewer has). To select text while an agent's TUI is running, hold Option and drag on macOS, or Shift and drag elsewhere. The wheel puts the pane into tmux copy-mode — shared by every viewer of that session — which scrolls back down to the bottom to exit, and kotgent cancels it anyway before sending keys, so Interrupt is never swallowed by it. - Notifications stay in the open tab instead of reaching the phone. On iOS, Web Push requires iOS
16.4 or later and an installed home-screen app; enable it from that app's sidebar so the permission
prompt runs from the tap itself. A missing/unusable
/usr/bin/openssl, denied browser permission, or an unreachable push service disables only server-sent push, and kotgent falls back to live-tab attention notifications. - Push stopped after
vapid.pemwas deleted, replaced, or regenerated. A browser subscription is bound to the VAPID public key it was created with. Toggle notifications off and on in each installed browser/PWA to register a fresh subscription with the daemon. The key at~/.kotgent/vapid.pemshould remain mode0600. - Inspecting the daemon itself. On macOS,
launchctl print gui/$UID/io.kotgent.daemonshows its state;~/Library/LaunchAgents/io.kotgent.daemon.plistholds the captured environment. On Linux, usesystemctl --user status kotgent,systemctl --user cat kotgent, andjournalctl --user -u kotgent -f.
kotgent uninstall # stop and remove the per-user service
tmux -L kotgent kill-server # stop every agent still living in tmux
rm -rf ~/.kotgent # token, config/hooks, SQLite data/subscriptions, VAPID private key
brew uninstall kotgent # if installed from the tapkotgent uninstall only removes the service entry — the agents in tmux and the state under ~/.kotgent
outlive it by design, so drop them explicitly if you mean to.
Kotgent's core control loop is one end-to-end path:
kotgent startan agent session → close IDEA (Detach) → open the browser → continue the same session → see it flag "needs attention" when the agent reports that it needs a response.
Concretely:
kotgent start <agent>launches the selected agent insidetmuxsessionkt-<id>and records it.- Attaching from the IDE terminal and then closing it (Detach) drops one WebSocket subscriber. The
daemon holds the single upstream
tmux attachclient and fans it out, so the agent keeps running with no client attached. - Running
kotgent webopens the credential-free sign-in form and prints a one-time code; after signing in, clicking the session re-attaches to the very same live process — the browser is just another client of the same fan-out. - When a provider hook reports that the agent needs a response, the daemon updates its session state, and the events WebSocket updates the browser's "Needs attention" queue. The available signals depend on the agent integration.
Kotgent is deliberately focused. The current product boundary is:
Implemented:
- Four launch kinds: Claude, Codex, Junie and Shell. The three providers run as a TUI in
tmuxand Shell uses the same lifecycle and terminal fan-out. State tracking and provider limitations are described in the agent guides. - Two keys, browser-friendly auth. The daemon still binds
127.0.0.1only, but browsers authenticate with a stateless, no-secret-in-URL session cookie (kotgent webmints a one-time ticket), and a phone can sign in through a cloudflared tunnel + Cloudflare Access. The CLI and hooks keep using the master token;kotgent token rotateinvalidates every cookie at once. - The full
start → Detach → browser → continue → needs-attentionpath, session reconciliation on daemon restart, provider-id capture, and per-user service install. - Session metadata & lifecycle polish. Each session shows its agent CLI version and, best-effort, the
model it is running; its name is an editable label (
kotgent session rename, or the palette's rename dialog) that reaches every open client live and falls back to the automatic one when cleared; Done stops an agent and archives it off the sidebar (restorable, history kept); and an opt-in, per-browser notification toggle registers server-sent Web Push for attention edges and early weekly quota resets, with live-tab fallback for attention only. - Shared usage meters. The sidebar keeps available Claude and Codex usage bars visible, with expected resets and observation freshness without provider polling. Early weekly resets retain their prior usage in the local notification inbox, independently of push availability.
- Installable mobile PWA. The manifest, root service worker, home-screen icons, responsive drawer, visual-viewport terminal sizing, software-keyboard focus handling, special-key toolbar, foreground terminal reattachment, and notification deep links are all shipped. The service worker is network-only: there is deliberately no offline shell when the local daemon cannot serve useful state.
- Import of externally started sessions.
kotgent import(and the Web UI's Import mode) registers a conversation begun in a plain terminal and continues it under kotgent — fan-out, push, and mobile access included, with the provider's own on-disk record as the history (see The CLI). - A browser end-to-end tier. Web UI behaviour is executed rather than described:
webuitestdrives a real Chromium through Playwright againstwebuicheck, a fixture binary that assembles the real daemon over the shared doubles infakesand serves a terminal from a real PTY running a deterministic script instead of a provider. Each test spawns its own harness on an ephemeral port, signs in through the real login form, and leaves nothing behind outside the checkout. - A browser-independent TypeScript tier. The pure Web UI rules — revision merges, link eligibility,
typeahead selection, readiness transitions — are proven under Node's own
node --test, inwebuitest/js/. The tests and thewebui/src/sources they import run directly through Node's type stripping and resolve packages fromwebui/node_modules. The service-worker tests execute the builtresources/webui/sw.js.WebUiLogicTestspawns the runner as part of./kotlin test.
Backlog (not built yet):
- Additional providers, including Cursor; see the Cursor integration research.
- Structured mobile actions such as native approve/deny buttons outside the agent's terminal. Approvals remain interactive TUI operations today.
- Durable checkout snapshots (the read-only Git Diff viewer is available).
- More of the Web UI's pure rules proven without a browser.
lib/commands.ts,lib/paths.tsandlib/unicode.tsimport cleanly under Node and are only partly covered by the tier inwebuitest/js/;lib/qr.tsimports the locallib/qrcode.tsand can also be tested in this tier. - Decomposing the remaining
app.tsxflows. Roughly sixtyuseCallbackflows still live in one module. State lives instate/and async coordination inlib/mutation.ts; splitting the component itself is still open and still not urgent.
The real-PTY tests. Kotlin Toolchain 0.12 links our own cinterop into test binaries
(KT-78062 is fixed), so these 11 assertions are ordinary
tests under test/pty/, one @Test each:
- a
catround-trip through the pty, resize(TIOCSWINSZ) succeeds,- the child's exit code is captured,
- spawning a nonexistent command throws,
- the spawned child inherits only its tty (Darwin CLOEXEC-default / Linux close-from spawn actions — an inherited listening socket would keep the port bound after the daemon dies),
prepareCloseunblocks a full master write,closestops the reader before releasing the master descriptor (a freed fd number can be reused by another session while a stale reader still runs),- concurrent
closeruns teardown exactly once, tmux attachruns on the spawned pts,- a resize reaches a running
tmux attach(the child gets no controlling terminal, soPty.resizemust deliverSIGWINCHitself — see CLAUDE.md), TerminalBridgefans out over that real attach.
webuicheck stays a separate binary, for a different reason: the browser tier is JVM (Playwright
cannot drive Kotlin/Native in-process), so the harness it talks to must be a process of its own. No test
task links a main binary, which is why ./kotlin build still has to run before ./kotlin test. Its own
two self-checks now live in webuicheck/test/; everything a browser can observe is a named assertion in
webuitest.
Everything around the cinterop is still tested directly via interface fakes (FakePtyHandle, FakeTty)
where the behavior is platform-independent.
Issues and pull requests are welcome. A few things worth knowing before you open one:
- The native build is the JetBrains Kotlin Toolchain, not Gradle. Use the committed
./kotlinwrapper; there is nobuild.gradle. Dependencies and module wiring live inmodule.yaml/project.yaml. - Keep
./kotlin buildand./kotlin testgreen, and runbuildbeforetest(see Build & test). New tests are expected to come with the change; the suite has no skips and should stay that way. - Web UI changes go through four tiers, and which tier a claim belongs to is decided by whether a
running page could answer it. Anything a browser is not needed for — data merges, matching, state
transitions and the other pure rules — belongs in
webuitest/js/, which runs undernode --testagainst the TypeScript sources; that is the cheapest tier and the one to reach for first.test/transport/WebUiServingTest.ktkeeps what only an address can prove — URLs, media types, caching headers and path safety — and checks that the built shell's references resolve and non-mapassets/files have content-hashed names. Anything a Chromium can answer belongs inwebuitest/, as executed behaviour against the real server. Changedwebui/srcmodules andwebuitest/jstests must passnpm run typecheck --prefix webui, and changedwebui/srcmodulesnpm run build --prefix webui. What remains manual is only what desktop automation cannot faithfully reproduce: installed-PWA lifecycle, safe areas, software-keyboard geometry, touch physics and notification prompts. The full strategy is docs/TESTING.md. - Read CLAUDE.md first if you are touching the build, native code, or the event model. It
documents the invariants (host-free core, single-upstream
tmuxfan-out, the event-sourcing rules) and the toolchain gotchas that are expensive to rediscover. - All three native targets are release gates. macOS and Linux x64 run the full suite. Linux ARM64 is cross-compiled on x64 and runs native suites and service checks on a real ARM64 runner.
Licensed under the Apache License, Version 2.0.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this work shall be licensed as above, without any additional terms or conditions.

