Skip to content

Repository files navigation

warden

warden

A curator for your terminals — windows, projects, and (mostly) muxers all the way down.

Release CI Platform Built with Rust Tauri License

warden running two windows (personal + work) — a curator-style sidebar with grouped project tabs (live/cold dots) over embedded libghostty terminals

warden is a config-driven terminal multiplexer. One TOML file is the source of truth: it defines windows and the project tabs inside them. warden materializes itself from that config and hot-reloads on save. Each window carries a colour + title banner for at-a-glance identity; each tab is a real terminal opened in a working directory, running an optional command.

warden is generic and content-agnostic — it knows nothing about any specific tool, so the command a tab runs is whatever you want: a shell, a TUI, a build watcher, an agent launcher. It stands on its own.

It's also built for a flow: I pair each tab with agentmux (amux), a tmux-based agent launcher — so the stack nests warden → agentmux → tmux (warden itself embedding libghostty as its terminal surfaces). A multiplexer for a multiplexer for a multiplexer; it's turtles the rest of the way down.

Targets macOS. Linux is a possible future direction, not a commitment; the config crate stays platform-neutral to keep that door open. Windows isn't addressed today — the terminal surface is a macOS-native embed — but it's unstarted, not ruled out.

Features

  • A window per [[window]] — native macOS windows, each with a colour + title banner, a curator-style draggable sidebar, and the terminal under an overlay titlebar. The Window menu lists every configured window — raise an open one or reopen one you've closed (⌘⇧T reopens the last closed).
  • Persistent, not last-window-quit — warden opens the windows you mark open_on_start (default all) and shows a home surface when none are open, listing every configured window so you can raise or reopen one with a click. It's a persistent app: closing the last window never quits it — ⌘Q does.
  • Project tabs — each tab is a real terminal in a working directory. load_on_open tabs spawn at launch and keep running; the rest spawn lazily on first focus. Tabs can be grouped into labelled sidebar sections.
  • Project trees — point a [[window.root]] at a directory (e.g. ~/Developer) and warden auto-discovers every git project under it, rendering them as a collapsible tree of tabs — no per-project config needed. Pair it with amux probe/kill for a per-project session dot on every discovered project.
  • Live hot-reload — edit the config and windows and tabs are added, removed, recoloured, and re-sectioned live on save. A missing config offers to create a starter one and an invalid one shows the error, both on the home surface; a parse error mid-edit instead keeps the last-good windows up behind an error banner. The Config menu opens the config file in your default editor or reveals it in Finder, so you needn't remember its path.
  • Tab-row affordances — a letter/colour tile and a live/cold dot (filled when the terminal is spawned, hollow when cold). Hover a live dot for a ✕ that unloads the tab — kills the terminal and PTY; it respawns a fresh shell on next focus.
  • Pop-out tabs — pop the active tab into its own banner-only window with ⌘⇧O (or the ⤢ control on its row). It's session-preserving: the live terminal, its scrollback, and the running process move across untouched — no restart. Closing the popped-out window returns the tab to where it came from (reopening its origin window first if you'd closed it), and a ⇱ control pops it back in from the sidebar. A popped-out window remembers the size and position you last gave it, so a tab you pop out often reopens where you left it. A split tab pops out with both terminals on the same sides and at the same divider ratio it had, with the same divider and focus ring — the pane UI is identical docked or popped out.
  • Splits — a split table (global, per-window, per-root, or per-tab) opens a tab already split, second pane on either side, at a declared size, running a declared command beside the tab's own. A tab with no config split still gets one at runtime with ⌘D (or Tab ▸ Split). Either way you get a thin draggable divider between the two terminals; to close a split, type exit in the second shell. A runtime split — and its ratio — is remembered per tab across restarts; a config split's dragged ratio holds only until the tab's terminals relaunch (unload, hot-reload, or app restart), then it snaps back to the declared size. ⌘W and ⌘⇧O always act on the whole tab, not just the focused pane: unloading drops both terminals, popping out takes both with it.
  • Live terminal affordances — URLs in a terminal are clickable: hover one for a hand cursor, click to open it in your default browser. And when a tab's process exits (the shell quits, an agent finishes), the tab goes cold — the dot empties and the row respawns a fresh shell on next focus — rather than stranding a dead "Process exited" screen.
  • Notifications — a background tab that rings the bell or emits a desktop-notification escape (OSC 9 / OSC 777) gets an amber badge, and a desktop notification additionally raises a macOS banner; the badge clears on focus. This is the channel agentmux's Claude hooks feed instead of shelling out to osascript.
  • Session-presence probes — a per-tab probe command drives a three-state dot from its exit code: cyan on exit 0 (live), a ghost on exit 3 (crashed but restorable), hollow otherwise — independent of whether warden's own terminal surface is loaded (details below).
  • Keyboard navigation (the Tab menu) — ⌘⇧[ / ⌘⇧] cycle the previous/next loaded tab (cold tabs are skipped) and ⌘1–⌘9 jump to a position; set tab_digit_keys = "cycle" to make ⌘1 / ⌘2 cycle instead (jumps shift to ⌘3–⌘9). ⌘W unloads the active tab, ⌘⇧W closes the window (Safari/Chrome convention), and ⌘⇧O pops the active tab into its own window (session-preserving; see above).
  • CLIwarden validate prints the resolved window/tab tree and warnings; warden fmt formats a config in warden's house TOML style.

