Daemon consumers
August 4, 2026 · View on GitHub
gwm daemon is a long-running JSON-RPC 2.0 server over a unix domain socket (a named pipe on Windows, #439): editors, statusbars, and tooling connect once and call list / doctor / path, or subscribe for pushed worktrees.changed notifications, instead of spawning gwm per query. This page covers the consumer side: the bundled gwm statusline, the raw protocol one-liner, and an editor recipe.
The daemon is one per repo: it answers for the repository it was launched in. Start it from inside the worktree set you want to watch (
cdinto any worktree of the repo, thengwm daemon). The default socket path is shared, so to run daemons for several repos at once give each a distinct--socket(and point that repo'sgwm statusline --socketat the same path); otherwise the secondgwm daemonis refused by the live-socket guard.
Start the daemon
# Binds $XDG_RUNTIME_DIR/gwm.sock (→ $TMPDIR → /tmp on macOS, where
# XDG_RUNTIME_DIR is unset). Override with --socket.
#
# Security note: when the chosen base dir is NOT owner-only (the shared
# /tmp last resort), the socket is isolated in a per-user owner-only
# sub-dir instead — $base/gwm-<uid>/gwm.sock — so another local user can't
# connect. The common $XDG_RUNTIME_DIR / per-user $TMPDIR paths (already
# private) are unchanged. The daemon prints its actual bound path to
# stderr ("gwm daemon listening on <socket>"), so a raw nc/socat consumer
# in that fallback case should read the path from there rather than
# hard-coding $base/gwm.sock.
gwm daemon
# A faster subscribe cadence (default 1000 ms):
gwm daemon --poll-ms 300
# Several repos at once: one socket each (the default path is shared, so a
# second daemon on it is refused). Point each consumer at the matching path.
gwm daemon --socket "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/gwm-api.sock" # in repo A
gwm statusline --socket "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/gwm-api.sock"
The daemon prints gwm daemon listening on <socket> to stderr only once the socket is bound, so a wrapper can treat that line as a readiness signal.
Statusline: gwm statusline
The first bundled consumer. It connects to the daemon, asks for the worktree set, and renders a single compact line for a tmux / starship / zsh prompt:
$ gwm statusline
feat/#309-daemon-consumer · 3 wt · * ↑1 · #309 · PR #310
The line is built only from the daemon's stable schema:
| Token | Meaning |
|---|---|
feat/#309-… | active worktree's branch (or its name when detached) |
3 wt | total worktree count |
* | the active worktree has uncommitted changes |
↑n / ↓n | commits ahead of / behind the upstream |
#309 | linked issue number |
PR #310 | linked PR number |
claude | an active AI-agent session in the active worktree (from the experimental agents field, #408). Idle sessions are not advertised here; the TUI overlay (a) has the full list. |
The active worktree is the one whose directory encloses the current working directory; outside any worktree the line collapses to just the count (3 wt).
No CI rollup. The
CI passing 9/9-style half of the original sketch is intentionally omitted: a CI rollup is not part of the daemon's stable schema, and fetching it would mean aghcall on every prompt redraw. It is left as a follow-up rather than smuggled into the wire protocol.
Live updates: --watch
--watch subscribes to the daemon's worktrees.changed stream and reprints the line on every change, one line per update, which is ideal for a long-running command feeding a statusbar:
gwm statusline --watch
Graceful degradation
When no daemon is reachable, gwm statusline prints an empty line and exits 0, so a prompt substitution degrades to nothing instead of showing an error. That makes it safe to drop into a prompt unconditionally.
Prompt recipes
zsh, right prompt (refreshed on each prompt; one socket round-trip to the running daemon):
# ~/.zshrc
function gwm_rprompt() { gwm statusline 2>/dev/null }
setopt PROMPT_SUBST
RPROMPT='$(gwm_rprompt)'
tmux status-right, re-queried on the status interval (for push-driven updates instead of polling, run gwm statusline --watch into a pane and read its tail):
# ~/.tmux.conf
set -g status-interval 5
set -ag status-right ' #(cd #{q:pane_current_path} && gwm statusline)'
starship, custom command:
# ~/.config/starship.toml
[custom.gwm]
command = "gwm statusline"
when = true
shell = ["bash", "--noprofile", "--norc"]
format = "[$output]($style) "
Raw protocol: the thin proof
No gwm binary required on the consumer side: the wire format is newline-delimited JSON, one request per line, one response per line. Anything that can write a line to a unix socket is a client.
# socat
printf '{"jsonrpc":"2.0","method":"list","id":1}\n' \
| socat - UNIX-CONNECT:"${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/gwm.sock"
# nc / netcat
printf '{"jsonrpc":"2.0","method":"list","id":1}\n' \
| nc -U "${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/gwm.sock"
# subscribe — holds the connection open and streams a `worktrees.changed`
# notification on every change (the first line is the current snapshot).
# `cat` keeps the write side open so the daemon keeps streaming; without it
# `printf` exits, the socket half-closes, and only the first snapshot
# arrives. Ctrl-C to stop.
{ printf '{"jsonrpc":"2.0","method":"subscribe","id":1}\n'; cat; } \
| socat - UNIX-CONNECT:"${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/gwm.sock"
Methods: list (worktree array), doctor (health report with severity + exit_code), path ({"params":{"pattern":"<fuzzy>"}} → { name, path, branch }), and subscribe (upgrades the connection to a one-way notification stream). Agent-session changes push too: a session appearing, vanishing, flipping active ↔ idle, or writing fresh activity (last_activity) triggers a worktrees.changed. Unlike age_seconds, last_activity only moves on real agent writes, and the 30 s detection cache bounds the push frequency. The response schemas are the same stable DTOs the --format=json CLI flags emit. See docs/schema/ and the gwm daemon reference.
Editor recipe
An editor doesn't need a plugin: a task that shells out to the daemon (or to gwm's --format=json surface) is enough to list worktrees and jump between them.
Zed
.zed/tasks.json, a task that prints the live statusline and one that lists the worktrees through the socket:
[
{
"label": "gwm: statusline",
"command": "gwm statusline",
"use_new_terminal": false,
"reveal": "always"
},
{
"label": "gwm: worktrees (via daemon)",
"command": "printf '{\"jsonrpc\":\"2.0\",\"method\":\"list\",\"id\":1}\\n' | socat - UNIX-CONNECT:\"${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/gwm.sock\" | jq -r '.result[] | \"\\(.name)\\t\\(.path)\"'",
"reveal": "always"
}
]
VS Code
.vscode/tasks.json, the same idea, using the daemon socket to list worktrees:
{
"version": "2.0.0",
"tasks": [
{
"label": "gwm: worktrees (via daemon)",
"type": "shell",
"command": "printf '{\"jsonrpc\":\"2.0\",\"method\":\"list\",\"id\":1}\\n' | socat - UNIX-CONNECT:\"${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/gwm.sock\" | jq -r '.result[] | \"\\(.name)\\t\\(.path)\"'",
"problemMatcher": []
}
]
}
To jump to a worktree by fuzzy pattern, call path instead of list:
printf '{"jsonrpc":"2.0","method":"path","params":{"pattern":"309"},"id":1}\n' \
| socat - UNIX-CONNECT:"${XDG_RUNTIME_DIR:-${TMPDIR:-/tmp}}/gwm.sock" | jq -r '.result.path'
Without a running daemon, the same data is one gwm invocation away (gwm list --format=json and gwm path <pattern> --format=json emit the identical schemas), but the daemon avoids a process spawn per query.