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 (cd into any worktree of the repo, then gwm 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's gwm statusline --socket at the same path); otherwise the second gwm daemon is 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:

TokenMeaning
feat/#309-…active worktree's branch (or its name when detached)
3 wttotal worktree count
*the active worktree has uncommitted changes
↑n / ↓ncommits ahead of / behind the upstream
#309linked issue number
PR #310linked PR number
claudean 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 a gh call 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 activeidle, 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.