To wire the session-presence dot — pairing with agentmux — set a tab's probe to a session check:

probe = '"$HOME/.agentmux/bin/amux" --probe'

so the dot shows whether its amux session is alive — amux --probe exits 0 for the agent session or a lingering frame (so the dot stays lit if the agent exits but the frame wrapper is still up; a plain bare-amux without a frame only ever has the agent). It exits 3 instead when the session's gone but restorable — a plain amux launch here would offer its restore picker — which warden shows as a ghost rather than cyan. amux owns the session naming and socket layout, so the probe stays a one-liner that can't drift from amux's internals. probe_interval is the settled slow-poll floor in seconds (default 5) — not the whole cadence: every trigger (tab activate, start, kill, hot-reload, focus) pushes that window into a fast burst which polls until the state stops changing, then drops back to this floor, so the rate between a trigger and settling is far higher than the floor. 0 means event-driven-then-idle — still bursting on every trigger, just no steady poll between them; it is not "no probing". Because the burst rate is fixed, probe_interval cannot bound the cost of a slow probe command — keep the probe itself fast. Cadence, bursts and their bounds: docs/probing.md. Name amux by absolute path: warden runs the probe via sh -c with the .app's own env, which is minimal on a Finder/Dock launch — amux's internal tmux calls then resolve via the login-shell PATH warden imports at startup. Not using agentmux? Point probe at any check that exits 0 when your session exists.

A tab's optional kill command severs the session the dot represents: click the cyan dot once to arm, click again to confirm, and warden runs kill fire-and-forget — the surface stays open, and the probe re-runs immediately to update the dot. With agentmux, set it to '"$HOME/.agentmux/bin/amux" --kill' (cwd = the tab dir): the mirror of amux --probe, it tears down the whole project — the agent session plus its frame and scratch terminal — so it reaps exactly what the probe detects. Since the control lives on the presence dot, kill only does anything on a tab that also sets probe (no probe ⇒ no dot to click).

The mirror also holds. When the probe reports the session gone on a tab whose terminal is still live — hollow or ghost alike — the same dot becomes a one-click start: warden types the tab's cmd into the existing shell, so a dead or crashed agent session restarts (or restores) in place — scrollback preserved, no terminal respawn.

Not yet built (see docs/FOLLOWUPS.md): ad-hoc cmd+T / cmd+N tabs and windows.

Config

~/.config/warden/config.toml (override with WARDEN_CONFIG):

shell = "fish -l"            # global default shell
format_on_save = true        # rewrite this file tidy on each clean save
density = "compact"          # condensed chrome

[[window]]                   # a native macOS window
title  = "work"
colour = "#0f8a8a"           # banner accent
width  = 1500                # initial size, px
height = 1000
cmd    = "amux"              # this window's default startup command (each tab can override)

  [[window.tab]]             # a project terminal
  title      = "myproject"   # defaults to the dir basename
  dir        = "~/code/myproject"
  load_on_open = true        # spawn at launch and keep running

  [[window.tab]]
  title = "notes"
  dir   = "~/notes"
  cmd   = ""                 # opt out: just a bare shell here

  [[window.group]]           # optional: a labelled sidebar section
  name = "services"
    [[window.group.tab]]     # same fields as [[window.tab]]
    title = "api"
    dir   = "~/code/api"

  [[window.root]]            # optional: scan a dir; every git repo under it becomes a tab
  name = "Developer"
  dir  = "~/Developer"

Every option

Global (top of the file):

Key Default What it does
shell your login shell, run as a login shell The shell every tab spawns. Cascades.
cmd none Command typed into that shell on spawn. Cascades.
probe none Session-presence check, run per tab (cwd = the tab's dir): exit 0 ⇒ cyan dot, exit 3 ⇒ ghost (restorable), anything else ⇒ hollow. Cascades.
probe_interval 5 Slow-poll floor in seconds once a burst settles. 0 = event-driven only (still bursts on triggers, no steady poll).
kill none Session-kill command, run on a two-step confirm click of the presence dot. Cascades. Only reachable on a tab that also sets probe.
split none Give every tab a second pane. A table: side ("left"/"right", default right), size (the second pane's share, 0.10.9, default 0.5), cmd (typed into it; absent = bare shell). Cascades as a whole table; false opts a level out, true = the default split.
format_on_save false Rewrite this file in house style on each clean save (same formatting as warden fmt).
density "comfortable" Chrome sizing. "compact" scales type + spacing down proportionally for denser tab lists.
tab_digit_keys "jump" ⌘1–⌘9 jump to a tab position. "cycle" makes ⌘1 / ⌘2 cycle next/prev instead, shifting jumps to ⌘3–⌘9.
sidebar_drag true The non-interactive sidebar chrome doubles as a window-move drag handle.
open_tabs_section false Pin an Open section above the main list, mirroring every tab that currently has a terminal open (including one popped out into its own window). Mirrored tabs keep their row in their own group or tree, so the main list never shuffles as terminals come and go; ⌘1–⌘9 and tab cycling ignore the mirrors. The two lists scroll independently — the section grows to 2/5 of the window then scrolls within itself, so paging through a long project list never pushes the open tabs off the top. While the section is showing, the main list below it is dimmed until you hover it. Cascades global → window.
auto_update true Check for a new release on launch and every 6h. false suppresses the auto-check; Check for Updates… still works. Takes effect at next launch.
notify_debug false Trace the notification path to $TMPDIR/warden-notify-dbg.log — a debug aid, read at launch (see docs/notifications.md).

[[window]] — one native macOS window each:

Key Default What it does
title required Banner text + window title; unique across the config. Changing it is destructive — the window is closed and reopened, so its terminals and saved size/position reset.
colour neutral Banner accent, #rgb or #rrggbb.
width / height 1500 / 1000 Initial size in px; the window's saved size/position wins after the first launch.
open_on_start true Materialize this window at launch. false = configured but closed — open it from the home surface or the Window menu.
open_tabs_section inherited from global Whether THIS window's sidebar pins the Open section. Set it per window to show the section in a busy window and not a quiet one.
shell / cmd / probe / kill / split inherited from global Per-window overrides for every tab in it.

[[window.tab]] (and [[window.group.tab]]) — one project terminal each:

Key Default What it does
dir required Working directory the terminal opens in (~ expanded). A dir that doesn't exist is a warning, not an error.
title basename of dir Display label. Purely cosmetic — may repeat within a window.
id unset Stable identity, needed only to disambiguate two tabs that share a dir. Otherwise the dir is the identity.
load_on_open false Spawn at launch and keep running in the background. Otherwise a tab spawns lazily on first focus.
shell / cmd / probe / kill / split inherited from the window Per-tab overrides.

[[window.group]] — a labelled sidebar section:

Key Default What it does
name required Section header. Unique within the window (one namespace shared with [[window.root]] names).

[[window.root]] — a scanned projects dir; every git repo found becomes a tab:

Key Default What it does
dir required Dir to scan. The walk stops at each .git (never descends into a repo) and skips hidden dirs and symlinks.
name basename of dir Section header for the discovered tree.
depth 6 How deep to scan; must be ≥ 1.
shell / cmd / probe / kill / split inherited from the window Overrides applied to every project discovered under this root.

Three rules the tables can't carry. cmd is typed into the shell, not exec'd — so a shell function like agentmux's amux resolves, and you drop back to a live prompt when it exits. The cascading keys resolve nearest-level-wins — global → window → tab, with "" opting a level out of an inherited value (cmd = "" gives you a bare shell under a global cmd); projects discovered under a [[window.root]] have no tab level, so they cascade root → window → global instead. split cascades the same way but as a whole table — the nearest table wins entirely, and split = false is its "". And grouping is cosmetic[[window.group]] only sections the sidebar; loose [[window.tab]]s appear first in a headerless section.

Everything above hot-reloads on save, auto_update aside.

Full schema, validation rules, and resolution semantics: docs/config.md.

Install

Download (no build): grab warden-<version>-macos.zip from the latest release, unzip, and move warden.app to /Applications. Release builds are signed with Developer ID and notarized, so they open without a Gatekeeper block. macOS only.

Guided (Claude Code): run /warden:install — it checks prerequisites (Xcode Command Line Tools, Rust, the Tauri CLI), builds warden from source, installs it to /Applications, and seeds your config.

One-liner:

curl -fsSL https://raw.githubusercontent.com/lockyc/warden/main/install.sh | bash

This clones warden to ~/.warden, builds the release bundle (cargo tauri build), installs warden.app to /Applications, and seeds ~/.config/warden/config.toml from the example if you don't already have one. Re-run it any time to update (it git-pulls and rebuilds). macOS only.

Prerequisites: macOS, Xcode Command Line Tools, a Rust toolchain (rustup). The installer installs the Tauri CLI itself if missing.

Updates

warden updates itself — no reinstall. On launch, every 6 hours while open (warden is long-running), and via warden ▸ Check for Updates…, it checks GitHub for a newer release; when one exists the sidebar shows an Update available: v X bar with a one-click Update & Relaunch.

  • Confirm-to-install — nothing installs silently; you approve each update, and the bar's × dismisses it for the session.
  • Signed — each update is verified against warden's own minisign key before it installs, independent of Apple notarization.
  • Opt out with auto_update = false (the Check for Updates… menu item still works).

Releases ship a universal binary — one warden.app that runs natively on both Apple Silicon and Intel Macs. Re-running the installer (or downloading the .zip) is needed only to bootstrap the first updater-capable version — 0.6.0 — after which updates land in-app.

Build & use

With just (run just to list recipes):

just run launches against examples/config.toml, whose tabs point at the mock project tree documented in examples/projects/README.md.

just hooks        # once per clone: enable .githooks (pre-push doc gate + active-[patch] guard)
just run          # launch the app against examples/config.toml (never touches your real config)
just validate     # validate the demo config (pass a path to validate another)
just test         # workspace tests
just fmt          # format Rust sources (cargo fmt)
just clippy       # lint (warnings as errors)
just gate         # the full pre-merge gate CI runs (fmt-check, clippy, tests)
just build        # build the release warden.app (needs: cargo install tauri-cli --version ^2)
just deploy       # build, install to /Applications, and relaunch

core.hooksPath is per-clone local git config that the repo can't carry, so run just hooks once after cloning — without it neither git hook is active.

Builds are signed with Developer ID and notarized automatically when the Apple signing/notary env vars are set in the build environment (APPLE_SIGNING_IDENTITY pointing at a Developer ID Application cert, plus APPLE_ID/APPLE_PASSWORD/APPLE_TEAM_ID, or APPLE_API_KEY*) — so release artifacts open on other Macs without a Gatekeeper block. Without those vars (e.g. building from source as a contributor), the cargo tauri build output is ad-hoc/unsigned; just deploy then strips the Gatekeeper quarantine xattr and — if a Developer ID Application cert is in your keychain — re-signs the installed bundle with it (auto-detected, no env vars, no notarization). That local signature gives warden a real Team ID, which on macOS 26 is what keeps it off syspolicyd's broken per-exec provenance path (qtn_proc): an ad-hoc, team-less terminal makes that check fail on every command run inside it, storming syspolicyd/kernel_task. No cert → it stays ad-hoc (still runnable via the xattr strip).

Or with cargo directly:

cargo build
cargo test
cargo run -p warden-app                                # launch the app (macOS; reads WARDEN_CONFIG or ~/.config/warden/config.toml)
cargo run -p warden-config --bin warden -- validate    # validate ~/.config/warden/config.toml
cargo run -p warden-config --bin warden -- validate path/to/config.toml
cargo run -p warden-config --bin warden -- fmt         # format ~/.config/warden/config.toml in place
cargo run -p warden-config --bin warden -- fmt path/to/config.toml
cargo run -p warden-config --bin warden -- fmt --check path/to/config.toml  # check only, no write

warden-app materializes a window for each [[window]] and hot-reloads on save; edit the config while it's running to watch windows and tabs appear, disappear, and recolour live.

warden validate prints the resolved windows/tabs and any warnings; exit code 0 (ok), 1 (load/parse/validation error), 2 (usage). warden fmt rewrites a config in warden's house TOML style — consistent indentation, aligned =, section spacing (--check reports without writing, for a CI gate); format_on_save = true applies the same formatting automatically on each clean save.

Layout

  • crates/warden-config/ — the config crate (library + warden CLI).
  • crates/warden-app/ — the macOS Tauri app: windows, the sidebar tab list, libghostty surfaces behind the TerminalSurface seam, and hot-reload wiring.
  • assets/ — icon masters (icon.svg, icon-app.svg), rendered PNGs, the macOS warden.icns, and build-icons.sh to regenerate the rasters from the SVGs.
  • docs/FOLLOWUPS.md — tracked list of intentionally-deferred work.

Related projects

warden is built on three shared library crates. Building it from source pulls them in automatically — they're pinned Git dependencies, resolved by a plain cargo build / just run with nothing extra to install:

  • chrome-core — the sidebar chrome (banner, grouped tab rows, resize drag, density tokens). A build-dependency: its CSS/JS is materialized into warden's bundled web assets at compile time.
  • config-core — the TOML config engine (parse, validate, format, hot-reload diff) behind warden's config and warden fmt.
  • shell-core — the shared release tooling + a sliver of Tauri runtime setup. build.rs materializes the release scripts (git-ignored) and stamps the build; the app registers window geometry persistence/updater/process via its register_plugins.

Those same cores are also shared with two sibling apps, curator (curates browser tabs) and lector (curates local documentation sites), the way warden curates terminals. Neither is a dependency of warden — they're peer projects that just draw from the same cores. (warden's embedded terminal is a separate, vendored third-party component; see the License note below.)

If you want to iterate on a shared core, just chrome-dev builds warden against a sibling ../chrome-core checkout (including uncommitted edits) and just chrome-pin re-pins to its pushed commit afterward; just config-dev / just config-pin and just shell-dev / just shell-pin are the same pair for ../config-core and ../shell-core. Never commit an active patch — just gate and a .githooks/pre-commit guard both refuse while one is live.

License

MIT — see LICENSE.

The vendored libghostty binary (crates/warden-app/vendor/) is Ghostty compiled from an unmodified, pinned upstream commit by lockyc/libghostty-build and distributed under Ghostty's MIT license; see crates/warden-app/vendor/LICENSE-ghostty and PROVENANCE.md in that directory.

About

Curated terminals - a macOS console of grouped project tabs of embedded libghostty terminal surfaces.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages