gtmux

gtmux / 文档docs

CLI referenceCLI 命令参考

Start with the commands you use most. Expand a category below for full syntax and details. For the installed version’s complete command list, run gtmux; for one command’s flags, run gtmux <command> --help.

Common commands

I want to…Command
See all agents and jump to onegtmux agents --watch
Open a specific panegtmux focus %7
Let HQ watch the fleetgtmux hq
Check and fix setupgtmux doctor --fix
Keep remote access on across rebootsgtmux tunnel --service
Connect my own phone or browsergtmux pair
Give a collaborator view-only access for a daygtmux share new --view %7 --expires 24h

Replace %7 with a pane from gtmux panes. Pairing needs reachable remote access; start with the phone and browser guide if this is your first connection.

Full reference

Monitor agents and the Mac

gtmux agents

Lists the coding agents running in your tmux panes, sorted by urgency.

gtmux agents · 7 agents · 1 waiting · 2 working · 3 idle · 1 running

‖ waiting  Claude Code  api:0.0                permission to run tests %7
⠿ working  Claude Code  hq:0.0                 api is waiting on you · rest normal %1
⠿ working  Claude Code  web:0.0                refactor auth middleware %11
✓ idle     Claude Code  app:0.0                wire up the dashboard %9
✓ idle     Codex        worker:0.0             add retry backoff %8  ✓ latest
✓ idle     Gemini       docs:0.0               draft the API reference %3
● running  Claude Code  infra:0.0              — %5

jump: gtmux focus <pane>   (e.g. gtmux focus %7)

Each row is status · agent · location · task · pane id.

  • ‖ waiting (red): blocked on you for a permission or approval mid-task; sorts to the very top, and is the one colour on the screen that means act now.
  • ⠿ working (cyan): busy, leave it alone.
  • ✓ idle (green): finished its turn, your move when ready (not urgent).
  • ● running (grey): an identified agent process whose turn state is not known. The terminal table also displays watched plain panes here and counts them as running.

The marks are text-presentation characters on purpose. ⏸ and ✳, which these replace, carry emoji presentation: a terminal may draw them from a colour emoji font that ignores the colour it was given, which put the red on the word and not on the mark beside it.

  • ⚠ errored (amber): an idle session that ended on an API or tool error (for example Unable to connect to API) instead of a clean finish. It is still idle (your move), and the row shows the error summary. In --json: error: true plus error_text.

gtmux agents --watch is a live, auto-refreshing dashboard (built with bubbletea): polls about every 1.5 s, ↑/↓ select, Enter jumps to the pane, r refreshes, q quits. It groups the fleet under NEEDS YOU · WORKING · IDLE · RUNNING, carries a since column, and ends on one line of plan state: the tightest three windows, the full one in amber. Columns give way as the terminal narrows (the agent name first, then the task) so no row ever wraps. --json emits the same data for scripts and the menu-bar app.

How detection works (not Claude-only)

  • Status comes from the pane title the agent sets itself. A leading braille spinner (⠋⠙⠹…, what most agent TUIs animate) means working; Claude Code’s ✳ means idle.
  • Which agent is matched by foreground command (claude, codex, gemini, cursor, opencode, …) or by a name in the title.
  • Extend or override via ~/.config/gtmux/agents.json, a JSON array of {"name","commands","idleGlyph"}; your entries win over the built-ins.
  • A pane is listed only if the agent process is actually running. A leftover agent title over a plain shell (a resurrect-restored session never relaunched) is not counted.
  • Agents running outside tmux (a bare codex/claude in a terminal) are sensed read-only via the same hook and listed under Elsewhere with source:"native". They have no pane (no jump, no reply). An idle, resumable conversation with a log on disk can be moved with gtmux adopt <session_id>; ChatGPT desktop Codex conversations stay in their owning app. A native Codex row uses its saved conversation title when Codex provides one; otherwise clients show the project or terminal name. If a Codex completion hook is missed, its own completed rollout also moves the row out of Working. Other agents retain the title fallback until they have a verified title source.

‖ waiting and latest normally come from state files written by the notification hook. Two narrow screen fallbacks can also show a wait: a live Codex approval menu, and a tracked dispatch stuck at startup or holding its undelivered draft. A spinner title over a shell requires a live agent in its process tree; text in a sh -c command string is not that evidence.

gtmux panes

gtmux panes lists every tmux pane, coding agent or not. It is the read-only producer behind the pane browser: a session → window → pane tree, or --json for a structured array. Each pane carries a tier of "agent" (a coding-agent pane, classified exactly as the radar does) or "plain" (a shell, editor, dev server, log, anything else), plus its loc, cwd, current command, title, active flag, and copy-mode flag.

gtmux panes            # session → window → pane tree; ▸ marks agent panes
gtmux panes --json     # [{pane_id, loc, session, window, pane, cwd, command, title, active, in_mode, tier, agent}]
gtmux panes watch %N   # opt a PLAIN pane onto the radar as a watched row
gtmux panes unwatch %N # remove it
gtmux panes --watched  # list watched pane ids

gtmux agents --json is the agent radar, including sensed native sessions and explicitly watched plain panes; panes is the full tmux set for the pane browser. gtmux focus, send and attach work on any pane id, so a tier:"plain" pane is a first-class focus, type and attach target; it just does not get the agent-only intelligence (digest, 1/2/3 approval, dispatch, HQ).

The tier capability matrix:

tieron the radarview/capturefocus/jumptype/sendattachintelligence (digest / 1·2·3 / dispatch / HQ)
agent (tmux)auto✓✓✓✓✓
plain tmux paneopt-in (panes watch)✓✓✓✓—
sensed non-tmux agent”Elsewhere”————read-only digest / usage

Native rows have no pane viewer or capture. Their title and log-derived digest goal/last are summaries, not a conversation viewer.

A plain pane appears on the agent radar only when you opt in with gtmux panes watch %N, as a distinct watched row (no agent status), and it is dropped automatically when the pane closes. Guest share scope still gates view and type on any pane the same way.

gtmux usage: token watch

PLAN   % used, and when the window comes back
  claude 5h                   16% ██░░░░░░░░░░   back in 2h 24m  Jul 13 at 1:29am
  claude week (all models)    74% ████████░░░░   back in 4h 34m  Jul 17 at 10:59pm

CONVERSATIONS  8                                  out    ctx   rate
  each conversation since it started
  ⠿ api:0.0                                      2.1M    85%   7k/m   ⚠ ctx 85%
  ⠿ web:0.0                                      830k    60%  391/m
    … 5 more idle, 50k between them

TOTALS   every agent on this Mac
  today                     2.8M
  this week                16.2M   claude 15.1M · codex 1.1M
  since Jun 12              233M   busiest day 9.4M · 23 days running, best 31

The plan leads: it is the one number local counting cannot produce, and it decides whether you can keep going at all. The conversation list keeps its head and folds its tail, and the column words are said once in a header that also names the PERIOD — one conversation’s own total can be larger than the whole week’s, because the conversation is older than the week, and the two numbers contradict each other until something says so.

Three different things used to be called a session here. A tmux session is what overview counts and restore brings back; a conversation is one agent’s ongoing chat, which is what this list holds; and Claude’s rolling five-hour allowance is named by its length, claude 5h. --json still carries the agent’s own label.

The today and this week lines show output tokens by local day across the agent logs gtmux can read; each message is attributed to the day it happened. “This week” is the last seven local dates including today, not the calendar week. The ledger scans logs modified within the last eight days and retains daily totals for 366 days; it does not backfill a whole year’s untouched logs. --json carries output and non-cached input totals for the last seven days under history, and the ledger’s retained history under history.activity (every day with output, the total since its first retained day, the peak, the streak). The since … busiest day … line summarises that history, and gtmux usage --activity draws it as the calendar heatmap the phone and the Mac reader show (weeks across, Monday to Sunday down, GitHub’s five greens; as many weeks as the terminal is wide, COLUMNS respected):

Token activity   last 26 weeks
all 70.8M · peak 5.3M · streak 14d (best 25d)

      Apr     May       Jun       Jul       Aug       Sep
Mo  · · · · · · · · · · · · · · · · · · ░ ░ ░ ▓ ░ · █ ▓
    · · · · · · · · · · · · · · · · · · ░ ░ ░ ▒ ▒ · ▒ ░
…
  Less · ░ ▒ ▓ █ More

Per-session token accounting is parsed from the agent’s own log without a model call: cumulative output/non-cached input, the context footprint of the last usage observation, and a recent spend rate. Context and rate use the last 1 MiB of the log, with a rate window of up to ten minutes and a one-minute minimum denominator. The context window comes from a configured window override first, then a window reported by the log, then inference from observed context sizes for that model on this machine. Layered thresholds per agent type live in ~/.config/gtmux/usage.json:

{"claude": {"ctxWarn": 0.8, "sessionOutWarn": 20000000,
            "typeRatePerMinWarn": 30000},
 "horizonMin": 30}

The evaluator projects context and session burn at the observed rate (current + rate × horizon; for example ctx→80% in ~9m). This is an estimate, not a deadline. A session above sessionOutWarn warns only while its recent rate is positive; stopping eventually clears the burn warning. typeRatePerMinWarn compares the type’s summed rate with its threshold and appears in the type rollup. Session warnings surface as usage_warn on radar/digest rows and in gtmux usage. Hooks update the warning at lifecycle events; a long silent generation waits for its next event. A newly reported layer can send a » gtmux·usage·warn … wake to a live HQ, subject to a 30-minute minimum interval per pane across all layers. A brief clear does not reset that interval; hqNudge:false disables the wake. GET /api/usage serves the same JSON shape as --json and requires owner access.

Claude records what each message cost, so its totals are a running sum. Codex records the session’s running totals after every turn, so its totals are the last reading, and it states model_context_window outright (unless a configured override takes precedence). The current fleet report skips conversations whose session ID or log cannot be found. An existing log without parsed usage yields a row with zero numeric usage fields.

Agent launch proxy: GTMUX_AGENT_PROXY takes precedence over agentProxy in ~/.config/gtmux/config.json. Set an explicit proxy URL with gtmux config agent-proxy <url>, or use off to stop gtmux adding a proxy prefix. gtmux does not detect networks or probe a local proxy port. An unset value adds no prefix, and a command that already sets HTTP_PROXY or HTTPS_PROXY keeps its own setting. Inherited environment variables are not cleared by this prefix logic.

gtmux resource: local machine resource watch

disk 40GB free · mem 38% free (warn) · load 0.64×14 cores · power 74% (battery 2:13)   ⚠ disk getting low · 40GB free
per-agent (RSS · CPU):
  %26    252MB · 9.2%
reclaim candidates (orphans no live agent owns):
  pid 3015  100MB · 0.0%  iOS Simulator runtime (12 procs) [simulator]
    ↳ leftover iOS Simulator runtime — `xcrun simctl shutdown all`

Disk (df), memory (memory_pressure -Q free % + the kernel kern.memorystatus_vm_pressure_level normal/warn/critical tier), CPU (loadavg÷cores), and power/battery (pmset -g batt: charge % · on-AC vs draining · time left; the CLI hides the battery line when present:false, which is what a Mac with no battery reports; JSON omits the object when the command fails, its answer has no power-source line, or a battery line’s charge cannot be read). A low charge counts toward the warn/tier only while draining, never on AC. Per-agent RSS/CPU by walking each pane’s process tree, and reclaim candidates: heavy processes no live pane owns, named with pid plus how to reclaim (a leftover iOS Simulator runtime aggregates into one entry; dev servers surface individually). Thresholds live in ~/.config/gtmux/config.json’s resource object (diskAmberGB 50 / diskRedGB 15 / loadAmber 1.0 / loadRed 1.5 / orphanRssMB 300 / batteryAmberPct 20 / batteryRedPct 10). In GET /api/usage, resource.agents maps pane IDs to rss_mb and cpu, next to resource.machine and resource.orphans; these are not fields of the token-usage session rows. The serve tick emits a resource·warn nudge to HQ (one per crossing); gtmux hq/new warn at a red line before adding load.

A candidate first has to survive one question: if this ends, what ends with it? Whatever the running work stands on — the tmux server every session lives inside, gtmux’s own resident processes, the agents themselves — is never offered, whatever its size. The warning once read disk getting low · 29GB free — maybe reclaimable: tmux, and acting on that frees nothing and stops every piece of work on the machine at once. Such a process reaches the list for a structural reason, not by accident: the list looks for something large that no task claims, and the floor is long-lived, sizeable and claimed by nothing precisely because it is the floor.

A candidate rides the warning only where ending it would help. It is a process, so its size is memory: the suggestion accompanies a memory or load warning, names what it holds and in which resource, and stays away from a disk or battery warning entirely. Killing a 902MB process returns no disk, and the line that suggested it during a full disk was acted on twice before anyone noticed (#1109).

The warning is damped three ways so a value sitting on a threshold can’t re-alert (the readout itself stays raw):

keydefaultwhat it does
diskHysteresisGB2GB of headroom above the entry line before a disk tier clears (red at <15 GB clears at ≥17)
loadHysteresis0.15load÷cores below the entry line before a load tier clears (amber at ≥1.0 clears below 0.85)
batteryHysteresisPct3% of charge above the entry line before a battery tier clears (amber at <20% clears at ≥23%)
confirmSamples3consecutive agreeing samples before a tier change is believed
minRestateMinutes30quiet period before the same tier warns again; an escalation to a worse tier is exempt and always warns

A full disk (0 GB free) and a battery draining at 0% are red, like any reading under the red line; a df or pmset that does not answer counts as no reading. A warning waiting to be typed into HQ is read again just before delivery. If the machine is back to normal, the warning is dropped; if it eased (red to amber), it says what is true now. If any reading could not be taken, it is delivered as it was.

gtmux limits: real subscription-window remaining

% used, and when the window comes back
  claude 5h                   16% ██░░░░░░░░░░   back in 2h 24m  Jul 13 at 1:29am
  claude week (all models)    74% ████████░░░░   back in 4h 34m  Jul 17 at 10:59pm
  claude week (fable)        100% ████████████   back in 4h 34m  Jul 17 at 10:59pm
  codex week                   0% ░░░░░░░░░░░░   back in 2d 15h  Jul 20 at 10:02am

  claude week (fable) is spent until Jul 17 at 10:59pm. Claude Code keeps answering;
  that window is at 74%.
read just now

Each reported window has a bar. A closing line names a full window and its reset, when supplied, or a weekly window near its warning threshold.

These figures come from the configured command’s output or the agent’s log; gtmux does not estimate subscription usage from its local token totals:

  • For Claude, gtmux parses the configured limitsCommand (default claude -p "/usage"). The command must work with your installed agent and account.
  • For Codex, gtmux reads the rate_limits records in local rollouts without launching an agent. Each read considers the eight newest rollout files by modification time, looking for a reading in each file’s last 1 MiB. It also does this when the command is disabled or its cache is fresh.

Two rules apply to the log route. A window is named by its duration, never by its position in the source (Codex’s primary field is observed carrying the weekly window as well as the 5-hour one). And a window whose reset has passed is dropped, since a log is only as fresh as its last turn.

When Codex has no live window, gtmux reports a gap if it found a window reading in a rollout modified less than seven days ago. gtmux limits names Codex and says its window has ended. The PLAN section of gtmux usage says codex plan unreadable. Without such a recent reading, no gap is added; this is not a check of your account.

gtmux limits lists every reported window. When gtmux usage has session data to show, its opening PLAN section also lists every window. With no session usage data, the plain usage command prints a no-data message; use limits for the plan alone. The mobile usage sheet also lists the windows by plan; compact summaries may select one window per plan. The warning rule is different: the weekly threshold warning ignores 5-hour windows, though any window at 100% carries a full tier.

Every window says whose plan it is, the first agent’s included: claude 5h, codex week, never a bare one. The spawn preflight prints the warning, so it names the plan the work will bill against. Because the Claude route spawns a process, results are cached with a 15-minute TTL, shortened to 5 minutes once any window is near its cap; --refresh forces one. Configure in ~/.config/gtmux/usage.json:

{"limitsCommand": "claude -p /usage", "limitsTTLMin": 15,
 "limitsTTLNearMin": 5, "limitsNearPct": 70, "limitsWarnPct": 85,
 "limitsTimeoutSec": 60}

Set limitsCommand with an env prefix if your network needs it ("HTTPS_PROXY=… claude -p /usage"). An empty limitsCommand stops command refreshes; it keeps the last good command result and still reads Codex’s local windows. A run that outlives limitsTimeoutSec is killed. It runs in an empty directory of its own (~/.local/share/gtmux/probe): the agent session it starts looks through the folder it starts in, and from /, where gtmux runs, that meant your Photos, Music and Documents, with macOS asking you in gtmux’s name. If that directory cannot be made, the command does not run at all, and the attempt counts as a failure. One refresh runs at a time, across serve, the menu-bar app and the CLI: a second caller serves the cache meanwhile, and --refresh waits for the running one and takes its outcome, even a failure; with no refresh running, --refresh runs one at once, backoff or not. If that lock cannot be taken at all, nothing refreshes and the cache is served. A run that fails is never cached as fresh, so the plan figures you already have are kept instead of blanked, and the command backs off (1, 2, 5 minutes, then the TTL) instead of being retried by every caller. A weekly window at or over limitsWarnPct marks amber and wakes a live HQ once (» gtmux·limits·warn …). The limits block also rides gtmux usage and GET /api/usage.

gtmux logs: what gtmux saw and what it did

Every gtmux process writes to one local store, in the spirit of the macOS system log: serve, the tunnel client, the hook, every command and the menu bar. It holds two kinds of entry. Diagnostics say what gtmux saw. Actions say what it did to this Mac, who started it, what it acted on, and how it ended (ok, refused with a reason, or failed with the error). The actor is user for a command you typed, hq for one HQ ran, agent:%7 for one an agent ran from pane %7, menubar, a phone or browser by device (phone:3f9c20e1), a share link (guest:…), or system for what serve and the hook do on their own.

For analysis, gtmux logs --json emits one JSON object per line with ts, level, component, kind, event, msg, and scalar attrs; actions also include actor, target, and outcome. The session journal is separate: gtmux events --all --json has ordered seq records. Knowledge changes live in HQ’s append-only ledger and can be inspected with gtmux knowledge list --json. A maintenance completion joins its request by kind and request_at in the event journal and by request_at in the diagnostic action. Audited actions share an op_id across event and diagnostic records; a knowledge mutation carries it in the ledger as well. Older records and ordinary agent lifecycle events have no such ID. A failed append warns on stderr without printing the entry text, at most once per store per minute.

09:36:05 serve   serve.start  serve started · backend=direct port=8765
09:41:12 serve   act.send  phone:3f9c20e1 → %7 ok · bytes=42 via=tunnel
09:44:02 serve   warn  act.pair  anonymous refused · a pairing code was not accepted · reason=expired via=tunnel
gtmux logs                                   # the last hour
gtmux logs --since 1d --acts --actor phone   # everything a phone did today
gtmux logs --event 'act.pair' --since 2h     # each pairing attempt, and why one was refused
gtmux logs --level warn --since 3d           # warnings and errors, refused actions included
gtmux logs --follow                          # new entries as they arrive
gtmux logs --json --since 10m                # raw entries, for scripts and agents
gtmux logs --since 1d --stats                # how much is kept, and how much of today went wrong

--stats answers about the store instead of printing it: its size and oldest day, the retention in force, and how many entries in the window were warnings or errors. With --json it is one object, which is what the menu bar reads for its Diagnostics section.

A refused pairing names one of three reasons: expired (the code’s 5 minutes ran out), used (a code works once), or unknown (this serve never issued it, which is what a code minted before a restart looks like).

Every action has a stable event name, which is what --event matches. This is all of them, each with the commands that record it (serve is what serve does for a phone, a browser, a share link or the CLI; hook is the agent hook; app is the menu bar app):

act.adopt               adopt
act.app.launch          app
act.attach              attach, serve
act.awake.off           awake
act.awake.on            awake
act.capture             capture
act.cleanup             doctor, serve
act.config.set          config, quiet
act.doctor.bundle       doctor
act.doctor.fix          doctor
act.focus               focus, serve
act.session.follow      follow, serve
act.hq.brief            hq
act.hq.export           hq
act.hq.import           hq
act.hq.migrate          hq
act.hq.maintenance      hq
act.hq.rotate           hq
act.hq.start            hq
act.install.app         install
act.install.hooks       install
act.advice              advice
act.knowledge           knowledge, serve
act.knowledge.sync      knowledge, doctor
act.mint                pair, serve
act.narrow              serve
act.new                 new
act.notify              hook
act.notify.post         app
act.notify.suppressed   hook
act.device.rename       serve
act.pair                serve
act.push.forget         devices, serve
act.push.register       serve
act.reap                reap
act.reap.snooze         reap
act.relay               relay
act.restore             restore
act.resume              restore
act.revoke              pair, devices, share, serve
act.screenshot.capture  app
act.screenshot.freeze   app
act.screenshot.send     app
act.send                send, serve
act.share.config        share, serve
act.share.create        share, serve
act.share.set           share, serve
act.spawn               spawn
act.tunnel.off          tunnel
act.tunnel.on           tunnel
act.tunnel.move         tunnel
act.tunnel.redeem       tunnel
act.uninstall.app       uninstall
act.uninstall.hooks     uninstall
act.unwatch             panes
act.update              update
act.upload              serve
act.wake.delivered      serve, hook
act.wake.dropped        serve, hook
act.wait.suppressed     hook
act.watch               panes

restore writes its reasoning here too, always: which save it picked and which conversation each pane was matched to (gtmux logs --component restore --since 1d).

Entries are English and never hold message text: a send records its length and a short hash. Tokens, pairing codes and Authorization values are replaced where the entry is written. The store is ~/.local/share/gtmux/logs/, one file per day, readable by you only. It keeps 30 days or 100 MB, whichever comes first (logs.retainDays and logs.maxMB in ~/.config/gtmux/config.json). Whichever process writes the first entry of a day also removes what has expired, so the store stays bounded without serve. A day that passes 20 MB starts a second file, and one log.runaway entry names what filled it. GTMUX_DEBUG=serve,tunnel (or all) adds debug entries for one run; "debug": "hook" in config.json does it for every process, including the ones launchd starts. What a daemon prints before it can log, a crash for instance, goes to logs/<component>.stderr, which serve’s sweep caps.

The menu bar has the same three things without a terminal. Preferences › Diagnostics says how much the store holds and how many of today’s entries went wrong; Open shows the last three days as a list, newest first, switchable to problems only; Pack… runs the bundle below and says where the file landed; and Record extra detail is gtmux config debug (CLI processes pick it up when they next start; the app refreshes its own switch when you change it or it reads log stats, so turn it off when you are done).

gtmux doctor has a Logs section: whether the stores can accept writes, their size and oldest day, a runaway writer in the last week, errors in the last day, whether any file gtmux keeps is readable by another account on the Mac, and the other stores against their bounds. gtmux doctor --fix runs the cleanup and narrows file modes. Nothing here is uploaded anywhere.

To report a problem, gtmux doctor --bundle packs one file: the log store, the status files, the last 256 KB of each launchd capture, the doctor report as text and the versions of gtmux, the app, macOS and tmux. It lists what it packed. Every token and pairing code gtmux keeps is replaced again on the way in, including in the launchd output that never went through the store. The event journal is left out because it holds the heads of your prompts; --with-events adds it. The file is readable by you only, and where it goes is up to you.

gtmux doctor --bundle                  # gtmux-diagnostics-20260920-0930.tgz here
gtmux doctor --bundle ~/Desktop/r.tgz  # a path of your own; an existing file is never replaced

The phone keeps its own record of the same kind: its failed requests to the Mac, each pairing attempt and why it failed, push registration and the live stream dropping and coming back, the last 500 entries or 200 KB. It stays on the phone. Settings → Diagnostic record opens it: each entry as a sentence (“Could not reach the Mac · GET /api/agents did not answer after 6s, then 4 more times in a minute”), grouped by day, with a problems-only filter. Copy or Share hands the record over untranslated, as JSON lines in the shape gtmux logs --json prints, so both sides of the same minutes read together.

gtmux overview

gtmux overview — 2 sessions · 3 windows · 5 panes

▶ web-api              1 window · 1 pane
    0: web-api *  (1 pane)
● worker               2 windows · 4 panes
    0: editor  (1 pane)
    1: claude *  (3 panes)

▶ current  ● attached  ○ detached   * active  Z zoomed  • new output

A sessions/windows/panes summary from any shell. --popup is size-fitted for a tmux display-popup, so you can bind it to a key and float it over a full-screen program without interrupting it.

Sessions and navigation

gtmux restore

Quitting your terminal leaves the tmux server and all sessions alive; only the tabs are gone. After reopening, run once in any tab:

gtmux restore            # one terminal tab per tmux session, all attached
gtmux restore --pick     # choose which sessions: "1 3" / "1,3", Enter = all, q = cancel
gtmux restore --one      # attach the next unattached session in this tab
gtmux restore <name>     # attach a specific session here
gtmux restore --dry-run  # print what would happen, change nothing
gtmux restore --plan     # preview: which sessions + agent conversations would come back (read-only)
gtmux restore --plan --json   # the same plan as JSON (the menu bar's source for its expandable restore row)

One restore at a time: a run holds a lock (pid + start time) and a second run says so and does nothing. A lock whose process is gone, or older than 10 minutes, is taken over. --plan and --dry-run are exempt from the lock.

A real gtmux restore prints its plan up front: the sessions it is about to bring back and the agent conversation (goal) under each pane. --plan is that preview on its own: it reads the last resurrect save plus the resume records and starts no tmux (safe to run or poll anytime). An agent line marked × is a conversation whose transcript is gone from disk and will not resume.

Only a pane that was running an agent when the layout was saved gets one back; restore reads that from the save’s own record of each pane’s command. A pane that was a plain shell at save time comes back a plain shell, even if you ran an agent in it last week. The conversation a pane gets is its resume record; if that is missing, restore reads the id out of the --resume the save recorded it running. If neither identifies the conversation, a directory/position fallback is allowed only for one unclaimed candidate whose original location is no longer present. Ambiguous matches are skipped with a receipt, and a conversation already bound to a running pane is not resumed into another pane. The read-only plan describes the saved workspace; execution additionally checks live owners.

Restore always prints the moment it is putting back, for example “Restoring the layout saved at 09:57 (37m ago)”. The autosave that writes that file hangs off tmux’s status bar, so it only runs while a terminal is attached and redrawing: close the lid and it saves nothing. gtmux serve backstops it by watching the file: if nothing has written the save for about 10 minutes (about 20 when an autosave trigger is present) serve runs the save itself. gtmux doctor’s resurrect autosave row flags an armed trigger that has not saved for hours.

If that row says trigger missing, gtmux doctor --fix offers to add the installed continuum script to the existing status bar and activates it in the running tmux. The change lives in the backed-up managed block of ~/.tmux.conf; reloading the file will not add a duplicate. If doctor reports multiple triggers, review the existing status line: the fixer will not remove custom entries.

After a restore, gtmux compares every saved window’s pane count and arrangement against the live one and names any that differ, on the terminal and in the log store (gtmux logs --component restore), since tmux-resurrect discards its own layout errors.

A tmux pane id is a per-server sequence number: restart the server and %25 is handed to a different pane. gtmux keys a lot of state by that number, so restore (and, every few minutes, gtmux serve) drops pane-keyed records whose panes are gone. Conversation records (resume/, usage/) are keyed by locator and conversation id and are never touched.

To diagnose a restore without a reboot, point XDG_DATA_HOME at a copy of any save and preview it read-only:

mkdir -p /tmp/probe/tmux/resurrect && cd /tmp/probe/tmux/resurrect
cp ~/.local/share/tmux/resurrect/tmux_resurrect_<stamp>.txt . && ln -sf tmux_resurrect_<stamp>.txt last
XDG_DATA_HOME=/tmp/probe gtmux restore --plan     # what restore would bring back from THAT save

The first run may ask for Automation permission to control the terminal hosting your tabs (Ghostty, iTerm2, cmux, or Warp); click Allow. After a reboot the tmux server is gone too; gtmux restore starts tmux and explicitly drives tmux-resurrect to restore the last autosave (it waits for the restore to finish, large layouts take 30 s or more, and if a saved layout exists but can’t be restored it refuses to overwrite it). Restored programs are new processes. By default, gtmux relaunches an agent that was running at save time when its conversation can be recovered, using that agent’s resume command. --resume-agents=type pre-fills the command without running it; off leaves the panes alone. Other programs are not resumed by this agent recovery step.

Each pane’s previous output (scrollback) comes back too, as a snapshot, when resurrect is set to capture it. Recommended in tmux.conf:

set -g @resurrect-capture-pane-contents 'on'   # snapshot each pane's scrollback
set -g history-limit 50000                     # how much scrollback to keep/restore

The shell’s ↑ command history is separate: it lives in your shell’s histfile, not in resurrect. By default it is written only on shell exit, so a reboot loses recent commands. To persist it immediately (bash): shopt -s histappend; PROMPT_COMMAND='history -a' in ~/.bashrc (zsh: setopt INC_APPEND_HISTORY).

The history behind these rules (phantom agents after a reboot, silent layout failures) is in TROUBLESHOOTING.

gtmux new

gtmux new                    # a session named for the current directory
gtmux new api                # …named api

Creates a tmux session and opens a terminal tab attached to it, through the same terminal driver focus and restore use, so the tab lands where you can see it instead of in a detached session you then have to go find. The session starts in the directory you run it from; from the menu bar’s New session, which runs at /, it starts in your home folder.

gtmux transcript

Read a verified external Codex conversation without enabling HQ follow. Take its exact session_id from gtmux agents --json.

gtmux transcript <session_id>                 # public prompts and replies
gtmux transcript <session_id> --json          # turns, dropped count and etag
gtmux transcript <session_id> --json --etag <revision>

JSON includes tool segments. Unchanged conditional reads return turns:null and unchanged:true; errors exit nonzero instead of pretending history is empty. The view is bounded to 300 turns and 512 KiB. Unverified identities are refused. Use --home-id <home_id> from the radar row to select its configuration identity. Mac and paired phone/iPad offer read-only viewing with updates; Web remains status-only. Continue replies in the original client.

gtmux follow

Set HQ follow for one verified external Codex conversation. Detection alone only displays its status. Take its session_id from gtmux agents --json.

gtmux follow <session_id> --json
gtmux follow <session_id> --hq on                 # observe and report only
gtmux follow <session_id> --notify on             # independently allow notifications
gtmux follow <session_id> --knowledge on          # collect future reusable experience
gtmux follow <session_id> --hq off                # stop; also disables both permissions

--revision <n> requires the revision read earlier; a conflicting save fails rather than overwriting another device. Settings are local to this Mac and conversation, outside HQ backups/migration. Stopping keeps existing records, clears future permissions, and ends the current follow interval. Knowledge capture starts with new activity after enabling it (the next Unix second); it does not mine the earlier conversation.

HQ follow does not grant input, approval or Move to tmux. Native Codex current control remains unconfirmed; its creation originator does not grant authority. Other agents keep their existing behavior. --home-id <home_id> scopes follow to the selected configuration. The menu bar and paired phone/iPad offer the same settings; guest links cannot change them.

gtmux adopt

gtmux adopt 4f0c1a2b                 # bring that conversation into a new tmux session
gtmux adopt 4f0c1a2b 91de77c4        # several at once

An agent started outside tmux is sensed read-only (the Elsewhere section: its hook fires with no $TMUX_PANE, so gtmux knows it exists but has no pane to show, jump to, or type into). adopt resumes the conversation by session id inside a fresh tmux session, and from then on the row is a full one. Take the id from gtmux agents --json (session_id) or the radar row. The conversation must be idle, its agent must support resume by id, and its log must contain a readable message timestamp. Codex conversations cannot currently be moved: the available protocol does not prove exclusive frontend control or provide a verified handoff. adoptable:true on the radar row means these checks currently pass; the command checks again before creating anything.

Once the resumed agent has taken over its new pane, gtmux tries to terminate the original process, checking the recorded command name when available to guard against PID reuse. This is best-effort: an unknown PID or a failed termination can leave the original running. The original terminal tab stays open. If the resumed agent does not take over, gtmux removes the tmux session it made and leaves the original running, so you can try again. If it cannot remove that session, it names it and the command that removes it; do that before trying again.

gtmux focus

gtmux focus web          # bring the terminal tab showing session "web" to front
gtmux focus %11          # jump to that exact window+pane, then focus its tab

Each tab title is session — window, so focus finds the matching tab and brings it to front (via the terminal’s AppleScript). A pane id (%N) also selects that window+pane inside the session, so you land exactly where the agent is, which is how a notification click drops you on the agent that just finished.

A session with no window open (a --headless spawn, or one you detached) has no tab to bring forward, so focus opens one and attaches it. The test is the session’s client count, not how the session was started: a headless session someone attached later is an ordinary jump. Surfaces mark such a row (no window / 无窗口) so you know a tab will open before you click.

Needs set-titles on with set-titles-string '#S — #W' so tab titles stay in the format focus matches. If another tool also writes the tab title, disable that so titles stay authoritative.

Host terminals: Ghostty, iTerm2, and cmux can be focused by their live tab/panel titles. cmux uses its AppleScript dictionary for focus and to open workspaces for restore/new. Because cmux sets TERM_PROGRAM=ghostty, gtmux distinguishes it by CMUX_WORKSPACE_ID or the tmux client’s cmux.app process ancestry. Warp is best-effort: it has no AppleScript dictionary, so focus jumps to the exact tab only when that tab’s Warp session uuid was recorded into the tmux session env (gtmux’s own restore/new attach does this; or add WARP_TERMINAL_SESSION_UUID to tmux’s update-environment to cover hand-opened tabs), and otherwise just activates the Warp app; restore/new open Warp tabs via launch configurations. Other terminals fall back to the Ghostty driver. The host is auto-detected; GTMUX_TERMINAL=ghostty|iterm2|cmux|warp overrides the detection.

HQ and agent coordination

gtmux digest + gtmux hq: HQ, the supervisor session

gtmux digest prints the fleet at a glance with meaning, as a column-aligned table:

1 needs input · 2 working · 1 completed

needs input (1)
  ⏸ api:0.0        1.Yes · 2.Yes, don't ask again · 3.No             3 opts   4m

working (2)
  ⠿ web:2.0        fix the login-token refresh bug                 working   1m
  ● hq:0.0 ⌂       ctx 92% — approaching limit                          ⚠   3h

completed (1)
  ✳ mobile:1.0     done, tests pass                                          2d

A one-line count by state leads, then a section per state (needs-you, working, completed, then errored if any). Each row is status glyph · name · goal/last/ask · a right badge (dispatch status / ask-option count / usage warning) · a relative time. Every field is assembled deterministically (zero LLM tokens): goal is the session’s last user prompt, last is the tail of its last reply (both from the agent’s own transcript), asks are a waiting prompt’s parsed options. --json emits the machine form (also served as the owner-only GET /api/digest; guests receive 403). With no session log, goal/last and usage fields are empty; radar state and parsed options still appear. Usage reads the session log separately and is not disabled by the driver’s content switch. Each JSON row reports sense: driver (a session record resolves and its registered, enabled content reader returns without error), partial (the record resolves but the reader is absent, disabled or returns an error), or screen (no session record resolves). A missing log returns no turns and no error, so it can still yield driver. This describes lookup results, not proof that conversation content exists or that every state classification came from a hook.

gtmux hq opens (or focuses, never duplicates) HQ: your coding agent running in a dedicated tmux session at ~/.config/gtmux/hq/, seeded once with a playbook: read gtmux digest --json, judge, drill into a pane (tmux capture-pane) only when warranted, drive via gtmux send, report to you. The playbook is AGENTS.md with CLAUDE.md as an @AGENTS.md import, so HQ can be any CLI agent. It ships in your language (GTMUX_LANG en/zh) and a version change regenerates it in that same language (prior file backed up; LOCAL.md untouched); gtmux hq --lang en|zh is the one thing that switches it.

On a fresh launch with no --agent, gtmux hq asks which installed hook-equipped agent to run and remembers the pick; gtmux hq --agent codex or GTMUX_HQ_AGENT names it outright, and a script falls back to the default without prompting. Personalize HQ in LOCAL.md (same directory): priorities, reporting style, quiet hours. That file survives every playbook upgrade; edits to the managed AGENTS.md are displaced to a backup. Notes HQ keeps in that directory persist across its sessions. In the radar its row carries role:"supervisor".

gtmux hq --help groups common actions into starting HQ, viewing information, backup and migration, and conversation rotation. gtmux help hq shows the same help. Use gtmux hq --help-all for advanced options, environment variables and HQ’s internal maintenance command. Both views wrap to the terminal width (40–80 columns); gtmux --help --json retains the full HQ option catalog. Reading help does not start HQ or change its records.

New dedicated HQ sessions are named Gtmux HQ. Existing session names and custom names stay unchanged. In All panes, verified HQ groups and panes carry an HQ badge; the legacy default HQ/hq is displayed as Gtmux HQ. Role detection still follows the HQ stamp and radar precedence, never the name.

Codex HQ starts with --approve-for-me: routine work stays in its workspace, and requests to cross the sandbox boundary go to automatic review. An explicit permission mode in --agent or GTMUX_HQ_AGENT takes precedence. This applies when launching or relaunching HQ; focusing an already-running session does not change its permissions. On a new Codex HQ process, gtmux also limits Codex’s own terminal notifications to approval and plan prompts so Ghostty does not announce every HQ think-cycle. gtmux hq --rotate sends /new in the same process and cannot update these flags; exit the Codex process and run gtmux hq to apply changed launch options. See Codex integration for attribution, notification, and history.

Where it runs. With no flag, gtmux hq keeps the window HQ already has: a live HQ is focused, a window whose HQ has quit gets it relaunched in place, and only when neither exists does it create its own tmux session and open a tab. A window that hosted HQ once keeps its stamp, so every gtmux hq from anywhere revives HQ there. To put it somewhere else: --pane %N starts it in that pane (an empty shell, cd’d to the HQ home), --here in the pane you are typing in, --new-pane in a fresh split of your window. Each moves HQ’s identity to the new pane, and each refuses while an HQ is running anywhere: there is only ever one.

Starting HQ in the pane you are typing in is a special case, because gtmux is holding that terminal: it hands the terminal to the agent directly instead of typing into itself, and the startup briefing is delivered by a background watcher once the agent’s input box is up. Nothing is typed into the pane meanwhile, which is what used to leave the briefing sitting in the box unsent.

gtmux hq --board [--json] prints the situation board instead of opening HQ (the menu-bar app’s board reader uses it). A board that was never written reports exists:false.

In the Mac menu-bar app’s situation board, click an underlined pane number such as %29 to open its terminal. The row’s arrow expands its details separately. Closed panes or terminal-switch failures show an error. References point to the current pane with that ID; the board itself may be older. This shortcut is specific to the Mac reader; CLI, phone, iPad and Web behavior is unchanged.

gtmux hq --home prints the HQ home path. gtmux knowledge’s mutations are accepted only from that directory, so a surface offering one runs the verb from there. With no HQ home yet it still prints the path and exits non-zero.

gtmux hq --rotate queues a rotation for the current HQ session. It reports pending until HQ’s current turn ends; the resident service then checks the empty input box and sends that agent’s reset command once. Only a different session ID confirms success. Rejected or timed-out attempts leave a failure record in gtmux events --all. HQ’s playbook runs it unprompted once the self-rotate knock says the session is worn out (Self-rotation), after bringing notes/board.md and the knowledge base current. With no HQ running there is nothing to rotate, and it says so.

HQ backup restore and selective migration

The menu bar’s HQ → Knowledge → Import… separates Restore HQ backup from Move from another Mac, with Continue staged migration for later review. gtmux hq --import ARCHIVE restores the entire home; the current home is moved aside as a backup. --expect-archive DIGEST can guard a restore against source changes after preview (source in migrate --from ARCHIVE --restore-preview --json). Exit HQ before restore or application. Preview and staging work while HQ runs.

gtmux hq migrate --from /path/hq.age --json   # read-only preview; asks for passphrase
gtmux hq migrate --from /path/hq.age --stage --include knowledge,local --json
gtmux hq migrate --list --json
gtmux hq migrate --review STAGE_ID --json
gtmux hq migrate --apply STAGE_ID --entries pitfalls/example --reviewed --json
# Separate replacement after comparing the current and source LOCAL.md:
gtmux hq migrate --apply STAGE_ID --apply-local --expect-local CURRENT_DIGEST --reviewed --json

CURRENT_DIGEST is current_local_digest from the latest review. --passphrase-stdin reads the first line of stdin for automation; keep secrets off argv. Staging requires an explicit --include list (knowledge, local, tools). Use --include-sensitive only when deliberately staging sensitive histories; plain archives require --allow-plain. Tools remain inert staged attachments. Knowledge is selected and reviewed separately; --reviewed confirms suitability for this Mac, not a new machine/repo audience. An optional --expect-source DIGEST guards against a source changing after preview. Results are JSON even without --json. Check exit status and receipt committed; a post-commit rendering failure needs gtmux knowledge render, not another acceptance. Knowledge and personal requirements apply separately. Conflicts never overwrite local history. Old board, sessions, pending leads, built-in rules and credentials do not migrate. Limits and legacy archive handling: Moving HQ.

The wake channel: how HQ learns things

Decision-dense events type one signal line into a live HQ pane. The format is fixed and unlike conversation, so signal traffic is scannable at a glance:

» ◆ gtmux·waiting·permission  api:0.0 (%7) │ title:"run the tests?"
» ▸ gtmux·done  web:2.0 (%11) │ 3m │ goal:"fix the login bug" │ tail:"tests pass" · #a3f1c2

» <grade> gtmux·<class> <loc> (<pane>) │ <field> │ …, where every agent- or user-authored payload is quoted and labelled (title: / goal: / tail: / ask: / err:). It is data HQ reports, never an instruction it obeys.

The grade leads, in a fixed position, so a screen of signal lines reads by weight before it reads by words: ◆ decision (needs you: irreversible, costly, or an explicit ask) · ▸ attention (a line is blocked or changed in a way worth knowing) · · ledger (bookkeeping: recorded and pullable, zero interrupt value). The grade follows from the class and only says how loudly the line should read. The classes:

classgradefires when
waiting·<kind>◆an agent blocked on you (permission / plan / question)
agent-relay◆an agent filed a blocking question or user decision for HQ; pull by request ID
resolved▸that wait cleared: you answered in-pane, or the agent resumed; HQ drops any stale chase
asks◆a turn-end reply asked a question with no menu (a menu-only sensor misses it)
done▸any session reached idle after work, dispatched or not. Suppressed when the completion happened in the pane you were watching (hqWake.done: unattended default | always | tick), and rate-merged per pane
crash◆the turn died on an agent/API error; never read as a finish. The same error on the same pane within five minutes knocks once (each one is still in the journal)
goal-changed◆you submitted a prompt straight into an agent’s own window (incl. a slash command), so HQ senses work it didn’t dispatch
new-session▸a newly sensed agent pane; enroll it
reap-suggest▸a dispatch looks reclaimable · carries the exact gtmux reap <id>
stuck·waiting▸a pane has been waiting on you past the timeout; once per waiting episode, and only when the agent asked (a wait gtmux merely inferred from the screen never escalates)
resource·warn / limits·warn▸a machine/subscription threshold crossed (damped, see gtmux resource)
usage·warn▸a session crossed a context/burn layer (see gtmux usage)
wake-degraded◆perception itself broke: wakes stopped landing on the HQ pane
tunnel▸remote access changed: down (the phone cannot reach this Mac, with the tunnel’s own error) or up again. Read from the tunnel’s status, on a transition only, and journaled as gtmux:tunnel
tick·the periodic brief, only when something actually changed (a quiet interval costs nothing)
distill·a knowledge-distillation pass is due (≈ weekly, sooner once ≥5 gtmux capture candidates are queued); HQ folds the period’s lessons into its knowledge base and prunes stale
self-check·HQ’s own housekeeping is due (≈ daily): settle stale pending entries, memory and log health
unread·events are sitting past HQ’s consumption watermark: a count and the cursor to pull from, no importance claim
self-rotate◆HQ’s own session is worn out (context / age / turns); it hands off and rotates itself, see below

The classes that repeat until an act clears them (self-rotate, unread, the warns) re-check themselves: a repeat whose breach set and world are unchanged is suppressed and re-arms on any drift, with a safety floor so a standing debt is never forgotten. A queued wake is re-sampled just before it is typed, and dropped if its premise died meanwhile.

distill and self-check are maintenance classes: they knock at the lowest priority, never ahead of a blocked agent, and both passes are silent unless HQ actually did something. They are the only classes raised purely on a clock; HQ has no timer of its own, so gtmux serve is what makes them happen.

Everything else is pull-side: HQ wakes, then reads gtmux events --since-seq <n> or gtmux digest. Ordinary progress turns never touch its screen.

HQ’s replies to signal lines are signal lines too: one line opening with ⟣ plus a glyph, so its pane scans the same way its inbox does. The legend:

replymeans
⟣ ✅ <pane> <judgment> → <next step>a completion worth knowing about
⟣ ▪ noted: <one clause>routine outcome, recorded to the board; nothing owed
⟣ 📓 captured: <topic>a durable lesson written into the knowledge base
⟣ ⚠ <escalation>something needs you, per the escalation policy
⟣ ◈ 简报 <time> │ <counts> │ top itemthe periodic brief (plus up to 5 indented · lines)

The watermark: why nothing goes missing

The classes above are priority labels: they say what to read first. Whether HQ hears about an event at all is guaranteed by a consumption watermark, gtmux’s record of how far HQ has read the event stream. When events sit past it for hqWake.unreadDebounceSec (default 120 s), gtmux knocks with a count and nothing else:

» · gtmux·unread  7 unconsumed (%21 ×4 · %13 ×2 · control) │ pull: gtmux events --since-seq 6653 --json

The line names what it counted, by source (control is gtmux’s own maintenance triggers, native a non-tmux agent session), most numerous first and folded to +N more past three. (Why the classes alone are not enough is in TROUBLESHOOTING.)

The debt clears only when HQ consumes, and only two things count: an unfiltered gtmux events --since-seq <n> read from the HQ home or a directory beneath it, or an explicit gtmux events --ack <seq> from that same directory tree. A --severity- or --acts-filtered read does not count, even with --all (it showed a subset), nor does one starting ahead of the watermark (it skipped the range between), nor does a read that detected a sequence gap (events rotated away unread): the pull re-warns and the knock gains a · sequence gap marker with a rebuild hint (gtmux digest --json, then the explicit --ack) until HQ acks over it deliberately. Until then the knock repeats every hqWake.unreadRepeatSec (default 300 s), at standing priority.

Three kinds of record are excluded from the count: HQ’s own lines; a pane-less lifecycle blink (a SessionStart with no pane whose SessionEnd follows within seconds); and gtmux’s own audit trail (gtmux:audit:*: each wake batch delivered or dropped and why, every gtmux send settlement, reaps, the HQ session rotation chain). The blink rule keys on the Start/End pairing, never on the empty pane alone: a native (non-tmux) agent’s turns and gtmux’s non-audit gtmux:* triggers are pane-less too, still count, and for them this knock is the only channel, since the class wakes all require a pane.

gtmux events says WHO wrote a prompt when it was not you. Two prompt submissions on one pane look identical whether you typed the words or HQ delivered them, and the only thing that separates them is gtmux’s record of the delivery, which the supervisor’s pull view withholds as something it does not owe. So the reader is simply told: a prompt gtmux delivered on someone else’s behalf prints with ← hq (or ← agent:%N) at the end of its line, and --json carries it as an additive author. A prompt with no author is one no delivery answers for, which normally means you typed it. Each delivery answers for one prompt: the one on its pane whose words agree most closely with its own, and of those the nearest in time (within a couple of minutes). So a delivery does not claim the same words you typed an hour earlier, and when two prompts fit equally well it claims neither.

The attribution is worked out at read time from the delivery trail, so nothing about what is owed or shown changes: the trail stays out of the consumption debt and stays hidden from the default view. --all is not needed for this and is unchanged. A delivery that was refused or failed never reached the pane and attributes nothing.

An unfiltered delta read from the HQ home itself hides those excluded records by default. A subdirectory read shows the raw view; --all also selects the raw view. Either can consume only if the read meets the rules above. Journal files rotate at their size bound, so old generations can leave the retained history.

gtmux doctor’s HQ maintenance section reports the lag (event consumption) and flags an HQ that is 20+ events or 30+ minutes behind.

Delivery guards your draft and confirms itself: a line is never typed into a non-empty HQ input box (it queues to disk and lands when the box clears), and a batch leaves the queue when confirmed or dropped with a recorded reason. An unconfirmed send has bounded retries; the trailing #<id> lets HQ recognise a duplicate, but does not guarantee delivery. The same id twice is a re-send, and HQ’s playbook says to ignore it. Every outcome is journaled (gtmux:audit:wake-delivered with the full batch, gtmux:audit:wake-dropped with the reason: evicted / unconfirmed / superseded) and readable with gtmux events --all.

Self-rotation: when HQ’s own session is the problem

self-rotate is about HQ itself: in a long, near-full session HQ starts to read its own output as input that came from outside, and it cannot catch that from inside. So gtmux serve watches from outside and knocks:

» gtmux·self-rotate  ctx 82% · 14h · 380 turns │ over: ctx 82% ≥ 75% │ board+KB current → hand off → gtmux hq --rotate

Three facts are sensed, and any one crossing its line is a breach:

hqWake keydefaultwhat it measures
selfRotateCtx0.75live context fraction, the same ctx gtmux digest reports
selfRotateHours12session age, from the transcript’s first message (a serve restart can’t reset it)
selfRotateTurns300HQ-pane prompt submissions since the session began

Set any of them to 0 to switch that one criterion off without losing the other two. selfRotateRepeatSec (default 1800 s) paces the re-knock, selfRotateCheckSec (default 300 s) paces the evaluation.

The debt clears only when the session actually rotates, observed as a new agent session id; delivering the wake does not clear it. Past hqWake.selfRotateRepeatSec a repeat fires only when the breach set or the fleet has changed, and hqWake.selfRotateFloorSec (12 h) is the longest it can stay silent when nothing has changed at all. HQ’s playbook tells it to do three things in order, without asking you first: bring notes/board.md and the knowledge base current, record the handoff, then run gtmux hq --rotate. The resident service submits /clear (or /new for Codex) after the current turn ends. A repeated self-rotate after a rotation means the rotation did not take. gtmux doctor’s HQ conversation health row shows the same figures. (The incident behind this class is in TROUBLESHOOTING.)

"hqNudge": false in ~/.config/gtmux/config.json stops hook-initiated wakes (waiting, done, asks, goal-changed, usage·warn and similar hook signals). Serve’s own wakes — summary tick, unread, self-rotate, resource/limits reminders and self-check — continue. It does not remove or stop the HQ session. The wake only informs: gtmux never answers another agent’s prompt, never sends navigation keys into a TUI, and the default policy tells HQ to surface decisions to you, not take them.

gtmux capture: a cheap notice into HQ’s knowledge base

gtmux capture "<one-line lesson> @<topic>"   # topic ∈ the knowledge vocabulary: six built-ins + topics hq declared
gtmux capture --list                         # show the pending-distill queue
gtmux capture --list --json                  # the same queue, with each line's dedup key
distill requested 3d ago; awaiting HQ completion
2 pending-distill candidate(s):
  [pitfalls] wrangler TLS-resets from the office network — retry

Any worker, not just HQ, can drop a one-line candidate into the pending-distill spool (~/.config/gtmux/hq/knowledge/.pending-distill.jsonl). Each line carries the lesson, its topic tag, a dedup key (<topic>/<lesson-slug>, so the distill pass merges same-key candidates), and auto-collected context (the current pane, the event high-water mark, a timestamp, and $GTMUX_TASK_ID if the caller is a tracked dispatch). A candidate is not a knowledge-base entry: HQ’s distill pass decides what is durable, files it under the right topic, and prunes.

--list heads the queue with when it was last drained. Five queued candidates pull the next distill forward, so a captured lesson is filed within a day instead of waiting out the week. --list --json is the same queue as one array (never null), each line with its topic, timestamp, provenance and dedup key, which is what gtmux knowledge dismiss --capture <key> names; dismissing a key consumes every pending line that shares it.

gtmux knowledge mine: session logs into the same queue, without a model

gtmux knowledge mine                 # one pass: read what the agents' logs gained, queue the leads
gtmux knowledge mine --dry-run       # show what a pass would queue, write nothing
gtmux knowledge mine --since all     # widen the correction window from 30 days to the whole stock
gtmux knowledge mine --status        # the ledger: last pass, sources, emitted, recurring errors
read 14 file(s), 3.2 MB, 61 human line(s) → 3 candidate(s) queued, 0 already emitted
  [corrections] this is wrong, do it again
      ↳ after: …moved the toggle into the header and shipped it.
  [pitfalls] bash: wrangler: command not found  (×6, 3 sessions)

The miner runs once a day (hqWake.mineIntervalHours, 0 disables). It reads the coding agents’ session logs from the byte offset it stopped at last time, subtracts everything the machine itself wrote (tool output, harness blocks, gtmux’s wake lines and gtmux send payloads matched against the audit journal, compaction summaries, pastes), and keeps two shapes as leads: a correction-shaped line a person typed right after an agent’s reply, carried with the tail of that reply, and a tool error whose normalized signature recurs across sessions, carried with its count. No model runs. HQ’s distill pass drains the leads with knowledge add --capture / dismiss --capture.

The ledger under ~/.local/share/gtmux/mine/ keeps every offset and every emitted id, so a pass never reads or queues anything twice, and a known error keeps counting after its lesson was filed; --status shows that tally. Sources: Claude Code, Codex, Kimi Code, and the transcript gtmux writes for opencode. Claude Code’s log carries tool output, and of the other three only Codex’s does, so recurring-error leads come from Claude Code and Codex; correction leads come from all four.

gtmux quiet: how much HQ is allowed to say

gtmux quiet on       # CRITICAL only — the quietest setting
gtmux quiet off      # clear quiet; configured/process overrides still apply
gtmux quiet status   # what is in effect right now

This sets the floor in HQ’s reporting instructions. quiet on sets the quiet flag; quiet off clears it. With that flag off, surfaceTier in config still applies, or the NORMAL-and-above default when it is absent. GTMUX_SURFACE_TIER / GTMUX_QUIET can override the threshold for one process; quiet status shows the resolved value.

gtmux tasks --pending lists only entries marked awaiting your decision with gtmux tasks --await. Changing quiet mode does not put events on that list.

One thing is never quieted: a read-time gap in the event log. That is HQ telling you it may have missed something.

gtmux advice: what HQ proposed, and what became of it

HQ’s other ledgers hold what it knows, what it dispatched and what it saw. This one holds what it PROPOSED. It is the only one that can answer two questions the others cannot: is the supervisor’s judgement worth anything, and is it still speaking up at all. A supervisor with nothing to say and one that stopped saying it look identical in every other record.

gtmux advice "<what you advised>" [--why "<the reasoning>"] [--about "<what it concerns>"]
gtmux advice <id> --taken | --declined [--words "<their answer>"] | --moot
gtmux advice [--open] [--limit N] [--json]        # newest first
gtmux advice --tally [--since 30d|12h|all] [--json]

Recording an outcome is a second record, never an edit of the first, so a reversal keeps its history: advice declined on Monday and taken on Thursday shows as taken, and the log still holds both. --taken and --declined are the only two outcomes the rate is computed from. Advice still open, and advice overtaken by events (--moot), are counted and reported but stay out of the denominator, because neither one is a verdict on the judgement behind it.

Writing runs from HQ’s home, by the same rule knowledge mutations use; reading works anywhere, so the tally is available wherever you are. You file nothing: this is the supervisor’s record of its own work, and asking the commander to maintain it would defeat the point.

gtmux knowledge: the knowledge ledger (entries with provenance)

gtmux knowledge add --topic pitfalls --title "wrangler TLS-resets; retry" [--body-file -] [--capture <key>[,<key>…]] [--seq-range a..b]
gtmux knowledge supersede <id> --title "…" [--body-file -]   # replaces an entry; history stays in the ledger
gtmux knowledge retire <id> --why "…"                        # prune, with a reason that survives
gtmux knowledge dismiss --capture <key>[,<key>…] --why "…"   # reject candidates WITH a trace
gtmux knowledge receipts [--capture <key>] [--json]         # committed settlements and retained sources
gtmux knowledge promote <id> --why "…" --for <hq|machine|repo:<path>|everyone>   # WHO must know it
gtmux knowledge land <id>                                  # gtmux carries it: LOCAL.md / every agent's block / the repo file
gtmux knowledge land <id> --ref "<issue url>"              # everyone: you opened the issue the brief links to
gtmux knowledge withdraw <id> --why "…"                    # the entry was right, the promotion was not
gtmux knowledge sync [--force] [--repo <path>]             # refresh the knowledge block in each agent's instruction file
gtmux knowledge carriers                                   # each agent's instruction file and whether it is in sync
gtmux knowledge lint [--json]                              # audit: issues plus standalone-entry info and similar-pair review candidates (reports, never edits)
gtmux knowledge style [--json]                             # how an entry should read: every rule with a before and an after
gtmux knowledge search "<text>" [--topic t] [--kind k]     # ask the base a question in words, in either language
gtmux knowledge neighbours <id> | --capture <key> | --text "…"   # the closest live entries; `add` shows them before writing; `capture --list` groups the pool by them
gtmux knowledge kind <id> <facts|howto|pitfalls|judgment|decisions>   # confirm or correct what an entry IS
gtmux knowledge hit <id> [--n N] [--why "…"]               # the lesson was hit again (its count is the feedback)
gtmux knowledge confirm <id>                               # a hypothesis held up
gtmux knowledge alt <id> --lang <zh|en> --title … [--body-file -]   # write or replace the entry's other-language half
gtmux knowledge add … --sensitive --confirmed "<their words>"   # the commander's own detail: stays on this machine, written after asking
gtmux knowledge sensitive <id> [--off] --confirmed "<their words>"   # mark (or unmark) an existing entry
# add/supersede also take --kind, --tags a,b, --provenance <correction|recurrence|mined|capture|self>, --hypothesis
gtmux knowledge topic <name> --desc "…"                      # declare your own topic (clients, datasets, …)
gtmux knowledge land <id> --ref "<pr/spec>"                  # close the loop when it lands
gtmux knowledge promotions [--json]                          # the pending export queue
gtmux knowledge list [--topic t] [--json]  ·  show <id> [--json]  ·  render [--check]

A reader’s tour of all this — where it sits, how a lesson travels, what you can change — is docs/knowledge.md.

The knowledge base’s authority is an append-only ledger (~/.config/gtmux/hq/knowledge/.ledger.jsonl); the topic .md files are rendered from its live entries, gtmux-owned and drift-checked (render --check catches a hand edit; render restores). Every entry carries provenance: the event seq at write, an optional distill range, and, when it consumed a capture candidate, that candidate’s pane/seq/task and key. add --capture <key> consumes every pending same-key candidate into one entry; dismiss removes them with a journal trace. Every mutation appends one gtmux:audit:knowledge record to the event journal.

Mutations are accepted only from the HQ home itself; workers record candidates with gtmux capture. list/show work anywhere. A second, narrower door exists for the commander: gtmux serve exposes the base to an owner-authenticated client (GET /api/hq/knowledge, the index without bodies; GET /api/hq/knowledge/entry?id=; POST /api/hq/knowledge/act) and accepts exactly four verbs from it: land, retire, carry and withdraw. HTTP land requires a ref; carry uses the selected audience’s carrier instead. Both doors journal the same gtmux:audit:knowledge record.

Six topics ship built in (accounts, workflows, best-practices, pitfalls, corrections, environment); gtmux knowledge topic <name> --desc "…" declares your own, rendered immediately with its description and honored by gtmux capture, every knowledge verb, and the dispatch-time echo (custom topics join pitfalls/workflows there; accounts / corrections / environment stay out of dispatch context). Names are slugs (a-z 0-9 -, ≤ 40 bytes); built-ins, existing topics, and the reserved directory names refuse. Declarations are add-only for now.

Every entry has a language (lang) and may carry the other as an alternate half: add/supersede take --lang and --alt-lang --alt-title [--alt-body-file -], alt <id> adds the half later. Readers get the source when its language matches, else the alternate, else the source with a [zh]/[en] tag. list/show follow GTMUX_LANG (--lang overrides), the files on this machine render in the base’s majority language, the everyone brief and its issue go out in English. lint counts monolingual entries; gtmux never translates, HQ writes both.

A sensitive entry is the commander’s own detail (an account, a personal fact, a credential they chose to keep here). It goes in only with --confirmed "<their own words>", the record that HQ showed them the entry and they said yes. It is excluded from machine/repo distribution (promote accepts hq only; machine.md and repo blocks skip it). A whole-HQ export still includes the ledger, including sensitive entries, so keep that archive private. The Mac and the phone show a lock. lint reports unmarked-sensitive for an entry that reads like a credential without the mark. Other people’s secrets still stay out: a pointer, never the thing.

lint also pairs scripts with entries: a script under knowledge/tools/ that no live entry names is orphan-tool, an entry naming a tools/<script> that is not there is broken-tool.

Every entry sits on three axes: kind (facts, howto, pitfalls, judgment, decisions); provenance (correction, recurrence, mined, capture, self, with a hit count that knowledge hit and the transcript miner keep growing); and audience, set when it is promoted. topic stays as the id prefix and a free tag. Entries written before the axes are mapped at read time and marked ? until knowledge kind <id> <kind> confirms; the ledger file is never rewritten. --hypothesis shelves an unverified lead in its own section, never distributed, until confirm.

promote <id> --why … --for <hq|machine|repo:<path>|everyone> writes a promotion brief under knowledge/promotions/ carrying the lesson, the why, the audience and its exit, and the entry’s provenance. For hq, machine and repo gtmux carries it: land <id> writes the entry where that audience reads (LOCAL.md · the canonical ~/.config/gtmux/knowledge/machine.md plus an index block in every agent’s global instruction file · the repository’s AGENTS.md, not committed) and closes the loop with that path as the ref. For everyone the brief holds a prefilled GitHub issue link; a person opens it and lands with --ref <issue url>. withdraw <id> --why drops a promotion and keeps the entry. gtmux doctor flags a brief past its floor (about 2 weeks); everyone never counts. knowledge sync refreshes every agent’s block, carriers shows each one’s state, and a block someone edited by hand is never overwritten without --force.

knowledge lint reports orphans, broken, ambiguous and outdated [[links]], near-duplicates, stale hypotheses and promotions, kinds still awaiting confirmation, ai-voice (an entry that reads like a machine wrote it) and title-unreadable (a title that names a field where it had one line to say what happens). It never edits, and its one-line summary rides the self-check knock. The JSON severity field and text output distinguish issue, review, and info. Writing hints, uncertain kinds, missing translations, credential-shaped candidates, and outdated references need review before any change. orphan remains the stable JSON check name, but means an unlinked standalone entry: links are optional, and this info finding is not a defect. The self-check summary calls these “standalone entries (info)”. near-duplicate is a review candidate, not evidence that either entry can be removed. Compare facts, provenance, and scope before changing a ledger entry. Example placeholders such as [[...]] are ignored; credential detection requires a credential-shaped value rather than prose following token=. If two topics contain the same slug, write [[topic/slug]]; lint will not guess which entry a bare [[slug]] means.

knowledge search "<text>" asks the base a question in words. neighbours <id> asks a different one, what is near this entry, and add asks it before it writes. All of them go through one retrieval, which is the only thing in the package that decides what a match is. It tokenizes ASCII words and CJK bigrams, so a question with no spaces in it works, and it weights a shared word by how rare it is across the base, so a word a handful of entries carry counts for more than one most of them carry. Measured against the 904 [[link]] pairs the supervisor had drawn by hand across 720 entries, that weighting takes recall from 36% to 55% at ten results. There is no model and no index: one pass to count, one to score, about 60ms over 720 entries. capture --list groups the pool into families so one add --capture k1,k2,… files them as one lesson.

knowledge style is where those rules are written down, and the lint takes its matchers from the same table, so the two cannot disagree. Each rule says what to do, why, and shows a sentence before and after, in English and Chinese. A rule is either checked or left to you: the tiering follows the humanizer skill, where a tell counts in proportion to how rarely a careful writer would make it on purpose. Chat residue, a decorative ⇒, an in-house coinage, a guess stated as a fact, a body that restates its title and a title made of identifiers count on one sighting; dashes, bold runs and “not X, but Y” count only in company; and whether a contrast is earned or a sentence carries a fact is nobody’s match to make, so those rules are stated and never checked. Code, tables and quoted spans are not read at all. Two rules are here for the reader that is an agent rather than a person: everything executable or checkable stays verbatim, and an entry says who, where, what happened and what to do.

A topic file written by hand before the ledger existed is moved to knowledge/legacy/<topic>.md on the first mutation touching that topic; the render links to it and the dispatch-time echo still consults it (details in TROUBLESHOOTING).

gtmux relay: agent requests to HQ

From a managed agent pane, gtmux relay report --body-file /tmp/update.txt records progress quietly. gtmux relay ask --body-file /tmp/question.txt records a blocking question and wakes HQ with its request ID. Use --nonblocking for a question that can wait, --for user for a decision only the user can make, and reuse --id on a retry. HQ reads gtmux relay list --json / show <id>, claims an item, then uses reply <id> --body-file <file> or close <id>. A reply is attributed to HQ and returns only to the original live pane. User-directed items must be answered by the user in that pane; HQ cannot approve them. gtmux relay --help lists the verbs. gtmux knowledge sync puts a short relay note in Claude, Codex, opencode, and Kimi’s global instruction blocks; newly started sessions load it. Other agent types receive the note when dispatched by gtmux spawn.

gtmux spawn / gtmux send / gtmux tasks / gtmux reap: verified dispatch

Direct Codex terminals launched by gtmux default to --no-alt-screen, including spawn, HQ and standard restore/adopt. This preserves future terminal scrollback. Explicit screen choices and custom launchers are retained; running clients and global Codex settings are not changed. Alternate-screen applications expose only their current terminal frame; Chat reads the persistent conversation. See Codex integration.

gtmux spawn <goal> dispatches new work to a coding agent and confirms it landed:

gtmux spawn "add a --dry-run flag to the deploy script"
gtmux spawn --title fix-auth-mw --worktree feat/dry-run --model opus "add a --dry-run flag"
gtmux spawn --pane %14 "keep going, then run the tests"
gtmux spawn --json "…"   # → {task_id, pane_id, loc, title, session, delivered, state, judged_by, evidence}

Anything longer than one short line goes through --goal-file <path> (or --goal-file - for stdin); gtmux send has the same channel as --message-file <path|->:

cat > /tmp/goal.txt <<'EOF'
把 `hqPlaybookVersion` 提到 13,并且:
1. 运行 `make check`
2. 别让 shell 碰 $HOME 或 `for f in *; do echo $f; done`
EOF
gtmux spawn --title bump-playbook --cwd ~/src/gtmux --goal-file /tmp/goal.txt
gtmux send %14 --message-file /tmp/reply.txt

A goal passed as a command-line argument is parsed by your shell first: a backticked span written directly inside double quotes is executed and $foo is expanded. A newline inside quotes remains in the argument; an unquoted newline ends the command. The file channel has no shell on it: the bytes go file → gtmux → tmux load-buffer - (a pipe) → the agent’s input box, with at most one trailing newline stripped (every heredoc appends one). Passing both a file and a positional goal is an error. The positional form is fine for short instructions (gtmux spawn --pane %14 "keep going").

Re-running a failed dispatch converges on one worktree, one session, one ledger entry: --worktree reuses a worktree that already serves that branch (a path held by a different branch is still a hard error); spawn adopts its own previous attempt (a ledger entry that owns its session, never got its goal delivered, and still has a live pane) and updates the same ledger row; and a worktree or branch this invocation created is rolled back when a step fails with nothing resumable.

A --worktree branch that does not exist yet starts from the commit checked out where spawn runs (--cwd, else the current directory; the main checkout or another worktree), which need not be the default branch. When that commit is behind the default branch (origin/main as of the last fetch, or a local main without a remote) or carries commits that are not on it, spawn says so on stderr, --json included, with the git rebase --onto that moves the new branch onto the default branch; run it with the agent stopped (it keeps what the branch has gained and refuses on uncommitted changes). To start there in the first place, create the branch first (git branch <branch> origin/main): an existing branch is used as it is.

--title names the window’s purpose as a concise verb-object kebab slug (fix-auth-mw, review-pr-518), which becomes the window and pane name across tmux, the radar, and the app. On success spawn reports the handle <loc> (%pane) · <title>; loc is the live tmux locator session:window.pane, recomputed each read so it stays correct under renumber-windows. Refer to a spawned window by that loc %pane · title so you can jump by number; HQ’s playbook requires a --title on every dispatch and this handle in every report.

A Codex worker starts with --approve-for-me, the mode Codex HQ runs with: work stays in its workspace, and a request to cross the sandbox goes to Codex’s automatic review instead of stopping the task to wait for you. An approval or sandbox mode in the agent command (-a, --ask-for-approval, --sandbox, approval_policy=…) takes precedence.

spawn refuses to run a worker in the HQ home (an explicit --cwd naming it, the inherited cwd when --cwd is absent, or --pane reuse of a pane sitting there): the home’s AGENTS.md is the HQ charter, and a worker launched there would read it and impersonate HQ. Pass --cwd <project dir>.

spawn launches the agent (a fresh detached session by default, --pane <id> to reuse one, or --worktree <branch> for an isolated git worktree) with the configured proxy environment, if any, waits for the agent to come up, then delivers the task via a tmux paste buffer and verifies it landed.

Waiting for the agent is a real gate. The pane is ready once its composer row (the agent’s input box) is drawn, no trust gate or choice menu is up, no boot banner is on screen, and two captures are byte-identical (or the agent’s session-start event has fired). A boot banner (Connecting…, Loading…) holds the gate; a standing notice such as ⚠ N MCP servers need authentication · run /mcp names an action only you can take and never clears, so it does not. On timeout the failure reads ✗ not delivered → <handle>. Evidence:, then agent composer not ready within the ready timeout — blocked by: <the line that said no> and the pane’s bottom region.

Verification is layered. For a hook-equipped agent (Claude Code, Codex, …) the receipt is the agent’s own UserPromptSubmit event, no screen-scraping; otherwise a two-frame screen read locates the input box structurally. A receipt-confirmed landing is final, and before any delivered:false the receipt is checked once more so a confirmation arriving at the deadline is not lost. The JSON result says which layer judged it (judged_by: driver|screen). driver.<agent>.receipt: false or driver.enable: false in ~/.config/gtmux/config.json forces the screen-read path, for isolating event-channel faults.

The paste is bracketed, so a multi-line instruction lands as one draft and the separate Enter submits it once. The only success is a confirmed landing: a swallowed Enter is re-sent with backoff, a fragment paste is retried, and a timeout reports delivered:false with on-screen evidence. A retry never duplicates: a re-paste happens only into a box confirmed empty (the clear key empties one line, so a multi-line draft can survive it), and a paste that merely rendered late is left alone. A queued submission is reported as state:"queued". A re-send interlock refuses an identical payload to the same pane within a window (so a duplicate /compact can’t double-fire); --force overrides it. Pre-flight output reports the configured proxy, machine resource warnings, cached subscription warnings and matching knowledge entries. It is advisory, is skipped with --json, and does not test proxy reachability.

--oneshot dispatches a one-shot, non-interactive worker through the agent’s headless mode (claude -p … --output-format stream-json, codex exec --json). The agent must have an enabled headless capability; otherwise spawn refuses, with no interactive fallback. The runner passes the goal as an argument to the agent, skipping interactive paste and landing verification. Spawn returns without waiting for the worker to finish: its landed result is the dispatch record, not a completion result.

The run still lives in a tmux pane (its JSON stream visible, its radar row present, reap applicable). An error in the parsed outcome or a nonzero process exit records failure; otherwise the runner records completion. Unrecognized output is ignored, so exit zero can record completion even without a recognized result event. Digest uses the same session-record and content-reader rules as other rows; --oneshot does not force sense:"driver". The worker is non-interactive: you can watch its pane but cannot take over mid-run. --headless only suppresses the terminal tab; that spawn is still an interactive session you can attach to and steer.

gtmux send

gtmux send <pane> <text> uses the same land-verification by default (it returns as soon as it confirms); --no-verify opts out, --force overrides the interlock, and --json prints the verified result ({delivered, state, judged_by, evidence}, verified sends only).

An option gtmux send does not have is refused and nothing is sent: a mistyped --body-file used to arrive in the other agent’s box as text. That holds for any word that starts with --, wherever it sits among the text’s words: gtmux send %5 make --dry-run is refused. Put such text after a lone -- (gtmux send %5 -- make --dry-run), or use --message-file.

--attach FILE (repeatable, up to 30 MB each) hands a file to the agent by path, the way the phone does: the file is copied into gtmux’s uploads dir (pruned after 7 days or 200 MB) and its path is added after the message on a line of its own; --json lists the copies as attachments. With an attachment the message may be empty (a screenshot on its own). A message with a file is never typed into an agent that is asking you something: if the radar shows the pane waiting, or a choice menu is on its screen, the send is refused (refused-waiting). It asks again right before the paste and before every Enter, so a question that appears mid-send gets no Enter and no retry; a pane it cannot read is refused too. A plain gtmux send without a file still answers menus. The check narrows the window, it does not close it: a question that pops up in the last instant before Enter can still receive it. It also errs toward refusing. A question you have just answered still has its marker while the approved tool runs, and the radar lets that through only once it has seen the pane change or use CPU across two looks within 6 seconds. The menu bar app’s polling usually provides the first look; a send with nobody polling, or a tool that runs silently, is refused as waiting. Try again a moment later. If the refusal came after the paste, the text may already be in the agent’s input box: look before sending it again. The copy is named by its content, so the same file and message sent twice are the same payload and the interlock refuses the second. The path means something on this Mac only.

gtmux send %5 --message-file note.txt --attach "Screen Shot.png"

The draft guard refuses text it identifies as someone else’s unsubmitted line. A paste appends to the input box, so delivering onto a half-typed line would submit their words with your payload. Every path (the CLI, HQ, --no-verify, and the phone) reads the draft first and refuses a confirmed conflicting draft (state:"refused-draft", nothing written) with the draft quoted back. Unreadable or unrecognised drafts can pass, as below. --force waives it; the phone’s idempotency key does not, since a send from another device has nobody present to undo it.

This draft guard runs only on a pane a known agent drives (a pane running vim, ssh or some other TUI has no composer, and reading one there returns the transcript as a “draft”). It reads the colour capture, so the agent’s own dim suggested-next-command is not mistaken for your typing, and wants the same thing in two frames before refusing. Everything it cannot judge lets the send through:

what it seeswhat it does
no agent in the pane (no composer)sends
the capture fails (tmux hiccup, pane gone)sends
the pane is scrolled (copy-mode)sends
a plain shell, no input boxsends
your own text, the same message being re-sentsends (idempotent)
someone else’s text, seen twicerefuses

It costs at most two reads and one poll interval, and never loops.

A send that ends failed may be retried immediately: the interlock drops the record of an attempt that never landed, so the retry is not answered with refused-duplicate. A queued send keeps its record; the agent already took it. --no-verify and the mobile POST /api/send skip only the confirmation: every text path pastes and then sends Enter as its own key, so an unverified send cannot split a multi-line message either. --key remains a single keystroke. For anything longer than a short line, use --message-file <path|->.

A plain terminal pane (no coding agent in the pane’s process subtree, a bare shell such as bash or zsh in the foreground) is typed into directly: text then Enter, no input-box confirm and no re-send interlock. --json reports such a send as {"delivered":true,"state":"sent"}. Anything not provably a plain shell (vim, ssh, an agent the process scan missed) keeps the agent pipeline.

gtmux tasks

gtmux tasks [--json] is the dispatch / needs-you ledger: every task you spawned with its live status (undelivered / waiting / done / working / gone), needs-you first. --verbose adds archived entries and the attention columns (tier · priority · surfaced · disposition).

undelivered leads the list. The ledger records the delivery verdict, so an entry whose goal never reached the agent reads ✗ undelivered however idle its pane looks. A queued delivery is not undelivered (the agent accepted it, behind the current turn). A gtmux send that lands that same goal in that pane marks the entry delivered; an unrelated line typed into the pane does not.

gtmux tasks --pending is the pending-decision standing view, what is on your plate:

▸ t1kx8p2m9dq3  hq %21                 08-09 14:32  ship v0.48.0 or hold for §4?
▸ t1kx9r4w0aa1  worker-b %8            08-09 09:11  which branch should the migration target?

The leading glyph is ▸ (an entry on this list is awaiting a decision). Ordering is the oldest wait first, then pane, then id. The view reads the ledger only and prints an absolute stamp, so two reads of an unchanged plate are byte-identical.

Entries go on and off the plate with gtmux tasks --await <task_id> and gtmux tasks --resolve <task_id> [disposition] (the disposition records how it left: decided / withdrawn / escalated; omitted, it just clears). Membership is the awaiting-commander disposition, so any other disposition also takes an item off the plate, and archiving an entry closes it out of the view.

gtmux reap

Resource closure failure stops worktree/branch cleanup. Any failed step retains the task ledger for recovery. --json adds resource_closed; closed tmux resources do not prove shared Codex backend exit or conversation deletion. Audit records carry task and settlement, plus conversation/binding/locator IDs only with exact retained live evidence.

gtmux reap <pane|task_id> safely reclaims a finished dispatch. It runs a safety gate first (the worktree must be clean and the branch merged) and only then kills the session, removes the worktree, and deletes the merged branch; when the gate fails it reports exactly what blocks it and touches nothing (--abandon overrides, --keep-branch keeps the branch). A step that fails is named with git’s own reason under ⚠ but these steps failed, and the command exits non-zero. --snooze [--for <dur>] silences a reap suggestion for a dispatch you’re keeping. When a tracked dispatch looks reclaimable, a live HQ gets a » gtmux·reap-suggest … │ gtmux reap <id> wake. Reclaim is always suggest → approve → execute, never automatic.

A suggestion needs more than an idle pane. The ledger holds only what HQ dispatched, so a session you drive yourself never enters it and goes on showing the one old task it was spawned for; half an hour of quiet used to be enough, and gtmux once proposed reclaiming two live flagship sessions in the same minute. A suggestion is now made only when the record shows that every prompt that pane has received since the dispatch was put there by gtmux itself. Where that cannot be shown, none is made: a withheld suggestion costs a pane that lingers, an acted-on one costs a session’s whole context.

Remote access and sharing

gtmux serve

Run the local service used by the phone and browser, on port 8765 by default. Use gtmux serve on a reachable local network, or gtmux tunnel across networks. Run gtmux serve --help for bind, port and credential flags.

gtmux tunnel

Standard uses a stable hosted address; --backend self uses Direct. --service keeps the tunnel running across reboots, --unservice turns it off, and --status shows the installed route.

For most users, open Pair your phone → Standard → Restore connection. There is one action: gtmux keeps the current address when possible and creates a replacement only after confirming the old tunnel is gone. If the address changes, the window shows a new QR and asks you to scan it again. A network or proxy error does not cause a new address to be created. The window rechecks the address after recovery; a running connector alone is not reported as proof that your phone can connect.

The same recovery from a terminal is:

gtmux tunnel --backend cloudflare --service --recover

For an in-place repair that must never create a replacement, the maintenance option remains --service --force:

gtmux tunnel --backend cloudflare --service --force

This reapplies ingress and repairs the proxied CNAME, refreshes the connector token, and reloads the services while keeping the device ID, tunnel, hostname, and pairing address. It requires an existing Standard registration and a control-plane version that acknowledges repair. If the provider fails or an unrelated DNS record occupies that hostname, the command fails before changing local service files.

--force does not repair a blocked Cloudflare edge connection. For repeated TLS handshake failures, check the firewall and proxy route; a proxy node that cannot carry Cloudflare Tunnel traffic can leave the address unreachable despite valid DNS.

gtmux attach: work in a remote session from another machine’s terminal

Where focus jumps to a local tab, attach opens a remote pane in your current terminal (Ghostty / iTerm2 / Terminal) as a raw, interactive passthrough: the local terminal becomes the remote tmux session, over the same gtmux serve surface (a WebSocket, GET /api/attach). It is for the owner and the owner’s paired devices.

# owner — full access with the serve token:
gtmux attach http://<mac>:8765 --token <serve-token> %12

gtmux attach <target>            # omit the pane: auto-attach the only one, else pick
gtmux attach <target> --read-only  # watch only, never send input
gtmux attach <target> --predict    # experimental: hide round-trip lag while typing

A share link cannot open a terminal: the serve refuses it with that reason. The bridge attaches a tmux client to the pane’s whole session, which would show the link’s holder panes the host never shared, and let a link that may type drive tmux into any session on the Mac. A share link opens in a browser, where it reaches exactly the panes the host allowed. A share code given with --code is refused the same way, before it is spent, and nothing is kept for the host.

--predict (experimental, off by default) is predictive local echo, the mosh idea adapted to the WebSocket bridge. Over a slow link every keystroke otherwise waits a full round-trip to echo (about 340 ms on a cross-continent tunnel). With --predict, your own printable typing and backspaces appear immediately, underlined to mark them unconfirmed, and are erased the instant authoritative output arrives; the server screen always wins. The real keystroke is forwarded to the pane unchanged. Nothing is drawn on a fast/LAN link, nothing inside a full-screen TUI (the server tells the client when the pane is on the alternate screen), and any state-changing key (Enter, ESC, arrows, Ctrl-C, Tab) ends the prediction epoch. The client learns the cursor from the server; see docs/design/mosh-predictive-echo-research.md.

  • <target> is a host plus --token (yours, or a paired device’s: full access), a host this terminal already paired with, or a …/#c=<code> pair link from gtmux pair, which pairs this terminal as one of your devices. A share link (…#code=), or its code given with --code, is refused, as above.
  • %N (optional) is the tmux pane id to attach; it selects the session that pane is in. Omit it and gtmux picks from the radar’s rows that have a pane (agents in tmux, plus any plain pane you watch; native sessions have none): with exactly one it attaches at once; with more, on a TTY, it shows a numbered menu with one row per pane, so two agents in one session are two rows (session · agent · status · task; Enter takes the first row, q cancels). Piped or scripted (stdin not a TTY), it prints the list and exits 2. With none, it reports no attachable sessions.
  • Detach with tmux’s own <prefix> d, or Ctrl-] (the local escape hatch).
  • Scope is enforced server-side; --read-only is a convenience, not the security boundary. See docs/design/remote-attach-research.md for the design and trade-offs.

Needs the host reachable: on the LAN directly, or through gtmux tunnel when your network allows its address (the WebSocket rides the same tunnel as the radar). Guest links are set up in the menu bar (per-pane 👁 See / ⌨️ Type + New link) or with gtmux share.

gtmux pair: enroll your own devices (full control)

gtmux pair                  # mint ONE one-time code, printed three ways:
                            #   phone QR · browser https://…/#c=<code> · a one-line
                            #   `gtmux attach '<url>/#c=<code>'` for another terminal
gtmux pair list             # your paired devices (guests live under `gtmux share`)
gtmux pair revoke <id>      # cut one device off, effective immediately

Pair is the owner track of the pair/share model: the enrolled device is you, with full view and input on every session. All three media redeem the same short-lived code (5 min, single use) into the same revocable roster. The terminal medium persists its token in ~/.config/gtmux/remotes.json (0600), so afterwards a bare gtmux attach <host> just works; pair revoke invalidates it instantly. The link carries the tunnel URL when gtmux tunnel is up, else a LAN address. gtmux devices remains as an alias of the roster.

gtmux devices --push / --forget-push: inspect and clean up push tokens

gtmux devices --push                       # roster annotated with each device's push
                                           #   token (✓ env·kinds), plus this Mac's own and paused ones
gtmux devices --forget-push <id|orphans|all>  # drop push tokens (host-only)

Push tokens are bound to the enrolled device that registered them, so gtmux devices revoke <id> already stops that device’s notifications. A token registered with this Mac’s own token is marked as such and is sent to, though it has no device. Two kinds are kept but paused, with nothing sent to them:

  • a token bound to a device that is no longer paired; clear it with --forget-push <that id>;
  • an unattributed token, with no device and not registered with this Mac’s own token. These are registrations from before tokens were bound to devices, and some may have been a guest’s. Clear them with --forget-push orphans.

If a paused token is your phone, it resumes once gtmux on the phone registers again with access this Mac still accepts: with notifications on for the app and for this Mac, opening gtmux or bringing it to the front does it while this Mac is reachable. A phone whose device was revoked has to be paired again first; reopening the app does not bring it back. --push shows all of this. orphans never removes this Mac’s own tokens; all removes every token. Host-only (the local master token); a remote device or guest is refused.

gtmux share: scoped, revocable access for a collaborator

gtmux share new --label Alice --view %1,%2 --type %1 --expires 24h
gtmux share set a1b2c3d4 --type %2            # edit ONE link (omitted flags untouched)
gtmux share link a1b2c3d4 [--json]            # re-show an existing link, both ways (+ QR)
gtmux share on|off                            # consent master switch for ALL guest typing
gtmux share status [--json]                   # per-link scope summaries
gtmux share revoke a1b2c3d4

Share is the collaborator track of the pair/share model: a guest link with least privilege. Each link carries its own scope: which panes the guest may see (--view) and type into (--type ⊆ view), plus an optional expiry (--expires 45m|24h|7d, default never; expired links fail auth like revoked ones). Typing additionally needs the host consent (share on, default off). Everything is enforced server-side; the web page and app only mirror it.

A link minted without --view/--type copies the current global lists (the template). The legacy global forms (share add/remove, share view add/remove/clear) still work but fan out to every existing link; per-link tailoring uses share set. status --json carries each guest’s view_panes/panes/expires_at and never a bare token, plus who has used the link: last_seen, platform (Chrome 141 · macOS), last_ip, all absent until someone has. gtmux share link <id> (or the menu-bar row’s copy button) re-hands an existing link at any time, full-scope callers only.

A share link is one collaborator’s access to this Mac: the panes they may watch, the shorter list they may type into, and an optional expiry. Each link carries its own access, so you can hand out three and revoke one.

The link ends in a short code, and that code is the link:

https://tunnel.example.dev/p35047#code=4F7K-Q9X2

Send it and they click it. Where nothing can be pasted, at a TV browser or a locked-down machine or over the phone, read them the two halves instead:

https://tunnel.example.dev/p35047
4F7K-Q9X2

Both reach the same place. The code lasts exactly as long as the link does, so there is one thing to keep track of and one thing to revoke. Guessing it is bounded at the door: too many wrong codes and the door stops answering for a minute.

In a browser, they open the link, or open the address and type the code into the box on the entry page. The browser keeps the access from then on, so coming back tomorrow just works. They see the panes on the view list, and can type into the shorter list while your consent switch is on (gtmux share on). Owner settings and HQ APIs are closed to them, but typing in an allowed pane can control its shell or agent with that program’s permissions; the pane list is not a system-permission sandbox.

A terminal cannot: gtmux attach <link>, or gtmux attach <host> --code 4F7K-Q9X2 when the link was read out, is refused, because a terminal would reach the whole tmux session, not only these panes. Send them the link to open in a browser.

gtmux share revoke <id> cuts both ends at once: the browser falls back to its entry page on its next request, and a token an older gtmux saved for the link stops working too. An expiry does the same on its own schedule. Your other links keep working.

A browser can lose what it kept, through cleared data, a private window, another browser, or Safari’s rule that clears storage for a site nobody has visited in a week. They reopen the link and they are back in, with nothing needed from you. A terminal keeps its token in ~/.config/gtmux/remotes.json, which stays until you revoke the link or they delete the file.

Links handed out before codes existed still work. They carry the long token in place of a code, and open the same access.

Setup and configuration

Installation and update commands

CommandPurpose
gtmux doctor [--fix [--yes] | --bundle]check this Mac, then set up what is missing
gtmux install [hooks|app]install agent hooks or the menu-bar app
gtmux uninstall [hooks|app]take back out what gtmux installed
gtmux update [--check]update gtmux, the CLI and the menu-bar app
gtmux appopen the menu-bar app

gtmux doctor --progress also shows progress in piped output; --bundle collects diagnostics. See installation for setup.

After a successful CLI update, the newly installed executable runs a read-only doctor --no-prompt --progress check (90-second limit). Findings do not turn a successful installation into a failed update. Menu-bar updates hand this check to the relaunched app: it checks once per version and keeps the last valid report. Open Environment check from the gear menu or Preferences → Diagnostics to review results or retry. Review repairs… opens doctor --fix in a terminal, where each action is explained and confirmed. Check again after completing repairs.

doctor --json returns schema 1, CLI version, checked_at (Unix seconds), grouped rows (status, label, value, note, optional description) and counts. description explains what a known check measures; note retains its technical details. JSON labels are human-readable and are not stable machine identifiers. Status is ok, recommended, blocking or info; exit 1 still contains a complete report when blocking items exist. --json and --no-prompt reject repair/bundle flags. doctor --fix --terminal opens the interactive fixer and cannot be combined with --yes; opening it is not confirmation that repairs succeeded.

Check completion is separate from the four result classes:

StatusMeaning
okThe checked prerequisite is satisfied.
recommendedConfiguration or maintenance can be improved; some features may be limited.
blockingThe condition affects the corresponding feature; the health exit code is 1.
infoOptional or unverified state for reference; excluded from findings.

A completed check explicitly says Check complete even when recommendations remain. Running it again reads the current conditions and does not repair them. The terminal report ends with all blocking and recommended findings, blocking first, including their group, current value and existing guidance. The fix pass uses the same list for remaining findings; a recommendation is not itself a failed repair. Some items need a separate action rather than doctor --fix.

The Mac report also ends with Findings. Its header and footer link directly to that list. Selecting an item locates its original check, highlights it and opens the full details. The information button explains the four classes.

Expanded check rows show the result once, with distinct technical guidance below. Long results expand in full; identical summary text is not repeated in details.

gtmux awake: keep working with the lid closed

gtmux awake on       # asks for your admin password once, then verifies it took effect
gtmux awake          # awake = on (clamshell) · up 2h13m · power battery 74%
gtmux awake off      # asks the guard to restore sleep; normally no password

Closing a MacBook’s lid sleeps the system, which drops the tunnel and freezes every agent mid-turn. gtmux awake on keeps the Mac awake with the lid shut (gtmux server-mode, the old name, still works). Enabling requires administrator authorization at the Mac. Keep gtmux serve running: its heartbeat tells the guard there is still something to serve. Finishing an agent’s task does not end server mode.

Normal off writes an unprivileged request for the guard and waits up to eight seconds for sleep to be restored. It can report that the request is still pending. If the guard is missing, the CLI instead asks for administrator authorization to restore sleep, and reports a failure unless the kernel then reads sleep as enabled. When the kernel’s sleep setting cannot be read at all, the state is unknown: gtmux claims neither on nor off, keeps its record and the request, and does not report a lapse.

A small root-owned guard is installed in the same authorization. Its only power is to give sleep back. These conditions trigger a restore attempt:

triggerwhat it means
you turn it offan unprivileged marker wakes the guard through launchd’s path watcher
charge reaches 20% on batterythe guard checks every 30 seconds; the serve process requests a desktop warning at 30%
the serve heartbeat stopsa heartbeat older than 120 seconds triggers restoration after the boot grace period
a reboot with nobody logging ina five-minute boot grace lets the per-user service resume after login

The guard removes itself only after reading back that sleep is enabled. If restoration fails or cannot be confirmed, it keeps the request and retries on a later run. When the guard ends server mode for any reason other than your own gtmux awake off on this Mac (the menu bar’s switch included), gtmux serve notices on its next slow tick and posts one notification naming the reason, with the time if it noticed late. Your own off is recognised by time: a stand-down the guard completes within ten minutes of it. A restore that takes longer than that may still be announced. (Right after an update, an end older than an hour from before gtmux tracked them is not announced.) That and the 30% warning are best effort: they need the menu-bar app running with notifications enabled. The paired-phone warning and exit notifications remain requirements; server mode does not yet send those phone pushes.

There is no timer-based expiry. It stays on until you turn it off or a guardrail ends it. While on, the menu-bar icon carries a slowly pulsing red dot.

Battery is a supported case: carrying a closed laptop between rooms keeps working. What ends it is remaining charge, not losing the adapter.

The status command reports a disablesleep without gtmux’s ownership stamp, supplies the manual undo command, and leaves it alone. gtmux doctor surfaces the same finding, and stays silent on machines that have never touched the setting.

Where the state is read from:

sourceuse it?
pmset -g / -g custom / -g live❌ never reports disablesleep, in either state
the power-management plist⚠️ lags a write; answers “would it survive a reboot”
ioreg -r -c IOPMrootDomain → SleepDisabled✅ the live, unprivileged truth

gtmux awake --json reports both readings plus owned_by_gtmux, guard, and a platform verdict. On a macOS the project has not verified, on says so; where the mechanism is absent it refuses before asking for a password. guard.healthy means both guard files exist; it does not confirm that launchd is running the guard.

Two boundaries:

  • gtmux serve is a per-user LaunchAgent, so after a reboot it starts only once someone logs in. On a FileVault Mac with nobody there the heartbeat never resumes and sleep is restored, so server mode does not survive an unattended reboot. gtmux will not “fix” that by touching FileVault or auto-login.
  • The underlying setting is undocumented by Apple. The recorded physical check used an M4 Pro on macOS 26.5.2. The CLI’s platform.verified flag groups macOS 26.x together; it is not evidence that every model and OS configuration was tested. Runtime checks refuse on if the setting or its readback is unavailable.

The phone UI shows this state but offers no switch. The owner-only POST /api/awake can request that server mode turn off without a password; its success response confirms the request, not restored sleep. Enabling again requires authorization at the Mac.

gtmux whatsnew: what changed for you

gtmux whatsnew                 # everything newer than the version you're running
gtmux whatsnew --since v0.36.0 # from a specific version
gtmux whatsnew --all           # notes within the latest 30 releases returned by the API

Per release, the lines written for users. gtmux update prints the first few of these after installing; this shows the full notes in range within the latest 30 releases returned by the release API. It does not page through older releases. The source is a user: block in the release’s tag message, which goreleaser copies into the release body. An optional user-zh: twin carries the same notes in Chinese:

After a successful update, the CLI also reminds you to run gtmux doctor to check this Mac’s setup. gtmux update --check only checks for a release and does not run doctor.

git tag -a v0.40.0 -m "v0.40.0 — …

user:
- spawn --title now names the session
- restore returns you to the window you were on

user-zh:
- spawn --title 现在会命名会话
- restore 会把你带回原来的窗口
"

Both gtmux update and gtmux whatsnew print the block matching your language (GTMUX_LANG): zh prefers user-zh:, en prefers user:, and either falls back to the other when a tag carries only one. The blocks may appear in either order; each ends at a blank line, a heading, or the other block’s marker. A release with neither a user: nor a user-zh: block contributes nothing.

gtmux config: the few settings that are not per-run flags

gtmux config agent-proxy [<url>|off]   # proxy applied when gtmux LAUNCHES an agent
gtmux config tab-alert  [on|off]       # mark the terminal TAB of a session that needs you
gtmux config lang       [en|zh|auto]   # machine-level output language (auto = system locale)

Each prints its current value when called with no argument.

lang: one language for every gtmux process

A launchd-started gtmux serve and a hook subprocess have no GTMUX_LANG and none of your shell’s locale, so without a machine-level choice the wake suffixes and desktop notifications they emit could disagree with the language your own terminal shows. gtmux config lang zh makes the choice once for all of them; auto follows the system locale. Per-process GTMUX_LANG and per-invocation --lang still override.

tab-alert: find the right tab without opening nine of them

tab-alert on puts a ● in front of the tab title of any session that has an agent waiting.

  • Only waiting marks; working and idle never do.
  • tmux renders the title and the terminal just displays it, so this works on Ghostty, iTerm2, Warp, Apple Terminal alike. It is a glyph, since a coloured tab is a per-terminal capability.
  • Your title format is kept. Enabling reads your set-titles-string, stores it, and prepends only its own field; off restores exactly what it found. If you have edited the format since, off refuses to overwrite your edit; delete the leading #{@gtmux_alert} yourself in that case.
  • Driven by the agents’ own hook events: a waiting marker lands the instant the agent reports it, and the serve tick reconciles as a backstop. HQ is not in this loop.
  • Also switchable in the menu-bar app’s Preferences → Notifications.

debug: record more while chasing something

gtmux config debug            # what is being recorded now
gtmux config debug on         # every part of gtmux writes debug entries
gtmux config debug serve,hook # just these
gtmux config debug off        # back to the ordinary entries

It lives in config.json rather than in a shell variable because the processes worth turning up are the ones no shell reaches: the launchd serve, the tunnel client, the hook. CLI processes pick it up when they next start. The menu-bar app refreshes its own switch when you change it or it reads log stats. gtmux logs --stats says whether it is on, and the menu bar’s Record extra detail is the same setting.

hqWake: tuning HQ’s wake channel

Hand-edited keys under "hqWake" in ~/.config/gtmux/config.json (all optional; an absent or invalid key keeps its default). What each one governs is explained where the behavior lives: the wake channel, the watermark, and self-rotation:

keydefaultgoverns
done"unattended"done-wake mode: unattended | always | tick
paneMinGapSec120per-pane merge window for done wakes (seconds)
tickMinutes10summary-tick minimum interval
tickBurst5outcome count that fires the tick early
unreadDebounceSec120how long unconsumed events must stand before an unread knock
unreadRepeatSec300unread re-knock interval while the watermark stays put
selfRotateCtx0.75context-fraction breach line (0 disables)
selfRotateHours12session-age breach line (0 disables)
selfRotateTurns300hq-turn-count breach line (0 disables)
selfRotateRepeatSec1800re-knock pacing while the breach stands
selfRotateFloorSec43200longest an unchanged breach may stay silent
selfRotateCheckSec300how often the self-rotate sensor evaluates

tmux integration

gtmux is just a CLI; bind whatever keys you like in tmux.conf. Suggested:

set -g set-titles on
set -g set-titles-string '#S — #W'
bind g run-shell -b "gtmux overview --popup"
bind a display-popup -E -w 80% -h 60% "gtmux agents --watch --popup"
bind J run-shell "gtmux focus --last"

gtmux status: the fleet in your tmux status bar

set -g status-right '#(gtmux status)'

One short, colored run of counts (●2 ✓14) for the tmux status line, so the bar you already have says who needs you without you asking. --plain drops the tmux color escapes for any other status bar (or a shell prompt) that wants the same numbers.

Put the pane ids in your tab titles (optional)

gtmux names a pane by its tmux id (%23) on every surface, and gtmux focus %23 takes that id directly. Two lines make the id visible on the other side too, so a row in the app and a tab in your terminal name the same thing:

set -g automatic-rename-format '#{b:pane_current_path} #{P:#{pane_id} }'
set-hook -g pane-exited 'set-window-option automatic-rename off ; set-window-option automatic-rename on'

The window name then lists every pane in that window (gtmux %23 %24), and since set-titles-string is #S — #W the tab inherits it. The title format is unchanged, so focus’s tab matching is untouched.

  • #{P:…} iterates the window’s panes, so the name does not follow focus (why is in TROUBLESHOOTING).

  • The hook is required: adding a pane re-evaluates the name immediately, closing one does not.

  • Inside a split window, if you want each pane to wear its id on screen as well:

    set -g pane-border-status top
    set -g pane-border-format ' #{pane_id} #{pane_current_command} '

    doctor does not suggest this one: pane-border-status is off by default, and turning it on costs a permanent screen row per pane in every split.

  • gtmux doctor reports this row and --fix offers it. If you already have your own automatic-rename-format, even one that shows the foreground command, --fix appends the ids to it; only tmux’s own default is replaced. gtmux does not rename your windows: rename-window would turn automatic-rename off for that window and overwrite your format for good.

A program prints a hyperlink as an OSC 8 escape, and tmux forwards it only to a terminal that claims the capability. No terminal claims it by default, so the link renders as plain text.

set -as terminal-features ',*:hyperlinks'
  • Needs tmux 3.4+ (terminal-features exists from 3.2, the hyperlinks feature from 3.4); on an older tmux the line is a startup error, so doctor does not offer it below that.
  • Only output printed after it takes effect carries a link.
  • Ghostty and iTerm2 both handle OSC 8. If a link still does not respond, the next suspect is the terminal’s own click modifier (some need ⌘ or ⌥ + click).

Give a plain pane a title worth reading (optional)

An agent writes its own pane title (an agent row reads 整理这周的发布清单和回归结果); a shell writes nothing, so gtmux falls back to the command name and every plain row reads bash. Two shell hooks fix it: the title becomes the command while it runs, and the directory at the prompt.

# bash — in the file your LOGIN shell reads (see below)
if [ -n "$TMUX" ]; then
  trap 'printf "\033]2;%s\007" "$BASH_COMMAND"' DEBUG
  PROMPT_COMMAND='printf "\033]2;%s\007" "${PWD##*/}"'"${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
fi
# zsh — ~/.zshrc
if [ -n "$TMUX" ]; then
  autoload -Uz add-zsh-hook
  gtmux_pane_title_preexec() { print -rn -- $'\e]2;'"$1"$'\a' }
  gtmux_pane_title_precmd()  { print -rn -- $'\e]2;'"${PWD:t}"$'\a' }
  add-zsh-hook preexec gtmux_pane_title_preexec
  add-zsh-hook precmd  gtmux_pane_title_precmd
fi
  • A tmux pane runs a login shell, and a login bash reads .bash_profile / .bash_login / .profile (the first that exists) and never .bashrc, so the bash version goes in one of those. doctor --fix picks the file your shell actually reads, and only ever appends to one that already exists (creating .bash_profile where you keep .profile would shadow it).
  • Use add-zsh-hook; defining a bare preexec() replaces whatever you (or oh-my-zsh) already had.
  • Both are gated on $TMUX: outside tmux this would fight your terminal’s own tab title, which gtmux matches on to jump.
  • gtmux doctor counts how many plain panes have a title that says anything, whatever hook produced it. New shells only; panes already open keep their titles.

Notification hook

⏸ waiting, ✓ latest, and click-to-jump notifications rely on a hook writing state files under ~/.local/share/gtmux/. gtmux ships that hook built in:

gtmux install                       # asks: hooks | app | all
gtmux install hooks                 # Claude, one-time setup (macOS)
gtmux install hooks --agent codex   # or cursor|gemini|copilot|kiro|opencode|kimi
gtmux uninstall [hooks|app|all]     # reverse it (asks when no target)

gtmux install hooks registers gtmux hook in ~/.claude/settings.json for turn, permission, session, compaction and tool events (idempotent; preserves other hooks and backs the file up). gtmux hook is the producer (Claude Code runs it, you don’t). Notification payloads distinguish permission requests from idle prompts; a missing notification type uses the legacy path. The hook also reads prompt/reply content for the event journal and supported conversation state.

Other agents: --agent codex|cursor|gemini|copilot|kiro|opencode|kimi wires that agent’s own hooks file instead. Codex uses its additive hooks system (~/.codex/hooks.json

  • features.hooks), so it coexists with any existing notify (e.g. computer-use) instead of replacing it. opencode has no command-hook file, so gtmux installs a small JS plugin (~/.config/opencode/plugins/gtmux.js) that forwards its events. Kimi Code keeps its hooks as [[hooks]] entries inside your own ~/.kimi-code/config.toml, so gtmux appends one marked block at the end of that file and leaves everything else byte for byte; uninstall removes exactly that block. If the block’s markers don’t pair up (say one was deleted by hand), gtmux changes nothing and names the line to fix. gtmux doctor checks hook configuration for the eight agents it finds; --fix repairs missing/invalid bindings and outdated bridge plugins, backing up files and preserving other hooks. Codex and Kimi interruption ends working without a completion notification. Kiro supports CLI 2.x profiles and V3 global hooks. Restart affected agents after upgrading; review/trust Codex changes in /hooks. See hook contracts and limits.

For the menu-bar app, gtmux doctor and doctor --fix both recognize installs in ~/Applications and /Applications; an existing system-level app is not offered for reinstall.

Doctor needs access to the running tmux server to check its settings. If it cannot read them, it reports the connection error instead of treating the options as unset; --fix stops before changing anything. Start tmux or retry from a terminal with access to its socket. If a config is saved but cannot be applied live, the failed command is reported and the fix exits non-zero. The final list shows current values and guidance: remaining checks can include skipped configuration steps and HQ work that needs to be completed separately.

Notifications are delivered by the menu-bar app; no terminal-notifier is needed. The hook queues a request under ~/.local/share/gtmux/notify/ and Gtmux.app posts a native banner (shown as Gtmux, with the agent icon and a Jump action; finished is calm and silent, needs your input sounds). Clicking it lands you on the exact pane. Grant “Allow Notifications” on first run and keep the app running to receive them.

Backing up HQ’s records

HQ’s records (the whole HQ home) are the one thing gtmux holds that is not reproducible: the situation board, the knowledge base, and a LOCAL.md that is seeded once and never overwritten (losing it does not self-heal).

gtmux hq --export ~/gtmux-hq.tar.gz   # the whole records folder as one LOCKED file (asks for a passphrase → .tar.gz.age)
gtmux hq --import ~/gtmux-hq.tar.gz.age   # put one back (asks for the passphrase)
gtmux hq --export ~/gtmux-hq.tar.gz --plain   # the unlocked form

gtmux hq --records [--json] (--memory is the old spelling) says how big the records are, their backup indicators and the last export made. That export record does not prove the file left this disk. The menu-bar reader shows the same line, from the same command, so the two surfaces cannot drift about the same number.

The export is an ordinary tar.gz locked with a passphrase in the age format (since 1.0.21), so any age tool opens it. The lock goes on the archive; the knowledge base on this Mac stays as it is. The passphrase is typed twice on the terminal, unechoed, eight characters at least; a script sets GTMUX_HQ_PASSPHRASE, an app pipes it as the first line of stdin with --passphrase-stdin, never on the command line, where ps would show it. Lose the passphrase and the file stays shut: gtmux keeps no copy. --plain writes the unlocked tar.gz. --import tells the two apart by the file’s header and asks for the passphrase when it needs one; a wrong passphrase changes nothing. --records adds when the last export was made and whether it was locked; the menu-bar reader asks for the passphrase in a sheet that can keep it in the Mac’s keychain.

--import never overwrites in place: existing records are moved aside to a sibling folder named hq.replaced-…, and the path is printed.

gtmux serve snapshots daily to ~/.local/share/gtmux/hq-snapshots/, keeping 14, and only writes when the records actually changed. Snapshots live beside the state, outside the HQ home.

Snapshots cover accidents, not the disk. They sit on the same one. The HQ records row in gtmux doctor says how much is at risk, how long it took to accumulate, and its backup indicators. An export on the same disk still needs to be copied elsewhere.

Permissions

gtmux asks for only what it needs:

  • Automation (control your terminal: Ghostty / iTerm2 / cmux / Warp), required for focus / restore / new and notification click-to-jump. macOS prompts the first time gtmux drives the terminal via AppleScript; click Allow.
  • Notifications, so the menu-bar app can post agent banners. Allow on first run.
  • Launch at login (optional), only if you enable it in Preferences.

It does not need these; if macOS prompts, you can safely Deny with no loss of function:

  • App Management (“modify apps on your Mac”). gtmux never modifies other apps; its code only ever touches its own bundle (on update/uninstall). The prompt can appear when macOS attributes another app’s self-update to gtmux’s long-running background process via its responsible-process chain. Denying changes nothing for gtmux.
  • Files & Folders (Downloads / Desktop / Documents). gtmux doesn’t read these. The prompt can appear when restore recreates a tmux session whose working directory lives in one of them; that’s tmux (run by gtmux) opening the folder. Safe to deny.
Events and automation

gtmux events: the session event stream (subscription)

22:50:40  working          api:0.0        Claude Code (%7)
22:51:02  waiting·permission  api:0.0     Claude Code (%7)
22:53:19  idle             web:1.0        Codex (%11)

The hook appends every session’s lifecycle event (start / finish / waiting / background) — except a finished tool that ended no wait, which is telemetry, not news — to a rotated log (~/.local/share/gtmux/events.jsonl, active 20 MB + 1 rotated ≈ 40 MB ceiling, eventsCapMB config; 0 disables). gtmux events prints the last hour; --since 10m|2h a window; --follow prints that last hour (or the --since window; --since 0 for new events only) and then streams each new event, rotation-aware. --since-seq N is a one-shot read of retained records with sequence greater than N, oldest first, combinable with --severity/--json. It has no upper cursor: events arriving after the wake can be included too. With --since-seq, --since and --follow do not change that one-shot read. Apps use /api/events for radar-change signals and alerts; it does not replay this lifecycle journal.

An unfiltered --since-seq read from the HQ home or one of its subdirectories advances the watermark through the retained delta when there is no sequence gap and the starting cursor does not skip past the existing watermark (see The watermark). --ack N writes the watermark back explicitly, for when the stream was reconciled some other way, e.g. a full gtmux digest. Both recognise the HQ home and its subdirectories (notes/, knowledge/); a read elsewhere does not advance HQ’s watermark. --ack N only advances it, never rewinds it or moves beyond the journal’s assigned sequence. A gap warning keeps the watermark unchanged: reconcile first, then acknowledge the position you reconciled.

A delta pull from the HQ root omits the records that never counted (HQ’s own pane’s lines, pane-less blinks, and gtmux’s gtmux:audit:* trail) and says on stderr how many it withheld. --all restores the raw view; both unfiltered forms consume. The subdirectory read counts as consumption but currently shows the raw view. Reads outside the HQ home also show the raw view and do not consume.

--severity <tier> filters to that tier and above. The tiers rank urgency, so they are three different reads: the unfiltered --since-seq delta is what you reconcile with; --severity notable is the fleet-change stream (an instruction reaching a session, origin:"instruction", plus turn-ends and lifecycle); --severity important is the escalation subset (blocked · asking · crashed), to triage first.

--acts keeps only the supervision’s own acts (dispatches, reaps, knowledge writes, rotations, self-checks, distillations) and drops the wake plumbing (wake-delivered / wake-dropped). It is the same partition the phone’s “HQ’s work” section reads over GET /api/hq/events?acts=1 and the menu bar’s “HQ did” row counts; “what did HQ do today” is gtmux events --since 24h --acts. Like every filtered read, it never counts as consumption, with --all or without. With --since-seq it still prints the acts, which a plain delta read hides as gtmux’s audit trail.

The stream also carries gtmux’s own control records, the periodic maintenance triggers it raises for HQ, rendered as [CONTROL <event>] with their reason:

09:57:16  [CONTROL gtmux:self-check]  due (daily) — review feed/ledger/memory health…
04:33:49  [CONTROL gtmux:distill]     due (weekly) — distil the period into the KB…

These records mean the pass was requested. After finishing, HQ runs gtmux hq --maintenance-done distill or gtmux hq --maintenance-done self-check from its home. A distill receipt requires the capture queue to be empty. The completion is a separate gtmux:maintenance-completed event with kind and request_at, plus an act.hq.maintenance diagnostic entry. gtmux doctor and gtmux capture --list show an unacknowledged request as pending; a trigger alone no longer reads as completed.

Source: docs/cli.md at v1.0.114 · Edit on GitHub

先看常用命令,完整语法和细节按用途展开。当前安装版本的完整命令列表用 gtmux 查看; 单条命令的参数用 gtmux <命令> --help 查看。

常用命令

我想做什么命令
查看所有 agent 并跳过去gtmux agents --watch
打开指定 panegtmux focus %7
让 HQ 盯全局gtmux hq
检查并修复安装配置gtmux doctor --fix
让远程访问重启后继续运行gtmux tunnel --service
连接自己的手机或浏览器gtmux pair
给协作者一天的只读访问gtmux share new --view %7 --expires 24h

把 %7 换成 gtmux panes 列出的 pane。配对前需开启并能访问远程连接;第一次连接可先看 手机与浏览器指南。

完整参考

查看 agent 和 Mac

gtmux agents

列出你 tmux pane 里在跑的 coding agent,按紧急程度排序。

gtmux agent · 7 agent · 1 等输入 · 2 运行中 · 3 空闲 · 1 只有 shell

‖ 等输入   Claude Code  api:0.0                permission to run tests %7
⠿ 运行中   Claude Code  hq:0.0                 api is waiting on you · rest normal %1
⠿ 运行中   Claude Code  web:0.0                refactor auth middleware %11
✓ 空闲     Claude Code  app:0.0                wire up the dashboard %9
✓ 空闲     Codex        worker:0.0             add retry backoff %8  ✓ 最近完成
✓ 空闲     Gemini       docs:0.0               draft the API reference %3
● 运行中   Claude Code  infra:0.0              — %5

跳转:gtmux focus <pane>   (例如 gtmux focus %7)

每行是状态 · agent · 位置 · 任务 · pane id。

  • ‖ waiting(红):干到一半,卡在你这儿等一个授权或批准;永远排最上面,也是这屏上唯一 表示「现在就得动」的颜色。
  • ⠿ working(青):在忙,别打扰。
  • ✓ idle(绿):这一回合结束了,你想动的时候再动,不急。
  • ● running(灰):认出了 agent 进程,但没有确定的回合状态。 终端表格里,被关注的普通 pane 也显示在这里,并计入 running 的数量。

这几个记号特意选的是文本呈现的字符。被它们换掉的 ⏸ 和 ✳ 带 emoji 呈现:终端可以用彩色 emoji 字体去画它们,那样你给的颜色会被忽略,红色只落在「waiting」这个词上,落不到旁边 那个记号上。

  • ⚠ errored(琥珀色):一个空闲会话,但结束在 API 或工具报错上(比如 Unable to connect to API),没有干净地跑完。它仍然算空闲(该你动),行上带错误摘要。 --json 里是 error: true 加 error_text。

gtmux agents --watch 是自动刷新的实时看板(用 bubbletea 做的):约 1.5 秒一轮, ↑/↓ 选择,Enter 跳到那个 pane,r 刷新,q 退出。它按「等你 · 运行中 · 空闲 · 只有 shell」 分组,带一列「有多久」,最后一行是额度:最紧的三个窗口,用满的那个标琥珀色。终端变窄时 列会依次让位(先是 agent 名字,再是任务),所以任何一行都不会折。--json 输出同样的数据, 给脚本和菜单栏 app 用。

它是怎么认出来的(不只支持 Claude)

  • 状态取自 agent 自己写的 pane 标题。开头是盲文旋转符(⠋⠙⠹…,大多数 agent TUI 都在转这个)就是 working;Claude Code 屏幕上那个 ✳ 是 idle。
  • 是哪个 agent,靠前台命令匹配(claude、codex、gemini、cursor、opencode …), 或者靠标题里的名字。
  • 用 ~/.config/gtmux/agents.json 扩展或覆盖:一个 {"name","commands","idleGlyph"} 的 JSON 数组,你写的优先于内置。
  • 只有 agent 进程真的在跑,那个 pane 才会被列出来。普通 shell 顶着残留的 agent 标题 (比如 resurrect 恢复出来但从没重新拉起的会话)不算。
  • 跑在 tmux 之外的 agent(终端里裸跑的 codex/claude)通过同一个 hook 被只读感知, 列在「不在 tmux」分区里,source:"native"。它们没有 pane,不能跳也不能回; 空闲、支持按 id 恢复且磁盘上有日志的对话,可以用 gtmux adopt <session_id> 转进 tmux; ChatGPT 桌面版的 Codex 对话留在原 App。Codex 若保存了会话标题, 这里会显示该标题;否则仍显示项目名或终端名。即使结束 hook 漏报,Codex 的 会话日志确认任务完成后,状态也会退出「工作中」。其他 agent 暂沿用标题回退, 等确认可靠的标题来源后再接入。

‖ waiting 和 latest 通常来自通知 hook写的状态文件。 还有两个狭窄的屏幕回退:Codex 当前的审批菜单,以及已记录的派工卡在启动门或留着未送达草稿。 旋转标题配 shell 时,进程树里必须还有活着的 agent;sh -c 命令串里写着 agent 名字不算证据。

gtmux panes

gtmux panes 列出每一个 tmux pane,有没有 coding agent 都列。它是 pane 浏览器背后那个 只读的数据源:一棵 session → window → pane 的树,或者用 --json 拿结构化数组。 每个 pane 带一个 tier:"agent"(coding agent 的 pane,判定方式和雷达一致)或 "plain"(shell、编辑器、dev server、日志,其余都算),另外还有位置、cwd、当前命令、 标题、是否活动、是否在 copy-mode。

gtmux panes            # session → window → pane tree; ▸ marks agent panes
gtmux panes --json     # [{pane_id, loc, session, window, pane, cwd, command, title, active, in_mode, tier, agent}]
gtmux panes watch %N   # opt a PLAIN pane onto the radar as a watched row
gtmux panes unwatch %N # remove it
gtmux panes --watched  # list watched pane ids

gtmux agents --json 是 agent 雷达,也包含感知到的 native 会话和明确 watch 的普通 pane; panes 是 pane 浏览器用的 tmux 全集。gtmux focus、send、attach 接受任何 pane id,所以 tier:"plain" 的 pane 也是一等的 focus、输入和 attach 目标,只是拿不到那些只对 agent 成立的智能(digest、1/2/3 批准、派活、HQ)。

分档能力对照:

档在雷达上查看/抓屏focus/跳转输入/sendattach智能(digest / 1·2·3 / 派活 / HQ)
agent(tmux)自动✓✓✓✓✓
普通 tmux pane手动挂(panes watch)✓✓✓✓—
感知到的非 tmux agent「不在 tmux」————只读 digest / usage

native 行没有 pane 查看器,也不能抓屏。它的标题和从日志得到的 digest goal/last 是摘要,不是对话查看器。

普通 pane 只有你用 gtmux panes watch %N 主动挂上去才会出现在 agent 雷达上, 作为一条独立的关注行(没有 agent 状态),pane 关掉就自动摘掉。访客的分享范围 在任何 pane 上同样把关可见与可输入。

gtmux usage:token 监看

额度   已用多少,以及窗口什么时候回来
  claude 5 小时              22% ██░░░░░░░░░░   1h后回来  Jul 13 at 1:29am
  claude 本周(全部模型)    74% ████████░░░░   3h后回来  Jul 17 at 10:59pm

对话  8                                        输出    ctx   速率
  每段对话自它开始以来
  ⠿ api:0.0                                    2.1M    85%   7k/m   ⚠ ctx 85%
  ⠿ web:0.0                                    830k    60%  391/m
    … 另外 5 段空闲对话,合计 50k

合计   这台 Mac 上的全部 agent
  今天                      2.8M
  本周                     16.2M   claude 15.1M · codex 1.1M
  自 6月12日                233M   最多的一天 9.4M · 连续 23 天,最长 31 天

额度排在最前:它是本地数不出来的那个数,也是决定你还能不能接着干的那个。对话列表只留头部, 尾巴收成一行;每行重复的列名收进一行抬头,抬头里还写明每列是哪段时间。一段对话自己的总量 可以比整周的还大,因为这段对话比这一周还老,没有那句话,这两个数字就是互相打架的。

这里以前有三样东西都叫「会话」。tmux 的 session 是 overview 数的、restore 接回来的那个; 这张表列的是 agent 的一段对话;Claude 那个滚动五小时的额度,现在按时长叫 claude 5 小时。 --json 里仍然是 agent 自己写的那个标签。

今天 和 本周 两行加总 gtmux 能读取的 agent 日志中的输出 token,每条消息按本地日期 记到它发生的那一天。「本周」指含今天在内的最近七个本地日期,不是自然周。账本扫描最近 八天内修改过的日志,保留 366 天的逐日总量;不会补读一整年未再修改的旧日志。 --json 的 history 带最近七天的输出和非缓存输入总量,history.activity 带账本保留 的历史(每个有输出的日子、自首个保留日期起的累计、峰值、连续天数)。自 … 最多的一天 … 一行概括这段历史;gtmux usage --activity 把它画成 手机和 Mac 阅读器上那张日历热力格(周在横向,周一到周日在纵向,GitHub 那五档绿; 终端多宽就画多少周,认 COLUMNS):

Token activity   last 26 weeks
all 70.8M · peak 5.3M · streak 14d (best 25d)

      Apr     May       Jun       Jul       Aug       Sep
Mo  · · · · · · · · · · · · · · · · · · ░ ░ ░ ▓ ░ · █ ▓
    · · · · · · · · · · · · · · · · · · ░ ░ ░ ▒ ▒ · ▒ ░
…
  Less · ░ ▒ ▓ █ More

按会话的 token 统计从 agent 自己的日志解析,不调用模型:累计输出和非缓存输入、最后一次 用量记录的上下文占用,以及近期消耗速率。上下文和速率读取日志末尾 1 MiB;速率取其中 最近十分钟内的记录,分母最少按一分钟计。上下文窗口优先用配置里的 window 覆盖值, 其次用日志明确报告的值,最后按本机观察到的该模型上下文大小推断。 分层阈值按 agent 类型写在 ~/.config/gtmux/usage.json:

{"claude": {"ctxWarn": 0.8, "sessionOutWarn": 20000000,
            "typeRatePerMinWarn": 30000},
 "horizonMin": 30}

评估器按观察到的速率外推上下文和会话消耗(current + rate × horizon,例如 ctx→80% in ~9m),这是估计,不是确定的截止时间。会话超过 sessionOutWarn 后, 只有近期速率仍为正才继续告警;停止产出后,消耗告警会随速率归零而消失。 typeRatePerMinWarn 比较该类型的合计速率和阈值,结果写在类型汇总里。 会话告警以 usage_warn 出现在雷达/digest 行和 gtmux usage 中。hook 在生命周期事件 发生时更新告警,长时间静默生成要等下一个事件。新出现的告警层可以向活着的 HQ 发出 » gtmux·usage·warn …,但同一 pane 的所有层共用至少 30 分钟的间隔,短暂解除告警 也不重置间隔;hqNudge:false 关闭这条唤醒。GET /api/usage 与 --json 使用相同的 JSON 结构,需要 owner 权限。

Claude 记的是每条消息花了多少,所以总量是累加出来的。Codex 每个回合记一次会话的运行 总量,所以总量就是最后那次读数;它还直接写出 model_context_window(配置覆盖值仍然 优先)。当前的整队报告会跳过找不到会话 ID 或日志的对话;日志存在但没有解析出用量时, 会保留这一行,用量数值为零。

agent 启动代理:GTMUX_AGENT_PROXY 优先于 ~/.config/gtmux/config.json 中的 agentProxy。用 gtmux config agent-proxy <url> 指定代理地址,或设为 off,让 gtmux 不再添加代理前缀。gtmux 不识别网络,也不探测本地代理端口;没有设置就不添加。 命令本身已写了 HTTP_PROXY 或 HTTPS_PROXY 时,保留它自己的设置。这段前缀逻辑 不会清除进程继承的环境变量。

gtmux resource:本机资源监看

disk 40GB free · mem 38% free (warn) · load 0.64×14 cores · power 74% (battery 2:13)   ⚠ disk getting low · 40GB free
per-agent (RSS · CPU):
  %26    252MB · 9.2%
reclaim candidates (orphans no live agent owns):
  pid 3015  100MB · 0.0%  iOS Simulator runtime (12 procs) [simulator]
    ↳ leftover iOS Simulator runtime — `xcrun simctl shutdown all`

磁盘(df)、内存(memory_pressure -Q 的空闲百分比,加上内核 kern.memorystatus_vm_pressure_level 的 normal/warn/critical 档)、CPU(loadavg÷核数), 以及电源/电池(pmset -g batt:电量 % · 接电还是放电 · 剩余时间;present:false 时 CLI 不显示电池行,没有电池的 Mac 就是这样;命令执行失败、回答里没有电源来源那一行、或者有电池行却读不出百分比时, JSON 才省略整个电池对象)。低电量只在放电时 才计入告警和档位,接着电时不计。按 agent 的 RSS/CPU 靠走 每个 pane 的进程树得到,可回收候选是没有活着的 pane 认领的重进程,带 pid 和回收办法 (残留的 iOS 模拟器运行时聚合成一条,dev server 各自单列)。阈值在 ~/.config/gtmux/config.json 的 resource 对象里(diskAmberGB 50 / diskRedGB 15 / loadAmber 1.0 / loadRed 1.5 / orphanRssMB 300 / batteryAmberPct 20 / batteryRedPct 10)。 候选要先过一道:这个东西死了,谁会跟着死?凡是在跑的活计赖以站立的东西 —— 所有会话都住在 里面的那个 tmux 服务、gtmux 自己常驻的进程、agent 进程本身 —— 不管多大都不会被提出来。 这条告警曾经写过 disk getting low · 29GB free — maybe reclaimable: tmux,照做不但腾不出 空间,还会一次性停掉机器上所有在跑的活。这类进程会进候选不是偶然:清单找的是「大的、没有 任务认领的」,而地基恰恰长期驻留、占用可观,又正因为它是地基,所以没有哪个任务会认领它。

可回收候选只跟着它救得了的那种告警走。候选是进程,它的体积是内存:这条建议只跟着内存和负载 的告警出现,并写明它占的是多少、哪一种资源;磁盘和电量的告警不带它。杀掉一个 902MB 的进程还不回 一个字节磁盘,而磁盘告急时给出这条建议,前后被照做过两次才有人发现(#1109)。

GET /api/usage 的 resource.agents 按 pane ID 给出 rss_mb 和 cpu,与 resource.machine、resource.orphans 并列;这些不是 token 用量会话行里的字段。 serve 的节拍会给 HQ 发 resource·warn 提醒 (每次越线一次);gtmux hq/new 在红档时先警告再加负载。

告警有三重阻尼,卡在阈值上的数值不会反复告警(读数本身保持原始):

键默认作用
diskHysteresisGB2高出入口线多少 GB 才解除磁盘档(<15 GB 进红,≥17 才解除)
loadHysteresis0.15load÷核数低于入口线多少才解除负载档(≥1.0 进琥珀,低于 0.85 才解除)
batteryHysteresisPct3高出入口线几个百分点才解除电池档(<20% 进琥珀,≥23% 才解除)
confirmSamples3连续几次采样一致才相信这次档位变化
minRestateMinutes30同一档再次告警前的安静期;升到更糟的档不受此限,立刻告警

磁盘剩 0 GB、放电时电量 0%,都和其他低于红线的读数一样算红档;df 或 pmset 没有应答, 则算没有读数。排队等着送进 HQ 的告警,送出前会再读一次:恢复正常就不送;有所缓解(红降到 琥珀)就按现在的情况说;有读数没取到时照原样送出。

gtmux limits:订阅窗口的真实剩余

已用多少,以及窗口什么时候回来
  claude 5 小时              22% ██░░░░░░░░░░   1h后回来  Jul 13 at 1:29am
  claude 本周(全部模型)    74% ████████░░░░   3h后回来  Jul 17 at 10:59pm
  claude 本周(Fable)      100% ████████████   3h后回来  Jul 17 at 10:59pm
  codex 本周                  0% ░░░░░░░░░░░░   2d后回来  Jul 20 at 10:02am

  claude 本周(Fable)用完了,Jul 17 at 10:59pm 才回来。Claude Code 照常还能用,
  那个窗口用了 74%。
刚读的

每个窗口一根条。收尾会指出已满的窗口,以及来源给出的重置时间;没有满的窗口时, 也会提示接近告警线的周窗口。

这些数字来自配置的命令输出或 agent 的日志,不是用本地 token 总量推算套餐用量:

  • Claude 走 limitsCommand,默认是 claude -p "/usage";这个命令需要在你安装的 agent 版本和账号下能正常运行。
  • Codex 直接读本地 rollout 的 rate_limits,不启动 agent。每次按修改时间选最新的 八个文件,在各文件末尾 1 MiB 中查找读数;命令关掉或缓存仍新鲜时也会读。

日志这条路有两条规矩。窗口按时长命名,不按它在数据里的位置(Codex 的 primary 字段里 既出现过周窗口也出现过 5 小时窗口)。重置时间已经过去的窗口会被丢掉,因为日志只新到 最后一个回合为止。

Codex 没有有效窗口时,如果 gtmux 在修改时间距今不足七天的 rollout 里找到了窗口读数, 就会注明缺口:gtmux limits 点名 Codex 并说明窗口已经过去,gtmux usage 的「额度」区 显示 codex 读不到额度。找不到这样的近期读数就不加提示;这不是在查询账号是否有效。

gtmux limits 列出每个窗口。gtmux usage 有会话用量可显示时,开头的「额度」区也列出 每个窗口;没有会话用量时,普通 usage 只显示暂无数据,单独看套餐请用 limits。 手机的用量页同样按套餐列出窗口;紧凑摘要可能每个套餐只取一个。告警规则不同: 周窗口的阈值告警不算 5 小时窗口,但任何窗口到 100% 都会带上 full 档。

每个窗口都写明属于谁的套餐,第一个 agent 的也不例外:claude 5 小时、 codex week,绝不会出现光秃秃的一个窗口名。spawn 的飞行前检查打印的就是这条告警, 说的是这活真正要计费的套餐。Claude 这条路要起进程,所以结果会缓存,TTL 15 分钟, 有窗口接近上限时缩短到 5 分钟;--refresh 强制刷一次。配置在 ~/.config/gtmux/usage.json:

{"limitsCommand": "claude -p /usage", "limitsTTLMin": 15,
 "limitsTTLNearMin": 5, "limitsNearPct": 70, "limitsWarnPct": 85,
 "limitsTimeoutSec": 60}

网络需要的话,在 limitsCommand 前面带环境变量前缀("HTTPS_PROXY=… claude -p /usage"), 把 limitsCommand 设成 "" 只会停止命令刷新,已有的成功结果保留,Codex 的本地窗口仍会读取。 跑超过 limitsTimeoutSec 会被杀掉。命令在一个专用的空目录里运行 (~/.local/share/gtmux/probe):它启动的 agent 会话会翻看所在的目录,以前从 gtmux 所在的 / 起跑,就翻到了照片、音乐、文稿,macOS 于是以 gtmux 的名义向你要权限。这个目录建不出来时命令 根本不跑,这一次按失败计。serve、菜单栏 App 和 CLI 同一时间只会有一个在刷新:另一个调用方 先拿缓存,--refresh 则等正在跑的那次结束、直接用它的结果(失败也算);没有刷新在跑时, --refresh 会立刻跑一次,不管是否在退避中。这把锁根本拿不到时不刷新,只给缓存。失败的一次不会被当成新鲜数据 缓存,已有的额度数字留着不清空,命令会退避(1、2、5 分钟,之后按 TTL),不会被每个 调用方各刷一遍。周窗口到达或超过 limitsWarnPct 会标成琥珀,并唤醒活着的 HQ 一次 (» gtmux·limits·warn …)。limits 这一块也随 gtmux usage 和 GET /api/usage 一起给出。

gtmux logs:gtmux 看到了什么、做了什么

所有 gtmux 进程都写进同一个本地日志库,思路和 macOS 的系统日志一样:serve、隧道客户端、 hook、每条命令、菜单栏。记录分两类。诊断记下 gtmux 看到了什么;操作记下它对这台 Mac 做了 什么、谁发起的、作用在什么上、结果如何(ok、refused 加原因、failed 加错误)。发起者 是 user(你敲的命令)、hq(HQ 跑的)、agent:%7(%7 里的 agent 跑的)、menubar、 哪台手机或哪个浏览器(phone:3f9c20e1)、分享链接(guest:…),或者 system,即 serve 和 hook 自己做的事。

做分析时,gtmux logs --json 每行是一条 JSON,包含 ts、level、component、 kind、event、msg 和标量 attrs;操作另有 actor、target、outcome。 会话事件在另一份有序日志里:gtmux events --all --json 的每条记录都有 seq。 知识变更保存在 HQ 的追加式台账中,可用 gtmux knowledge list --json 查阅。 维护完成事件用 kind 和 request_at 对上请求,诊断操作也带 request_at。 HQ 审计操作的事件与诊断记录共用 op_id;知识变更在台账中也带同一个 ID。旧记录和 普通 agent 生命周期事件没有这个字段。写入失败会在 stderr 告警,不打印记录正文; 同一存储一分钟内最多提示一次。

09:36:05 serve   serve.start  serve started · backend=direct port=8765
09:41:12 serve   act.send  phone:3f9c20e1 → %7 ok · bytes=42 via=tunnel
09:44:02 serve   warn  act.pair  anonymous refused · a pairing code was not accepted · reason=expired via=tunnel
gtmux logs                                   # 最近一小时
gtmux logs --since 1d --acts --actor phone   # 今天手机做过的所有事
gtmux logs --event 'act.pair' --since 2h     # 每一次配对,以及被拒的原因
gtmux logs --level warn --since 3d           # 警告和报错,被拒的操作也在内
gtmux logs --follow                          # 持续打印新记录
gtmux logs --json --since 10m                # 原样输出,给脚本和 agent 用
gtmux logs --since 1d --stats                # 存了多少,今天出了多少问题

--stats 不打印条目,只回答日志库本身:多大、最早哪天、保留多久,以及这段时间里有多少 条是警告或报错。加 --json 就是一个对象,菜单栏的诊断那一节读的就是它。

配对被拒会写明三种原因之一:expired(码的 5 分钟过了)、used(码只能用一次)、 unknown(这个 serve 没发过这个码,重启前发的码就是这样)。

每种操作都有固定的事件名,--event 按它匹配。下面是全部事件,以及会记录它的命令 (serve 指 serve 替手机、浏览器、分享链接或命令行做的事,hook 指 agent 的 hook,app 指菜单栏 app):

act.adopt               adopt
act.app.launch          app
act.attach              attach, serve
act.awake.off           awake
act.awake.on            awake
act.capture             capture
act.cleanup             doctor, serve
act.config.set          config, quiet
act.doctor.bundle       doctor
act.doctor.fix          doctor
act.focus               focus, serve
act.session.follow      follow, serve
act.hq.brief            hq
act.hq.export           hq
act.hq.import           hq
act.hq.migrate          hq
act.hq.maintenance      hq
act.hq.rotate           hq
act.hq.start            hq
act.install.app         install
act.install.hooks       install
act.advice              advice
act.knowledge           knowledge, serve
act.knowledge.sync      knowledge, doctor
act.mint                pair, serve
act.narrow              serve
act.new                 new
act.notify              hook
act.notify.post         app
act.notify.suppressed   hook
act.device.rename       serve
act.pair                serve
act.push.forget         devices, serve
act.push.register       serve
act.reap                reap
act.reap.snooze         reap
act.relay               relay
act.restore             restore
act.resume              restore
act.revoke              pair, devices, share, serve
act.screenshot.capture  app
act.screenshot.freeze   app
act.screenshot.send     app
act.send                send, serve
act.share.config        share, serve
act.share.create        share, serve
act.share.set           share, serve
act.spawn               spawn
act.tunnel.off          tunnel
act.tunnel.on           tunnel
act.tunnel.move         tunnel
act.tunnel.redeem       tunnel
act.uninstall.app       uninstall
act.uninstall.hooks     uninstall
act.unwatch             panes
act.update              update
act.upload              serve
act.wake.delivered      serve, hook
act.wake.dropped        serve, hook
act.wait.suppressed     hook
act.watch               panes

restore 也一直往这里写它的判断过程:选了哪份存档、每个 pane 接回了哪段对话 (gtmux logs --component restore --since 1d)。

记录只用英文,不包含消息正文,一次发送只记长度和一个短哈希。token、配对码和 Authorization 的值在写入时就被替换掉。日志库在 ~/.local/share/gtmux/logs/,每天一个 文件,只有你能读。保留 30 天或 100MB,先到哪个算哪个(~/.config/gtmux/config.json 里的 logs.retainDays 和 logs.maxMB)。每天第一条记录由哪个进程写,就由它顺手删掉过期的, 所以不开 serve 也不会越积越多。某天超过 20MB 会另起一个文件,并用一条 log.runaway 点名是谁写满的。GTMUX_DEBUG=serve,tunnel(或 all)让这一次运行多记 debug; config.json 里写 "debug": "hook" 则对所有进程生效,launchd 拉起的也算。守护进程在能写 日志之前打印的东西(比如崩溃信息)进 logs/<组件>.stderr,由 serve 的定期清理控制大小。

菜单栏里同样的三件事不用开终端。偏好设置的「诊断」一节会说日志库有多大、今天有几条出了 问题;打开是一个列表,最近三天、新的在上面,可以切到只看出问题的;打包… 跑的就是 下面那条 bundle 命令,跑完会说文件落在哪;多记一些细节就是 gtmux config debug(各个 CLI 进程下次启动才生效;app 在切换设置或读取日志统计时更新自己的开关,追完了记得关掉)。

gtmux doctor 有「日志」一节:先检查各存储能否写入,再看日志库的大小和最早的日期、一周内有没有组件刷屏、一天内的 报错、gtmux 存的文件有没有被这台 Mac 上别的账号读到的可能、其他数据有没有超出上限。 gtmux doctor --fix 会执行清理并收紧权限。这里的内容不会上传到任何地方。

要报告问题,用 gtmux doctor --bundle 打一个文件:日志库、状态文件、每份 launchd 输出的 最后 256KB、文字版的 doctor 报告,以及 gtmux、app、macOS、tmux 的版本。打完会列出装了 哪些东西。gtmux 保存的每个 token 和配对码在打包时会再替换一遍,没经过日志库的 launchd 输出也一样。事件流默认不放,因为里面有你的提示词开头;要带上就加 --with-events。文件 只有你本人能读,发给谁由你决定。

gtmux doctor --bundle                  # 在当前目录生成 gtmux-diagnostics-20260920-0930.tgz
gtmux doctor --bundle ~/Desktop/r.tgz  # 自己指定路径;已存在的文件不会被覆盖

手机也留着同一类记录:它请求 Mac 失败的情况、每次配对以及失败原因、推送注册、实时连接 的断开和恢复,最多 500 条或 200KB,只存在手机上。「设置 → 诊断记录」点进去就能看:每条 都是一句话(「联系不上 Mac · GET /api/agents 等了 6s 没有回应 · 之后一分钟内又失败了 4 次」),按天分组,可以只看出问题的。「拷贝」或「分享」交出去的是没翻译过的原始记录, JSON 行,和 gtmux logs --json 一样,两边同一段时间的记录可以对着看。

gtmux overview

gtmux overview — 2 sessions · 3 windows · 5 panes

▶ web-api              1 window · 1 pane
    0: web-api *  (1 pane)
● worker               2 windows · 4 panes
    0: editor  (1 pane)
    1: claude *  (3 panes)

▶ current  ● attached  ○ detached   * active  Z zoomed  • new output

在任何 shell 里给出的 session/window/pane 汇总。--popup 按 tmux display-popup 的尺寸排好版,你可以绑一个键,让它浮在全屏程序上面而不打断它。

会话与跳转

gtmux restore

退出终端不会杀掉 tmux 服务器和任何会话,没了的只是标签页。重新打开之后,在任意一个 标签页里跑一次:

gtmux restore            # one terminal tab per tmux session, all attached
gtmux restore --pick     # choose which sessions: "1 3" / "1,3", Enter = all, q = cancel
gtmux restore --one      # attach the next unattached session in this tab
gtmux restore <name>     # attach a specific session here
gtmux restore --dry-run  # print what would happen, change nothing
gtmux restore --plan     # preview: which sessions + agent conversations would come back (read-only)
gtmux restore --plan --json   # the same plan as JSON (the menu bar's source for its expandable restore row)

一次只能恢复一次:一次运行会持有一把锁(pid + 启动时间),第二次运行会说明情况然后 什么都不做。锁的进程没了、或者锁超过 10 分钟,就会被接管。--plan 和 --dry-run 不受锁的限制。

真正的 gtmux restore 会在开头打印计划:它即将带回来的会话,以及每个 pane 下面那段 agent 对话(目标)。--plan 就是单独的预览:读最后一次 resurrect 存档加上 resume 记录, 不启动任何 tmux(随时跑、随时轮询都安全)。标着 × 的 agent 行,是 transcript 已经从 磁盘上消失、恢复不了的对话。

只有存布局时确实在跑 agent 的 pane 才会把 agent 带回来;恢复是从存档里每个 pane 自己的 命令记录读出来的。存档时是普通 shell 的 pane,回来还是普通 shell,哪怕你上周在里面跑过 agent。一个 pane 拿到哪段对话,按它的 resume 记录来;记录丢了的话,恢复会从存档记下的 --resume 命令确定会话 ID。两种来源都缺失时,目录和窗格位置只能作为回退依据:原位置须已不存在,且只能有一个未被占用的候选。无法确定时会提示跳过,不再按更新时间猜选;已在运行窗格中绑定的会话也不会被恢复到另一个窗格。只读预览展示存档工作区,执行时还会检查当前会话是否已在运行。

恢复总会打印它正在放回去的那个时刻,比如「Restoring the layout saved at 09:57 (37m ago)」。 写那个文件的自动保存挂在 tmux 的状态栏上,只在有终端接着并在重绘时才跑:合上盖子它 什么都不保存。gtmux serve 会盯着那个文件兜底:大约 10 分钟没有任何东西写过存档 (有自动保存触发器时约 20 分钟),serve 就自己跑一次保存。gtmux doctor 的 resurrect autosave 一行会标出已经武装、但几个小时没保存过的触发器。

如果这一行显示「触发器缺失」,运行 gtmux doctor --fix 即可在保留现有状态栏内容的 同时补上 continuum 自动保存,并让当前 tmux 立即生效。改动写在 ~/.tmux.conf 的 托管块里,写入前会备份;重新加载配置也不会重复添加。如果 doctor 报告有多个触发器, 请检查现有状态栏配置:修复程序不会擅自删除自定义内容。

恢复之后,gtmux 把每个存下来的窗口的 pane 数量和排布跟活着的那个比一遍,把对不上的 点名出来,打在终端上并写进日志库(gtmux logs --component restore),因为 tmux-resurrect 会把自己的布局错误丢掉。

tmux 的 pane id 是每个服务器的序号:重启服务器之后,%25 会发给另一个 pane。gtmux 有 很多状态按这个号索引,所以恢复时(以及 gtmux serve 每隔几分钟)会丢掉那些 pane 已经 不在的按 pane 索引的记录。对话记录(resume/、usage/)按定位和对话 id 索引,永远不动。

诊断一次恢复不需要重启:把 XDG_DATA_HOME 指到任意一份存档的副本上,只读地预览:

mkdir -p /tmp/probe/tmux/resurrect && cd /tmp/probe/tmux/resurrect
cp ~/.local/share/tmux/resurrect/tmux_resurrect_<stamp>.txt . && ln -sf tmux_resurrect_<stamp>.txt last
XDG_DATA_HOME=/tmp/probe gtmux restore --plan     # what restore would bring back from THAT save

第一次运行可能弹出自动化权限对话框(控制 Ghostty、iTerm2、cmux 或 Warp,取决于你的 标签页由谁托管),点允许。重启之后 tmux 服务器也没了;gtmux restore 会启动 tmux 并 显式驱动 tmux-resurrect 恢复最后一次 自动保存(它会等恢复完成,大布局要 30 秒以上;存档在但恢复不了时,它拒绝覆盖那份存档)。 恢复后运行的是新进程。默认情况下,保存时在跑且对话可恢复的 agent,会由 gtmux 用它的 恢复命令重新拉起,接着原来的对话。--resume-agents=type 只填入命令、不执行;off 不动 pane。这一步针对 agent 对话,不负责恢复其他程序。

只要 resurrect 配了抓取,每个 pane 之前的输出(回滚缓冲)也会作为快照回来。 推荐写进 tmux.conf:

set -g @resurrect-capture-pane-contents 'on'   # snapshot each pane's scrollback
set -g history-limit 50000                     # how much scrollback to keep/restore

shell 的 ↑ 命令历史是另一回事:它住在你 shell 的 histfile 里,不在 resurrect 里。 默认只在 shell 退出时写盘,所以一次重启会丢掉最近的命令。想立刻落盘(bash): 在 ~/.bashrc 里 shopt -s histappend; PROMPT_COMMAND='history -a'(zsh 是 setopt INC_APPEND_HISTORY)。

这些规则背后的事故(重启后的幽灵 agent、无声的布局失败)见 TROUBLESHOOTING。

gtmux new

gtmux new                    # a session named for the current directory
gtmux new api                # …named api

新建一个 tmux session 并开一个接上它的终端标签页,走的是 focus 和 restore 用的 同一个终端驱动,所以标签页落在你看得见的地方,省得之后再去找一个 detached 会话。 session 从你运行它的目录起步;菜单栏的「新建 session」在 / 下运行,这时改从你的主目录起步。

gtmux transcript

当前支持已核实 thread 身份的外部 Codex 对话,包括 CLI 创建的对话。创建来源不代表当前控制端。 可从 gtmux agents --json 取 home_id,为 transcript 或 follow 传入 --home-id <home_id>。 读取不改变授权;跟进按服务器、Codex 目录和 thread 隔离,继续回复请回到原客户端。 Codex 暂不支持转入 tmux,其他 agent 保持原操作。

读取已核实的外部 Codex 对话,不开启 HQ 跟进。精确 session_id 来自 gtmux agents --json。

gtmux transcript <session_id>                 # 公开提问与回复
gtmux transcript <session_id> --json          # 轮次、省略数量及 etag
gtmux transcript <session_id> --json --etag <revision>

JSON 包含工具步骤。条件读取未变化时返回 turns:null 和 unchanged:true;失败时返回 非零退出码,不伪装为空历史。视图至多 300 轮、512 KiB。身份未核实则拒绝。 Mac 与已配对手机/iPad 提供只读查看及更新, Web 仍只显示状态;回复请回到原客户端。

gtmux follow

为一段身份已核实的外部 Codex 会话设置 HQ 跟进。检测到会话时, gtmux 默认只显示状态。会话的 session_id 可从 gtmux agents --json 获取。

gtmux follow <session_id> --json
gtmux follow <session_id> --hq on                 # 读取、分析和汇报
gtmux follow <session_id> --notify on             # 单独开启通知
gtmux follow <session_id> --knowledge on          # 采集后续可复用的经验
gtmux follow <session_id> --hq off                # 停止跟进,同时关闭通知和知识采集

--revision <n> 使用上次读取的版本号;其他设备已修改时,保存会失败,避免覆盖。 设置仅对当前 Mac 的这段会话生效,不随 HQ 备份或迁移转移。停止跟进会保留已有记录, 关闭后续权限并结束本轮跟进。知识采集从开启后的新活动开始(以随后一个 Unix 秒为界), 不会回溯采集此前对话。

HQ 跟进不包含输入、批准操作或转入 tmux。Native Codex 的当前控制端未确认, 创建来源不赋予任何权限。其他 agent 保持原行为。菜单栏和已配对的手机/iPad 提供同样的设置;访客链接无权修改。

gtmux adopt

gtmux adopt 4f0c1a2b                 # 把那段对话接进一个新的 tmux session
gtmux adopt 4f0c1a2b 91de77c4        # 一次转入多段对话

在 tmux 之外起的 agent 是只读感知的(「不在 tmux」那一节:它的 hook 触发时没有 $TMUX_PANE,gtmux 知道它存在,但没有 pane 可以显示、跳转或输入)。adopt 按会话 id 在一个全新的 tmux 会话里恢复那段对话,从此这一行就是完整的一行。id 从 gtmux agents --json(session_id)或雷达行上取。对话必须空闲,agent 支持按 id 恢复, 日志里还要能读到消息时间。Codex 对话暂不支持转入:现有协议无法证明前端独占控制, 也没有经过验证的交接入口。雷达行的 adoptable:true 表示这些条件当前满足; 命令在创建会话前还会再查一次。

恢复出来的 agent 接管新 pane 后,gtmux 才尝试终止原进程;有记录的命令名时,会先核对, 防止误杀复用同一个 PID 的别的进程。这不保证成功:不知道 PID 或终止失败时,原进程可能 仍在运行,原终端标签页也不会关闭。没接管成功时,gtmux 会删掉自己建的 tmux session, 原来的对话照常运行,可以再试。 如果那个 session 删不掉,gtmux 会说出它的名字和删除命令,先删掉再试。

gtmux focus

gtmux focus web          # bring the terminal tab showing session "web" to front
gtmux focus %11          # jump to that exact window+pane, then focus its tab

每个标签页的标题是 session — window,所以 focus 找到匹配的标签页并把它调到最前 (通过终端的 AppleScript)。给 pane id(%N)还会在会话内部选中那个 window+pane, 你正好落在 agent 所在的位置;点通知能把你直接放到刚跑完的那个 agent 上,就是这个机制。

一个没有窗口开着的会话(--headless 派出来的,或者你自己 detach 掉的)没有标签页可以 调到前面,所以 focus 会开一个并接上去。判据是会话的客户端数量,跟它当初怎么起的无关: 后来被人接上的 headless 会话就是一次普通跳转。各端会把这样的行标出来 (no window / 无窗口),你点之前就知道会开一个新标签页。

需要 set-titles on 配 set-titles-string '#S — #W',标签页标题才会保持 focus 匹配的那个格式。如果还有别的工具也在写标签页标题,把它关掉,让标题保持权威。

宿主终端:Ghostty、iTerm2 和 cmux 可以根据标签页或面板标题精确跳转。 cmux 通过 AppleScript 定位面板,并在 restore/new 时新建工作区。 cmux 会把 TERM_PROGRAM 设为 ghostty,因此 gtmux 用 CMUX_WORKSPACE_ID 或 tmux 客户端的 cmux.app 进程路径区分两者。Warp 是尽力而为: 它没有 AppleScript 词典,只有当那个标签页的 Warp 会话 uuid 被记进了 tmux 的会话环境时, focus 才能跳到确切的标签页(gtmux 自己的 restore/new 接入会做这件事;或者把 WARP_TERMINAL_SESSION_UUID 加进 tmux 的 update-environment 以覆盖手开的标签页), 否则只是把 Warp 激活到前台;restore/new 通过启动配置来开 Warp 标签页。其它终端 回退到 Ghostty 驱动。宿主自动探测,GTMUX_TERMINAL=ghostty|iterm2|cmux|warp 可以覆盖。

HQ 与 agent 协作

gtmux digest + gtmux hq:HQ,也就是中控会话

gtmux digest 把整支舰队连同含义一眼摆出来,是一张排好列的表:

1 needs input · 2 working · 1 completed

needs input (1)
  ⏸ api:0.0        1.Yes · 2.Yes, don't ask again · 3.No             3 opts   4m

working (2)
  ⠿ web:2.0        fix the login-token refresh bug                 working   1m
  ● hq:0.0 ⌂       ctx 92% — approaching limit                          ⚠   3h

completed (1)
  ✳ mobile:1.0     done, tests pass                                          2d

先是一行按状态的计数,然后每个状态一节(需要你、working、completed,有报错的话 最后是 errored)。每行是状态字形 · 名字 · 目标/最近/在问什么 · 右侧徽标 (派活状态 / 选项个数 / 用量告警) · 相对时间。每个字段都是确定性拼出来的,零 LLM token:goal 是这个会话最后一条用户提示,last 是它最后一条回复的尾巴(两者都来自 agent 自己的 transcript),asks 是等待提示里解析出来的选项。--json 输出机器形态 (也由仅限 owner 的 GET /api/digest 提供;访客返回 403)。没有会话日志时,goal/last 和用量字段为空; 雷达状态和解析出的选项仍会显示。用量另行读取会话日志,不受 driver 的 content 开关控制。 JSON 每行都有 sense:driver(会话记录解析成功,已注册且启用的内容读取器返回时无错误)、 partial(会话记录解析成功,但读取器未注册、被关闭或返回错误)、screen(未解析到会话记录)。 日志不存在时,读取器返回空内容且无错误,因此也可能是 driver。 这说明查询结果,不保证对话内容存在,也不保证每一次状态分类都来自 hook。

gtmux hq 打开 HQ(中控,你的监督会话;已经在跑就聚焦,绝不重复起):你的 coding agent 跑在 ~/.config/gtmux/hq/ 下一个专属 tmux 会话里,第一次会种下一份说明书: 读 gtmux digest --json,做判断,值得时才钻进某个 pane(tmux capture-pane), 用 gtmux send 驱动,向你汇报。说明书是 AGENTS.md,配一个 @AGENTS.md 导入的 CLAUDE.md,所以 HQ 可以是任何 CLI agent。它按你的语言下发(GTMUX_LANG en/zh), 版本变了会用同一种语言重新生成(旧文件留备份,LOCAL.md 不动);只有 gtmux hq --lang en|zh 能换语言。

全新启动且没给 --agent 时,gtmux hq 会问你用哪个已装好 hook 的 agent,并记住选择; gtmux hq --agent codex 或 GTMUX_HQ_AGENT 直接指定,脚本里不弹问、走默认。 个性化写在同目录的 LOCAL.md:优先级、汇报口味、免打扰时段。这个文件扛得过每一次 说明书升级;直接改受管的 AGENTS.md,改动会被挪进备份。HQ 在那个目录里记的笔记 跨会话保留。雷达里它那一行带 role:"supervisor"。

gtmux hq --help 按启动、查看信息、备份与换机、会话轮换分组展示常用操作; gtmux help hq 显示相同内容。完整参数、环境变量和 HQ 内部维护命令见 gtmux hq --help-all。两种帮助均按终端宽度换行(40–80 列); gtmux --help --json 保留完整的 HQ 参数目录。查看帮助不会启动 HQ 或修改档案。

新建的专属 HQ 会话默认命名为 Gtmux HQ,已有会话及自定义名称保留。在“所有 pane”中,确认属于中控的会话组和窗格显示 HQ 标记;旧默认名 HQ/hq 展示为 Gtmux HQ。识别仍依据 HQ 标记和雷达的角色判定,不按名称猜测。

Codex 当 HQ 时默认带 --approve-for-me 启动:日常操作留在工作区内,越过沙箱边界的 请求交给自动审核。通过 --agent 或 GTMUX_HQ_AGENT 明确指定权限参数时,以你的选择 为准。这个设置在启动或重新拉起 HQ 时生效;只切到已运行的会话不会改变其权限。 新启动 Codex HQ 进程时,gtmux 也把 Codex 自身的终端通知限于审批与计划提示, 避免 Ghostty 为 HQ 的每次例行思考弹通知。gtmux hq --rotate 只在同一进程里发送 /new,不能更新启动参数;退出 Codex 进程后再运行 gtmux hq 才会采用新参数。 归属、通知和历史规则见 Codex 接入。

在哪儿跑。不带参数时,gtmux hq 沿用 HQ 已有的窗口:在跑就切过去,退出了就在原窗口 重新拉起,两者都没有才自己建一个 tmux 会话并开标签页。跑过 HQ 的窗口带着标记, 所以在任何地方敲 gtmux hq 都会把 HQ 拉回那里。要换地方:--pane %N 在那个 pane 里 启动(得是空着的 shell,会先 cd 到 HQ 目录),--here 在你正敲命令的这个 pane 里, --new-pane 在当前窗口拆一个新 pane。三者都会把 HQ 的身份挪到新 pane;只要有 HQ 在跑,三者都拒绝,HQ 只有一个。

在你自己正在用的这个 pane 里启动 HQ 是特殊情况:这时终端在 gtmux 手里,所以它不往自己 这个终端里打字,而是直接把终端交给 agent;启动简报改由一个后台进程在 agent 的输入框 出来之后送进去。这期间不会往 pane 里打任何东西,以前简报卡在输入框里没发出去就是这么来的。

gtmux hq --board [--json] 打印态势板,不打开 HQ(菜单栏 app 的态势板阅读器就用它)。 从没写过态势板时返回 exists:false。

在 Mac 菜单栏的态势板中,点击带下划线的 %29 等窗格编号即可打开对应终端; 行首箭头只负责展开详情。窗格已关闭或终端无法切换时会显示原因。 编号指向当前同号窗格,态势板内容可能尚未更新。本次只增加 Mac 阅读器入口, 终端、手机、iPad 和 Web 的操作方式不变。

gtmux hq --home 打印 HQ 目录的路径。gtmux knowledge 的写操作只接受从那个目录发起的 调用,所以提供这些操作的端要先到那儿再执行动词。还没有 HQ 目录时它照样打印路径, 并以非零退出。

gtmux hq --rotate 会为当前 HQ 会话安排轮换。本回合结束、输入框确认安全后,常驻服务 才会投一次重置命令;只有出现不同的会话 ID 才算完成。命令被拒或超时会在 gtmux events --all 留下失败记录。self-rotate 的敲门说会话磨损了之后,HQ 的说明书会不等 你吩咐就执行它(见自轮换),而且先把 notes/board.md 和知识库更新到位。没有 HQ 在跑就没什么可轮换的,它会这么说。

HQ 备份恢复与选择性迁移

菜单栏 HQ → 知识库 → 导入… 区分恢复 HQ 备份和从另一台 Mac 迁移, 继续核对暂存内容可在关闭后返回核对。gtmux hq --import 档案 恢复整份 HQ 目录, 当前目录另存为备份。恢复可加 --expect-archive DIGEST,防止预览后来源变更;摘要来自 migrate --from 档案 --restore-preview --json 的 source。应用或恢复前需退出 HQ; 预览和暂存可在 HQ 运行时进行。

gtmux hq migrate --from /path/hq.age --json   # 只读预览;会询问口令
gtmux hq migrate --from /path/hq.age --stage --include knowledge,local --json
gtmux hq migrate --list --json
gtmux hq migrate --review STAGE_ID --json
gtmux hq migrate --apply STAGE_ID --entries pitfalls/example --reviewed --json
# 比较当前和来源 LOCAL.md 后,单独选择替换:
gtmux hq migrate --apply STAGE_ID --apply-local --expect-local CURRENT_DIGEST --reviewed --json

CURRENT_DIGEST 来自最新核对结果的 current_local_digest。自动化可用 --passphrase-stdin 从标准输入首行读取口令,不将秘密放入参数。暂存需明确指定 --include 列表(knowledge、local、tools);只有决定迁移敏感历史时才加 --include-sensitive,未加密档案需加 --allow-plain。工具仅作为无执行权限的附件暂存。 知识单独选择、核对;--reviewed 表示已确认适用于新 Mac,不表示批准新的机器/仓库分发范围。 可用 --expect-source DIGEST 防止预览后来源档案变更。即使不加 --json 也输出 JSON。 需同时检查退出码与回执 committed;提交后视图失败用 gtmux knowledge render 修复, 不重复采纳。知识和个人要求分别应用;历史冲突不覆盖当前内容。旧态势板、会话、待整理线索、 内置规则及凭据不迁移。限制及旧档案处理见换机迁移。

唤醒通道:HQ 怎么知道事情

决策密度高的事件会往活着的 HQ pane 里敲进一行信号。格式固定,也刻意不像对话, 一屏信号扫一眼就能读:

» ◆ gtmux·waiting·permission  api:0.0 (%7) │ title:"run the tests?"
» ▸ gtmux·done  web:2.0 (%11) │ 3m │ goal:"fix the login bug" │ tail:"tests pass" · #a3f1c2

» <grade> gtmux·<class> <loc> (<pane>) │ <field> │ …,其中每一段由 agent 或用户 写出的内容都加引号并带标签(title: / goal: / tail: / ask: / err:)。 它是 HQ 转述的数据,永远不是它要照办的指令。

等级打头、位置固定,所以一屏信号先按分量读、再按字读:◆ 决策(需要你:不可逆、 代价大,或者对方明确在问)、▸ 注意(某条线卡住了,或者变化到值得你知道)、 · 台账(记账:记下来、可拉取,不值得打扰)。等级由类别决定,只说这一行该读得多响。 类别如下:

类别等级什么时候发
waiting·<kind>◆一个 agent 卡在你这儿(授权 / 计划 / 提问)
agent-relay◆agent 向 HQ 提了阻塞疑问或待用户决策的事;按请求 ID 读取
resolved▸那个等待解除了:你在 pane 里回了,或者 agent 自己继续了;HQ 会撤掉过期的追问
asks◆回合末尾的回复里问了个问题,但没有菜单(只看菜单的传感器会漏掉)
done▸任何会话干完活进入空闲,不限于派出去的任务。完成发生在你正看着的那个 pane 里就抑制(hqWake.done:默认 unattended | always | tick),并按 pane 合并限流
crash◆这一回合死在 agent / API 报错上,绝不会被读成「完成」。同一个 pane 五分钟内的同一个错误只敲一次门(每一次仍都记进 journal)
goal-changed◆你直接往某个 agent 自己的窗口里提交了提示(包括斜杠命令),HQ 于是感知到一件不是它派的活
new-session▸新感知到一个 agent pane,去建联
reap-suggest▸某次派活看起来可以回收了,行里带着可直接用的 gtmux reap <id>
stuck·waiting▸一个 pane 等你超时了。每次等待只发一次,而且只在是 agent 在问时才发(gtmux 仅凭屏幕推断出来的等待永不升级)
resource·warn / limits·warn▸机器 / 订阅越过了某条线(有阻尼,见 gtmux resource)
usage·warn▸某个会话越过了上下文 / 消耗的某一层(见 gtmux usage)
wake-degraded◆感知本身坏了:唤醒不再落到 HQ pane 上
tunnel▸远程访问变了:down(手机连不上这台 Mac,附隧道自己报的错误)或重新 up。读的是隧道自己的状态,只在切换时发,并记入事件流 gtmux:tunnel
tick·周期简报,只在真的有变化时才发(安静的那一轮零成本)
distill·该做一轮知识沉淀了(约每周一次;gtmux capture 攒够 5 条候选会提前):HQ 把这段时间的教训折进知识库,并剪掉过期的
self-check·HQ 该做自己的家务了(约每天一次):清理陈旧的待办,检查记忆和日志健康
unread·有事件压在 HQ 的消费水位之外:给一个计数和拉取游标,不对重要性下判断
self-rotate◆HQ 自己的会话磨损了(上下文 / 时长 / 回合数):它交接完自己轮换,见下

要等一个动作才能清掉的类别(self-rotate、unread、各种 warn)在重复之前会自我复核: 前提集合和世界都没变的重复会被抑制,一有漂移就重新武装,另有一条安全下限保证常驻的债 不会被忘掉。排队中的唤醒在敲进去之前会重新采样,前提已经不成立的会被丢弃。

distill 和 self-check 是维护类:以最低优先级敲门,永远不插到卡住的 agent 前面, 两轮都只在 HQ 真的做了事情时才出声。它们也是仅有的纯靠时钟触发的类别;HQ 自己没有 定时器,所以是 gtmux serve 让它们发生。

其余一切都在拉取侧:HQ 醒来,自己读 gtmux events --since-seq <n> 或 gtmux digest。普通的进展回合永远不碰它的屏幕。

HQ 对信号线的回复也是信号线:一行,以 ⟣ 加一个字形开头,所以它的 pane 和收件箱 是同一种扫法。图例:

回复含义
⟣ ✅ <pane> <judgment> → <next step>一次值得知道的完成
⟣ ▪ noted: <one clause>例行结果,记进态势板,什么都不欠
⟣ 📓 captured: <topic>一条持久经验写进了知识库
⟣ ⚠ <escalation>有事需要你,按升级策略
⟣ ◈ 简报 <time> │ <counts> │ top item周期简报(后面最多 5 行缩进的 · )

消费水位:为什么不会漏掉东西

上面的类别是优先级标签,说的是先读哪个。HQ 到底能不能知道某个事件,靠的是一条 消费水位:gtmux 记录 HQ 把事件流读到了哪儿。有事件压在水位之外超过 hqWake.unreadDebounceSec(默认 120 秒),gtmux 就敲一次门,只给计数:

» · gtmux·unread  7 unconsumed (%21 ×4 · %13 ×2 · control) │ pull: gtmux events --since-seq 6653 --json

这一行会说清它数的是什么,按来源分(control 是 gtmux 自己的维护触发,native 是非 tmux 的 agent 会话),多的排前面,超过三项折成 +N more。(为什么光靠类别不够, 见 TROUBLESHOOTING。)

这笔债只有 HQ 消费才会清,而且只有两件事算数:从 HQ home 本目录或其子目录发起的、不带过滤的 gtmux events --since-seq <n>(醒来之后的日常拉取),或者从同一目录树执行一次显式的 gtmux events --ack <seq>。带 --severity 或 --acts 过滤的读不算,加 --all 也一样 (只看到子集);起点超前于水位的读不算(跳过了中间那段),检测到序号断裂的读也不算(有事件在没被读之前就轮转 掉了):拉取会重新告警,敲门行带上 · sequence gap 标记和一条重建提示(先 gtmux digest --json,再显式 --ack),直到 HQ 刻意 ack 过去。在那之前,敲门每 hqWake.unreadRepeatSec(默认 300 秒)以常驻优先级重复一次。

三类记录不计入计数:HQ 自己写的行;没有 pane 的生命周期闪烁(一条没有 pane 的 SessionStart,几秒内就跟上了 SessionEnd);gtmux 自己的审计轨迹(gtmux:audit:*: 每批唤醒送达或丢弃及原因、每次 gtmux send 的结算、回收、HQ 会话的轮换链)。 闪烁规则认的是 Start/End 配对,不只认空 pane:native(非 tmux)agent 的回合和 gtmux 非审计的 gtmux:* 触发同样没有 pane,照样计数,而且对它们来说这条敲门是唯一的通道, 因为按类别的唤醒都要求有 pane。

HQ home 本目录下的不过滤增量拉取默认隐藏这些不计数的记录;子目录下显示原始视图, --all 也显示原始视图。是否计入消费仍要满足上面的条件。日志按大小上限轮转, 旧代文件可能离开保留的历史。

gtmux events 会说清一条指令是谁写的 —— 前提是那不是你。同一个 pane 上的两条提交,不管是你自己敲的还是 HQ 投递的,字段完全一样;唯一分得开的是 gtmux 自己那条投递留痕,而它恰好被中控的拉取视图当作「不欠的东西」隐掉了。所以现在直接告诉读的人:由 gtmux 代别人投递进去的那条,行尾会打上 ← hq(或 ← agent:%N),--json 里是一个附加的 author 字段。没有 author 的,是没有哪次投递对得上的,通常就是你自己写的。每次投递只认一条:同一个 pane 上,先看文字吻合得最完整,再看时间上最近(相差两分钟以内)的那条。所以一小时前你自己敲的同样的话不会被后来的投递认走;两条一样合适时,一条都不认。

归属是读的时候算出来的,所以欠什么、显示什么都没有变:投递留痕仍然不计入消费债务,也仍然不出现在默认视图里。这件事不需要 --all,--all 本身也没动。被拒绝或失败的投递没有真的进到 pane,不会归属任何一条。

gtmux doctor 的 HQ 维护一节报告这个滞后(event consumption),落后 20 条以上或 30 分钟以上就标出来。

投递会守住你的草稿,并自己确认:一行永远不会被敲进非空的 HQ 输入框(它会排到磁盘上, 等框清了再落),一批内容在被确认或记录了丢弃原因后才离开队列。未确认的发送只会有限重试, 行尾的 #<id> 让 HQ 能认出重复,但不保证送达:同一个 id 出现两次就是重发,HQ 的说明书要求 忽略它。每一种结果都进审计(gtmux:audit:wake-delivered 带完整批次, gtmux:audit:wake-dropped 带原因:被挤掉 / 未确认 / 被取代),用 gtmux events --all 能读到。

自轮换:HQ 自己的会话成了问题

self-rotate 说的是 HQ 本身:在一个又长又接近满的会话里,HQ 会开始把自己的产出读成 来自外部的输入,而它从里面察觉不到这一点。所以由 gtmux serve 从外面盯着并敲门:

» gtmux·self-rotate  ctx 82% · 14h · 380 turns │ over: ctx 82% ≥ 75% │ board+KB current → hand off → gtmux hq --rotate

三个事实被感知,任意一个越线就算越界:

hqWake 键默认量的是什么
selfRotateCtx0.75实时上下文占比,和 gtmux digest 报的 ctx 是同一个
selfRotateHours12会话年龄,从 transcript 的第一条消息算起(serve 重启也归不了零)
selfRotateTurns300会话开始以来 HQ pane 上的提示提交次数

任意一项设成 0 就单独关掉那条判据,另外两条照常。selfRotateRepeatSec(默认 1800 秒) 控制重敲的节奏,selfRotateCheckSec(默认 300 秒)控制评估的节奏。

这笔债只有会话真的轮换了才清,判据是出现了新的 agent 会话 id;把唤醒送到不算。 过了 hqWake.selfRotateRepeatSec 之后,只有越界集合或舰队变了才会再敲, hqWake.selfRotateFloorSec(12 小时)是什么都没变时它能沉默的上限。HQ 的说明书要求它 按顺序做三件事,不先问你:把 notes/board.md 和知识库更新到位,记下交接,然后跑 gtmux hq --rotate。常驻服务等当前回合结束后再投重置命令(/clear,Codex 是 /new)。 轮换之后又收到 self-rotate,意思是那次轮换没成。gtmux doctor 的 HQ 对话健康一行给的是同一组数字。(这个类别背后的事故见 TROUBLESHOOTING。)

在 ~/.config/gtmux/config.json 里写 "hqNudge": false 会停掉 hook 发起的唤醒 (waiting、done、asks、goal-changed、usage·warn 等 hook 信号)。serve 自己发出的 summary tick、unread、self-rotate、resource/limits 提醒和 self-check 照常;它不会删除 或停止 HQ 会话。唤醒只告知:gtmux 从不替另一个 agent 回答提问,从不往 TUI 里发导航键,默认策略也是让 HQ 把决策交到你面前。

gtmux capture:往 HQ 知识库里丢一条便签

gtmux capture "<one-line lesson> @<topic>"   # topic ∈ the knowledge vocabulary: six built-ins + topics hq declared
gtmux capture --list                         # show the pending-distill queue
gtmux capture --list --json                  # the same queue, with each line's dedup key
蒸馏已请求 3d 前;等待 HQ 完成
2 pending-distill candidate(s):
  [pitfalls] wrangler TLS-resets from the office network — retry

任何 worker(不只是 HQ)都可以把一条一行的候选丢进待沉淀池 (~/.config/gtmux/hq/knowledge/.pending-distill.jsonl)。每行带这条经验、主题标签、 一个去重键(<topic>/<lesson-slug>,沉淀轮会把同键的候选合并),以及自动采集的上下文 (当前 pane、事件水位、时间戳,调用方是被跟踪的派活时还有 $GTMUX_TASK_ID)。 候选不等于知识库条目:HQ 的沉淀轮判断什么算持久、归到哪个主题、剪掉什么。

--list 开头先说队列上次什么时候被清空。攒够五条候选会把下一次沉淀提前,一条捕获到的 经验一天之内就归档,不用等满一周。--list --json 是同一个队列,一个数组(绝不会是 null),每行带主题、时间戳、来源和去重键,也就是 gtmux knowledge dismiss --capture <key> 要点的那个名字;驳回一个键会消掉所有共用它的 待处理行。

gtmux knowledge mine:不用模型,把会话日志里的线索投进同一个队列

gtmux knowledge mine                 # one pass: read what the agents' logs gained, queue the leads
gtmux knowledge mine --dry-run       # show what a pass would queue, write nothing
gtmux knowledge mine --since all     # widen the correction window from 30 days to the whole stock
gtmux knowledge mine --status        # the ledger: last pass, sources, emitted, recurring errors
read 14 file(s), 3.2 MB, 61 human line(s) → 3 candidate(s) queued, 0 already emitted
  [corrections] this is wrong, do it again
      ↳ after: …moved the toggle into the header and shipped it.
  [pitfalls] bash: wrangler: command not found  (×6, 3 sessions)

采矿器每天跑一次(hqWake.mineIntervalHours,0 关掉)。它从上次停下的字节偏移起读 各 coding agent 的会话日志,减掉机器自己写的一切(工具输出、harness 注入块、gtmux 的 唤醒行和对着审计日志核出来的 gtmux send 内容、压缩摘要、粘贴),只留两种形状当线索: 人在 agent 回话之后紧接着打的那句纠正,连同那段回话的尾巴;以及规范化之后跨会话反复 出现的报错签名,连同次数。全程不跑模型。HQ 的沉淀轮用 knowledge add --capture / dismiss --capture 逐条处理这些线索。

~/.local/share/gtmux/mine/ 下的台账记着每个偏移和每条发过的 id,所以不会重复读、 重复投;已经写进知识库的坑再被踩到时计数照样涨,--status 看的就是这个。来源: Claude Code、Codex、Kimi Code,以及 gtmux 替 opencode 写的记录。只有 Claude Code 和 Codex 记了工具输出,所以报错线索来自这两种,纠正线索四种都有。

gtmux quiet:HQ 可以说多少

gtmux quiet on       # 只呈现 CRITICAL,最安静的设置
gtmux quiet off      # 清掉 quiet;配置和进程环境的覆盖仍生效
gtmux quiet status   # 查看当前真正生效的阈值

这条命令设置 HQ 汇报指令里的呈现下限。quiet on 设置 quiet 开关,quiet off 清掉它。 关掉后,配置里的 surfaceTier 仍然生效;没有配置时才是默认的 NORMAL 及以上。 GTMUX_SURFACE_TIER / GTMUX_QUIET 可以在单个进程里覆盖阈值;quiet status 显示最终值。

gtmux tasks --pending 只列通过 gtmux tasks --await 标成待你决定的条目。 切换安静模式不会把事件放进这张表。

有一样东西永远不会被安静掉:事件日志里的读取时断裂。那是 HQ 在告诉你它可能漏了东西。

gtmux advice:HQ 提过什么建议,结果怎样

HQ 的其他台账记的是它知道什么、派了什么活、看到了什么。这一本记的是它提过什么建议。 只有它能回答另外两本回答不了的问题:这个参谋的判断值不值钱,以及它是不是还在开口。 一个没什么可说的参谋,和一个不说了的参谋,在别的记录里看起来一模一样。

gtmux advice "<你建议了什么>" [--why "<依据>"] [--about "<关于什么>"]
gtmux advice <id> --taken | --declined [--words "<他的原话>"] | --moot
gtmux advice [--open] [--limit N] [--json]        # 最新的在前
gtmux advice --tally [--since 30d|12h|all] [--json]

标结果是追加一条新记录,不是改原来那条,所以反悔这件事本身留得住:周一没采纳、周四采纳了, 显示为采纳,日志里两条都在。算采纳率只看 --taken 和 --declined 两种。还没结果的, 以及被事情本身解决的(--moot),会计数也会报出来,但不进分母 —— 它们都不是对那条判断 的裁决。

写要在 HQ 家目录里跑,规矩和知识库的写入一样;查在哪儿都行,所以你在哪都能看这个数。 你什么都不用记:这是参谋给自己记的工作账,让司令来维护它就没意义了。

gtmux knowledge:知识台账(带来源的条目)

gtmux knowledge add --topic pitfalls --title "wrangler TLS-resets; retry" [--body-file -] [--capture <key>[,<key>…]] [--seq-range a..b]
gtmux knowledge supersede <id> --title "…" [--body-file -]   # replaces an entry; history stays in the ledger
gtmux knowledge retire <id> --why "…"                        # prune, with a reason that survives
gtmux knowledge dismiss --capture <key>[,<key>…] --why "…"   # reject candidates WITH a trace
gtmux knowledge receipts [--capture <key>] [--json]         # 采纳/驳回回执与保留的来源
gtmux knowledge promote <id> --why "…" --for <hq|machine|repo:<path>|everyone>   # WHO must know it
gtmux knowledge land <id>                                  # gtmux carries it: LOCAL.md / every agent's block / the repo file
gtmux knowledge land <id> --ref "<issue url>"              # everyone: you opened the issue the brief links to
gtmux knowledge withdraw <id> --why "…"                    # the entry was right, the promotion was not
gtmux knowledge sync [--force] [--repo <path>]             # refresh the knowledge block in each agent's instruction file
gtmux knowledge carriers                                   # each agent's instruction file and whether it is in sync
gtmux knowledge lint [--json]                              # 体检:问题、独立条目信息提示、相似条目复核候选;只报不改
gtmux knowledge style [--json]                             # 条目该怎么写:每条规则配一组改前改后
gtmux knowledge search "<要找什么>" [--topic 主题] [--kind 种类]  # 用一句话问知识库,中英都行
gtmux knowledge neighbours <id> | --capture <key> | --text "…"   # the closest live entries; `add` shows them before writing; `capture --list` groups the pool by them
gtmux knowledge kind <id> <facts|howto|pitfalls|judgment|decisions>   # confirm or correct what an entry IS
gtmux knowledge hit <id> [--n N] [--why "…"]               # the lesson was hit again (its count is the feedback)
gtmux knowledge confirm <id>                               # a hypothesis held up
gtmux knowledge alt <id> --lang <zh|en> --title … [--body-file -]   # write or replace the entry's other-language half
gtmux knowledge add … --sensitive --confirmed "<their words>"   # the commander's own detail: stays on this machine, written after asking
gtmux knowledge sensitive <id> [--off] --confirmed "<their words>"   # mark (or unmark) an existing entry
# add/supersede also take --kind, --tags a,b, --provenance <correction|recurrence|mined|capture|self>, --hypothesis
gtmux knowledge topic <name> --desc "…"                      # declare your own topic (clients, datasets, …)
gtmux knowledge land <id> --ref "<pr/spec>"                  # close the loop when it lands
gtmux knowledge promotions [--json]                          # the pending export queue
gtmux knowledge list [--topic t] [--json]  ·  show <id> [--json]  ·  render [--check]

写给使用者的那一份导览(存在哪、一条经验怎么走完、你能改什么)在 docs/knowledge.zh.md。

知识库的权威是一份只追加的台账(~/.config/gtmux/hq/knowledge/.ledger.jsonl); 主题的 .md 文件从有效条目渲染出来,归 gtmux 所有、会查漂移(render --check 抓手改, render 复原)。每条都带来源:写入时的事件 seq、可选的沉淀区间,以及消费了捕获候选时 那条候选的 pane/seq/task 和键。add --capture <key> 把所有同键的待处理候选消费进一条; dismiss 把它们连同一条日志痕迹一起删掉。每一次写操作都往事件流追加一条 gtmux:audit:knowledge。

写操作只接受从 HQ home 本目录发起的调用; worker 用 gtmux capture 记候选。list/show 在哪儿都能用。给指挥官另留了一扇更窄 的门:gtmux serve 把知识库暴露给经 owner 认证的客户端(GET /api/hq/knowledge 是 不带正文的索引、GET /api/hq/knowledge/entry?id=、POST /api/hq/knowledge/act), 只接受 land、retire、carry 和 withdraw 四个动词。HTTP 的 land 必须带 ref; carry 则按选中的 audience 写入载体。两扇门写的是同一条 gtmux:audit:knowledge 记录。

内置六个主题(accounts、workflows、best-practices、pitfalls、corrections、environment); gtmux knowledge topic <name> --desc "…" 声明你自己的,立刻带着描述渲染出来, gtmux capture、每一个 knowledge 动词和派活时的知识回声都认它(自定义主题在那里和 pitfalls/workflows 一起出现;accounts / corrections / environment 不进派活上下文)。 名字是 slug(a-z 0-9 -,不超过 40 字节);内置的、已存在的和保留目录名会报错。 声明目前只能新增。

每条都有语言(lang),可以带另一种语言的那一半:add/supersede 接 --lang 与 --alt-lang --alt-title [--alt-body-file -],事后用 alt <id> 补。读者拿到的是: 源语言对上用源,否则用另一半,都没有就用源并打 [zh]/[en] 标。list/show 随 GTMUX_LANG(--lang 覆盖),本机文件按台账的多数语言渲染,everyone 的简报和 issue 用英文。lint 报 monolingual 计数;gtmux 自己不翻译,两半都由 HQ 写。

敏感条目是指挥官自己的信息(账号、个人情况、他自己决定放进来的凭据)。写入必须带 --confirmed "<their own words>",也就是 HQ 把条目给他看过、他点头的记录。它不进入 本机全局或仓库的分发(promote 只接受 hq,machine.md 和仓库块跳过它)。 整个 HQ 的导出仍包含台账和其中的敏感条目,所以要私下保管归档。Mac 和手机上显示一把锁。 lint 的 unmarked-sensitive 报长得像凭据却没打标的条目。别人的密钥照旧不进库: 只记在哪,不记本身。

lint 还核对脚本和条目的配对:knowledge/tools/ 下没有条目指向的脚本报 orphan-tool,条目指向了不存在的 tools/<script> 报 broken-tool。

每条条目落在三条轴上:kind(facts、howto、pitfalls、judgment、decisions); provenance(correction、recurrence、mined、capture、self,计数由 knowledge hit 和采矿器往上涨);audience,晋升时填。topic 保留为 id 前缀和自由 标签。轴出现之前写的条目读取时按固定表映射、带 ? 标记,直到 knowledge kind <id> <kind> 确认;台账文件永不改写。--hypothesis 把没证实的线索搁在 单独一节,不分发,直到 confirm。

promote <id> --why … --for <hq|machine|repo:<path>|everyone> 在 knowledge/promotions/ 写一份晋升简报,带教训、理由、读者及其出口、条目的出处。 hq、machine、repo 三种由 gtmux 替你搬:land <id> 把条目写到那个读者看的地方 (LOCAL.md · 正本 ~/.config/gtmux/knowledge/machine.md 加每个 agent 全局指令文件里的 索引块 · 仓库的 AGENTS.md,不提交),并以那个路径为 ref 闭环。everyone 的简报里带 预填好的 GitHub issue 链接,人开完用 --ref <issue url> 落地。withdraw <id> --why 撤回晋升,条目留着。gtmux doctor 会标出滞留超过两周左右的简报;everyone 不计超期。 knowledge sync 刷新各 agent 的块,carriers 看每家状态,被手改过的块不给 --force 不覆盖。

knowledge lint 报孤儿、断链、有歧义和过时的 [[links]]、疑似重复、超期的猜想和晋升、 待确认的种类,以及两类写法问题:ai-voice 是读起来像机器写的条目, title-unreadable 是标题拿字段名占住了本该说清发生了什么的那一行。只报不改,一行摘要随 self-check 的敲门送到。 JSON 的 severity 和终端输出区分 issue、review、info。写法提示、待确认种类、 待补翻译、疑似凭据及过时引用也属于 review,需先核实。orphan 为兼容旧 JSON 保留这个名字,含义是没有进出链接的独立条目;链接不是必填项,这类 info 不是 缺陷。self-check 摘要称它为 standalone entries (info)。near-duplicate 是 review 候选,核对事实、出处和适用范围后才能调整台账,不能凭相似度删除。 [[...]] 一类示例占位符不算链接;token= 后面 只有像凭据的值才触发敏感提醒,普通说明文字不算。 两个主题有同名 slug 时,请写 [[topic/slug]];lint 不会替 [[slug]] 猜目标。

knowledge search "<要找什么>" 用一句话问知识库。neighbours <id> 问的是另一个问题: 跟这一条挨得最近的是哪几条;add 写入前也会问一次。三者走同一套检索,全包里只有它 一处决定什么叫「对得上」。它把英文按词切、中文按相邻两字切,所以一句没有空格的中文 也能查;它还按一个词有多罕见来给权重,少数条目才有的词比满库都有的词更算数。拿 720 条里 HQ 亲手连的 904 组 [[link]] 量,这个权重把前十条的召回从 36% 提到 55%。不用模型也没有索引: 数一遍、打一遍分,720 条大约 60 毫秒。capture --list 把候选池按家族分组, 一次 add --capture k1,k2,… 收成一条。

knowledge style 就是这些规则本身,lint 的匹配器取自同一张表,所以两边不会各说各的。 每条规则讲清怎么做、为什么,再给一组改前改后,中英各一份。每条还标明是机器判还是人判: 分级借的是 humanizer 的判法,一个毛病算不算数,取决于一个认真写字的人有多罕见会故意这么写。 聊天残留、装饰性的 ⇒、只有自己人懂的自造词、把猜测当结论、正文第一句重复标题、 标题由标识符拼成,出现一次就算;破折号、粗体、「不是 X,是 Y」要凑够两类才算; 至于一个对比该不该立、一句话有没有说事,没有正则判得了,所以那几条只写出来、不设检查。 代码、表格和引文原样跳过不读。其中两条是为「读者是 agent 而不是人」准备的: 能执行、能核对的东西逐字不动,以及一条条目要说清谁、在哪、发生了什么、该怎么改。

台账出现之前手写的主题文件,在第一次动到那个主题的写操作时被搬到 knowledge/legacy/<topic>.md;渲染里链过去,派活时的知识回声仍然查它(细节见 TROUBLESHOOTING)。

gtmux relay:agent 向 HQ 汇报和提问

在受 gtmux 管理的 agent pane 中,gtmux relay report --body-file /tmp/update.txt 把进展安静记入台账;gtmux relay ask --body-file /tmp/question.txt 记录阻塞疑问, 按请求 ID 唤醒 HQ。不阻塞的疑问加 --nonblocking;必须由用户决定的事项加 --for user;重试时沿用 --id。HQ 用 gtmux relay list --json / show <id> 查看,认领后用 reply <id> --body-file <file> 回复,或用 close <id> 结案。 回复标注 HQ 来源,只送回原来的存活 pane。待用户决策的请求必须由用户在源 pane 回答,HQ 不能代为批准。全部用法见 gtmux relay --help。 gtmux knowledge sync 会把简短入口写进 Claude、Codex、opencode、Kimi 的全局 指令块,新会话启动时加载。其他类型的 agent 通过 gtmux spawn 派发内容得到说明。

gtmux spawn / gtmux send / gtmux tasks / gtmux reap:带核验的派活

gtmux 直接启动的 Codex 默认使用 --no-alt-screen,覆盖 spawn、HQ 和标准恢复/转入 tmux,保留后续终端滚动历史。显式屏幕设置和自定义启动器保持原样;正在运行的客户端和全局 Codex 配置不会被修改。独立屏幕程序只提供当前终端画面,持久会话内容在对话模式中查看。详见 Codex 集成。

gtmux spawn <goal> 把新活派给一个 coding agent,并确认它落进去了:

gtmux spawn "add a --dry-run flag to the deploy script"
gtmux spawn --title fix-auth-mw --worktree feat/dry-run --model opus "add a --dry-run flag"
gtmux spawn --pane %14 "keep going, then run the tests"
gtmux spawn --json "…"   # → {task_id, pane_id, loc, title, session, delivered, state, judged_by, evidence}

只要比一行短句长,就走 --goal-file <path>(--goal-file - 读 stdin); gtmux send 有同一条通道,叫 --message-file <path|->:

cat > /tmp/goal.txt <<'EOF'
把 `hqPlaybookVersion` 提到 13,并且:
1. 运行 `make check`
2. 别让 shell 碰 $HOME 或 `for f in *; do echo $f; done`
EOF
gtmux spawn --title bump-playbook --cwd ~/src/gtmux --goal-file /tmp/goal.txt
gtmux send %14 --message-file /tmp/reply.txt

直接写进双引号命令行参数的目标会先被 shell 解析:反引号包住的会被执行,$foo 会被展开; 引号里的换行会保留,没被引用的换行才会结束命令。文件通道上没有 shell:字节从文件 → gtmux → tmux load-buffer - (一个管道)→ agent 的输入框,最多剥掉一个结尾换行(每个 heredoc 都会加一个)。 同时给文件和位置参数是错误。短指令用位置参数没问题(gtmux spawn --pane %14 "keep going")。

重跑一次失败的派活会收敛到一个 worktree、一个会话、一条台账记录:--worktree 复用 已经服务于那个分支的 worktree(同一路径挂着另一个分支仍是硬错误);spawn 会接管自己 上一次的尝试(一条拥有自己会话、目标从没送达、pane 还活着的台账记录)并更新同一条 台账行;某一步失败且没有可续的东西时,这次调用创建的 worktree 或分支会被回滚。

--worktree 给的分支还不存在时,新分支从 spawn 运行处(--cwd,否则是当前目录; 主签出或另一个 worktree 都可能)当前签出的提交开出,不一定是默认分支。这个提交落后于 默认分支(有远端时是上次 fetch 到的 origin/main,没有远端时是本地 main),或带着 默认分支上没有的提交时,spawn 会在 stderr 上说明,--json 也一样,并给出把新分支挪到 默认分支上的 git rebase --onto,先让 agent 停下再运行(它保留分支自己新加的提交,有未提交改动时会拒绝)。想一开始 就从默认分支开,先建好分支(git branch <分支> origin/main):已有的分支会原样使用。

--title 命名这个窗口的目的,一个简短的动宾 kebab slug(fix-auth-mw、 review-pr-518),它会成为 tmux、雷达和 app 里的窗口名和 pane 名。成功时 spawn 报出句柄 <loc> (%pane) · <title>;loc 是实时的 tmux 定位 session:window.pane, 每次读时重算,在 renumber-windows 下仍然正确。引用派出去的窗口就用 loc %pane · title,这样能按号跳过去;HQ 的说明书要求每次派活都给 --title, 每次汇报都带这个句柄。

Codex worker 默认带 --approve-for-me 启动,和 Codex HQ 一样:日常操作留在工作区内,越过沙箱 边界的请求交给 Codex 的自动审核,不会停下任务等你。agent 命令里自带审批或沙箱参数(-a、 --ask-for-approval、--sandbox、approval_policy=……)时,以它为准。

spawn 拒绝在 HQ 目录里跑 worker(显式 --cwd 指到那儿、没给 --cwd 时继承的 cwd 落在那儿、或者 --pane 复用一个坐在那儿的 pane):那个目录的 AGENTS.md 是 HQ 的 章程,在那儿起的 worker 会读到它然后冒充 HQ。请传 --cwd <project dir>。

spawn 拉起 agent(默认是一个全新的 detached 会话,--pane <id> 复用一个, --worktree <branch> 跑在隔离的 git worktree 里),配置了代理就带上代理环境,等 agent 起来,然后经 tmux 粘贴缓冲区投递任务并核验它落地。

等 agent 起来是一道真的闸。pane 要满足这些条件才算就绪:输入框已经画出来,没有信任闸 或选择菜单挡着,屏幕上没有启动横幅,而且两次抓屏逐字节相同(或者 agent 的会话启动 事件已经发出)。启动横幅(Connecting…、Loading…)会挡住这道闸;常驻通知 (如 ⚠ N MCP servers need authentication · run /mcp)说的是只有你能做的动作,永远 不会自己消失,所以不挡。超时时失败信息是 ✗ 未送达 → <handle>。证据:,接着是 agent composer not ready within the ready timeout — blocked by: <the line that said no>, 后面跟着 pane 的底部区域。

核验是分层的。装了 hook 的 agent(Claude Code、Codex …)以 agent 自己的 UserPromptSubmit 事件为回执,不抓屏;其余的用两帧屏幕读取按结构定位输入框。 回执确认过的落地是终局,而且在给出任何 delivered:false 之前会再查一次回执, 卡在截止线上才到的确认不会丢。JSON 结果会说明是哪一层判的 (judged_by: driver|screen)。在 ~/.config/gtmux/config.json 里写 driver.<agent>.receipt: false 或 driver.enable: false 会强制走屏幕读取, 用来隔离事件通道的故障。

粘贴是 bracketed paste,所以多行指令作为一个草稿落下,再由单独的 Enter 提交一次。 唯一算成功的是确认落地:被吞掉的 Enter 会退避重发,粘了一半会重试,超时会带屏幕证据 报 delivered:false。重试永远不会重复:重粘只发生在确认为空的输入框里(清除键只清 一行,多行草稿可能扛得住它),只是渲染晚了的粘贴会被放着不动。排队中的提交报为 state:"queued"。重发互锁会拒绝在一个时间窗内向同一个 pane 发完全相同的载荷 (重复的 /compact 不会连打两下);--force 越过它。启动前会提示当前代理配置、机器资源 警告、缓存的订阅额度警告和匹配的知识条目;这些提示不拦派发,也不测试代理是否可达, --json 模式会跳过。

--oneshot 通过 agent 的 headless 模式派一个一次性、非交互的 worker (claude -p … --output-format stream-json、codex exec --json)。agent 必须具备且开启 headless 能力,否则拒绝,不会降级成交互式 spawn。runner 把目标作为参数交给 agent, 跳过交互输入框的粘贴和落地核验。spawn 不等 worker 跑完就返回:它的 landed 是派发记录, 不是任务完成结果。

运行仍在 tmux pane 里,JSON 流看得见,雷达上有它的行,也能 reap。解析后的结果是错误或进程以 非零码退出,就记录失败;否则记录完成。无法识别的输出会被忽略,因此即使没有识别到结果事件, 退出码 0 也会记录完成。digest 仍按会话记录和 content 读取器来分档,--oneshot 不保证 sense:"driver"。worker 是非交互的,可以看,不能中途接管。 --headless 只是不开终端标签页,派出来的仍是能 attach 进去操纵的交互会话。

gtmux send

gtmux send <pane> <text> 默认用同一套落地核验(一确认就返回);--no-verify 跳过 核验,--force 越过互锁,--json 打印核验结果({delivered, state, judged_by, evidence}, 仅限核验过的发送)。

gtmux send 不认识的选项会被拒绝,什么都不发:以前拼错的 --body-file 会当成正文进到对方 agent 的输入框。以 -- 开头的词不管夹在正文哪里都算,gtmux send %5 make --dry-run 也会被拒。 这样的文字整段放在单独的 -- 后面(gtmux send %5 -- make --dry-run),或者用 --message-file。

--attach 文件(可重复,每个不超过 30 MB)像手机一样按路径把文件交给 agent:文件拷进 gtmux 的 uploads 目录(7 天或 200 MB 后清理),路径单独一行接在消息后面;--json 用 attachments 列出拷贝。带附件时消息可以为空(只发一张截图)。带文件的消息不会打进正在问你话的 agent:雷达显示它在等你,或屏幕上有选项菜单,就拒发(refused-waiting)。粘贴前、每次按回车前都会 再查一次,发到一半冒出来的提问不会被按回车,也不会重试;读不到屏幕同样拒发。不带文件的普通 gtmux send 照旧可以用来回答菜单。这只是把窗口缩小,没有消除:回车前最后一刻弹出的提问仍可能吃到 这个回车。它也宁可多拒:刚回答完的提问,标记会留到批准的工具跑完;雷达要在 6 秒内两次看到 pane 有变化 或在占 CPU,才认定它在工作而放行。平时菜单栏 app 的轮询会提供前一次观察;没有人在轮询、或者工具运行时 悄无声息,就会被当成还在等你而拒发,稍后再发即可。如果是粘贴之后才拒发,文字可能已经留在 agent 的 输入框里,再发之前先看一眼。拷贝按内容命名,同一个文件加同一条消息发两次就是同一份载荷,互锁会拒掉第二次。路径只在 这台 Mac 上有效。

gtmux send %5 --message-file note.txt --attach "Screen Shot.png"

草稿检查会拒绝它识别出的、别人还没提交的文字。 粘贴是追加到输入框的,落在写到一半的行上 会把对方的字连同你的载荷一起提交。每一条路径(CLI、HQ、--no-verify、手机)都先读 草稿,确认冲突后拒绝(state:"refused-draft",什么都没写),并把草稿引回来给你看。 读不到或认不出的草稿仍可能被放行,见下表。 --force 可以豁免;手机那把幂等键不豁免,因为来自另一台设备的发送现场没人能撤销。

这道草稿检查只在已知 agent 驱动的 pane 上运行(跑着 vim、ssh 或别的 TUI 的 pane 没有 输入框,在那儿读会把 transcript 当成「草稿」)。它读带颜色的抓屏,所以 agent 自己那条 暗淡的「建议下一条命令」不会被当成你在打字;而且要在两帧里看到同样的东西才拒绝。 凡是它判断不了的,都放行:

它看到什么它怎么做
pane 里没有 agent(没有输入框)发
抓屏失败(tmux 打了个嗝、pane 没了)发
pane 正在回滚(copy-mode)发
一个普通 shell,没有输入框发
你自己的文字,同一条消息在重发发(幂等)
别人的文字,而且看到了两次拒绝

它最多花两次读取加一个轮询间隔,从不循环。

以 failed 收尾的发送可以立刻重试:互锁会丢掉从没落地的尝试记录,重试不会被 refused-duplicate 顶回来。queued 的发送保留记录,agent 已经收下了。--no-verify 和手机端 POST /api/send 跳过的只是确认:每一条文本路径都先粘贴、再把 Enter 作为 独立的键发出去,所以不核验的发送同样不会把多行消息拆开。--key 仍然是单个按键。 比一行短句长的内容,用 --message-file <path|->。

普通终端 pane(进程子树里没有 coding agent,前台是 bash、zsh 这样的裸 shell)是直接 打进去的:文本然后 Enter,没有输入框确认,也没有重发互锁。--json 把这种发送报为 {"delivered":true,"state":"sent"}。不能证明是普通 shell 的(vim、ssh、进程扫描漏掉的 agent)走的还是 agent 那条管线。

gtmux tasks

gtmux tasks [--json] 是派活 / 需要你的账本:每一个你派出去的任务,带实时状态 (undelivered / waiting / done / working / gone),需要你的排最前。--verbose 加上 归档条目和注意力列(层级 · 优先级 · 是否呈现 · 处置)。

undelivered 排在最前面。台账记的是投递判定,所以目标从没抵达 agent 的记录,无论它的 pane 看起来多空闲,都读作 ✗ undelivered。queued 的投递不算未送达(agent 收下了, 排在当前回合后面)。一条把同一个目标送进同一个 pane 的 gtmux send 会把记录标成 已送达;往 pane 里敲一句无关的话不会。

gtmux tasks --pending 是待决策的常驻视图,也就是摆在你面前的事:

▸ t1kx8p2m9dq3  hq %21                 08-09 14:32  ship v0.48.0 or hold for §4?
▸ t1kx9r4w0aa1  worker-b %8            08-09 09:11  which branch should the migration target?

打头的字形是 ▸(这张表上的条目在等一个决策)。排序是等得最久的在前,然后按 pane, 再按 id。视图只读台账,打的是绝对时间戳,所以对一张没变的表读两次,结果逐字节相同。

条目进出这张表用 gtmux tasks --await <task_id> 和 gtmux tasks --resolve <task_id> [disposition](处置记录它是怎么离开的: decided / withdrawn / escalated;不给就只是清掉)。在表上的判据是 awaiting-commander 这个处置,所以任何别的处置同样会把条目拿掉,归档一条记录也会把它 从视图里关掉。

gtmux reap

关闭资源失败时不继续删除 worktree/分支;任一步失败都会保留任务台账,便于收尾。 --json 的 resource_closed 表示 tmux 资源关闭,不代表共享 Codex 后台退出或对话删除。 回执记录任务和结果;精确绑定仍可核实时,才附带对话、绑定和位置 ID。

gtmux reap <pane|task_id> 安全地回收一次跑完的派活。先过一道安全闸(worktree 必须 干净、分支必须已合并),过了才杀会话、删 worktree、删掉已合并的分支;闸没过就精确报告 是什么挡住了,什么都不碰(--abandon 强行越过,--keep-branch 保留分支)。失败的步骤 连同 git 自己给的原因列在 ⚠ but these steps failed 下面,命令以非零退出。 --snooze [--for <dur>] 对一个你要留着的派活消掉回收建议。被跟踪的派活看起来可以回收时, 活着的 HQ 会收到一条 » gtmux·reap-suggest … │ gtmux reap <id> 唤醒。回收永远是 建议 → 批准 → 执行,不会自动发生。

但建议不只看「闲了多久」。台账里只有 HQ 派出去的活,你自己直接驱动的会话从来不进台账, 它会一直显示当初那条老 task、标着已完成;以前安静半小时就够让它变成候选,gtmux 曾在同一 分钟里建议回收两个还活着的主力会话。现在只有在记录能证明「这个 pane 自派活以来收到的每一 条指令都是 gtmux 自己投递的」时才会建议。证明不了就不建议:少提一条的代价是一个 pane 多留 一会儿,提错一条的代价是一整份上下文。

远程访问与分享

gtmux serve

运行手机和网页连接的本地服务,默认端口 8765。局域网内使用 gtmux serve;跨网络可用 gtmux tunnel。完整绑定地址、端口和凭据参数用 gtmux serve --help 查看。

gtmux tunnel

Standard 使用固定的托管地址,--backend self 使用 Direct。--service 让隧道开机常驻, --unservice 关闭,--status 查看已安装的线路。

通常只需打开配对手机 → 标准 → 恢复连接。不用判断该修复还是重建:gtmux 尽量保留原地址, 只有确认旧隧道已不存在时才新建。地址变了,窗口会显示新二维码并提示重新扫码。 网络或代理报错不会触发换地址。操作完成后会重新检查地址,连接进程有指标不代表手机已能连上。

终端里对应的恢复命令是:

gtmux tunnel --backend cloudflare --service --recover

维护时若只允许修复原隧道,不允许新建,仍可使用 --service --force:

gtmux tunnel --backend cloudflare --service --force

它会重新设置入口、修复代理 CNAME、获取连接 token 并重载服务,保留设备 ID、隧道和配对地址。 需要已有的 Standard 注册,且服务端已支持修复确认。服务商报错或同名 DNS 被其他用途占用时, 命令失败,保留本地服务文件。

--force 不能修复被阻断的 Cloudflare 连接。持续出现 TLS 握手失败时,检查防火墙和代理线路; 代理节点不支持隧道流量,即使 DNS 正常,地址仍然可能连不上。

gtmux attach:在另一台机器的终端里进远端会话

focus 跳到的是本地标签页;attach 把一个远端 pane 开在你当前的终端(Ghostty / iTerm2 / Terminal)里,作为原始、可交互的透传:本地终端变成那个远端 tmux 会话, 走同一个 gtmux serve 面(一个 WebSocket,GET /api/attach)。它只给 owner 和 owner 自己配对的设备用。

# owner — full access with the serve token:
gtmux attach http://<mac>:8765 --token <serve-token> %12

gtmux attach <target>            # omit the pane: auto-attach the only one, else pick
gtmux attach <target> --read-only  # watch only, never send input
gtmux attach <target> --predict    # experimental: hide round-trip lag while typing

分享链接不能打开终端,serve 会拒绝并说明原因。这座桥是把一个 tmux 客户端接到 pane 所在的 整个会话上:拿链接的人会看到主机没分享的 pane,能输入的链接还能借 tmux 切到这台 Mac 的任何 会话。分享链接请用浏览器打开,那里只能碰到主机放行的 pane。用 --code 给的 分享码同样会被拒绝,码不会被用掉,这台 host 也不会记下任何东西。

--predict(实验性,默认关)是预测性本地回显,把 mosh 的想法搬到 WebSocket 桥上。 慢链路上每一次击键否则都要等一个完整往返才回显(跨洲隧道约 340 ms)。开了 --predict, 你自己打的可打印字符和退格立刻出现,带下划线表示尚未确认,权威输出一到就被抹掉; 服务端屏幕永远赢。真实击键原样转发给那个 pane。快链路/局域网上什么都不画,全屏 TUI 里也不画(服务端会告诉客户端这个 pane 进了备用屏幕),任何会改变状态的键(Enter、ESC、 方向键、Ctrl-C、Tab)都会结束这一段预测。客户端的光标位置从服务端学;见 docs/design/mosh-predictive-echo-research.md。

  • <target> 是一个地址加 --token(你自己的,或已配对设备的,完全权限)、这台终端已经 配对过的地址,或者 gtmux pair 给的 …/#c=<码> 配对链接,它会把这台终端配成你的设备。 分享链接(…#code=)或用 --code 给的分享码都会被拒绝,原因见上。
  • %N(可选)是要 attach 的 tmux pane id,它选中的是那个 pane 所在的会话。不给的话, gtmux 从雷达上有 pane 的行里挑(tmux 里的 agent,加上你主动 watch 的普通 pane;原生会话 没有 pane):只有一个就直接接;不止一个时,在 TTY 上给一个带编号的菜单,一个 pane 一行, 所以同一会话里的两个 agent 是两行(每行是会话 · agent · 状态 · 任务;回车取第一行,q 取消)。被管道接走或在脚本里跑(stdin 不是 TTY)时,它打印列表并以 2 退出。一个都没有时, 报「没有可附着的会话」。
  • 退出用 tmux 自己的 <prefix> d,或者 Ctrl-](本地逃生口)。
  • 范围由服务端强制,--read-only 只是本地的方便开关。设计与取舍见 docs/design/remote-attach-research.md。

前提是主机可达:局域网里直连,或者网络允许访问 gtmux tunnel 的地址(WebSocket 和 雷达走同一条隧道)。访客链接在菜单栏里配置(按 pane 的 👁 可见 / ⌨️ 可输入 + 新建链接),或者用 gtmux share。

gtmux pair:接入你自己的设备(全权)

gtmux pair                  # mint ONE one-time code, printed three ways:
                            #   phone QR · browser https://…/#c=<code> · a one-line
                            #   `gtmux attach '<url>/#c=<code>'` for another terminal
gtmux pair list             # your paired devices (guests live under `gtmux share`)
gtmux pair revoke <id>      # cut one device off, effective immediately

pair 是 pair/share 模型里的 owner 轨:接进来的设备就是你,所有会话上完整的可见和 可输入。三种介质兑换的是同一个短命配对码(5 分钟,一次性),进的是同一份可吊销的名册。 终端那一种把 token 持久化到 ~/.config/gtmux/remotes.json(0600),之后裸敲 gtmux attach <host> 就能用;pair revoke 立刻让它失效。gtmux tunnel 开着时链接带 隧道地址,否则是局域网地址。gtmux devices 仍是这份名册的别名。

gtmux devices --push / --forget-push:查看与清理推送 token

gtmux devices --push                       # roster annotated with each device's push
                                           #   token (✓ env·kinds), plus this Mac's own and paused ones
gtmux devices --forget-push <id|orphans|all>  # drop push tokens (host-only)

推送 token 绑在注册它的那台已接入设备上,所以 gtmux devices revoke <id> 本身就停掉了 那台设备的通知。用这台 Mac 自己的 token 注册的,会标明来源,虽然不属于任何设备,照常推送。 有两类 token 会保留、但暂停,什么都不发:

  • 绑定的设备已经不在配对清单里:用 --forget-push <那个 id> 清掉;
  • 没有归属的:既没有设备,也不是用这台 Mac 自己的 token 注册的。这是 token 绑定到设备 之前留下的注册,其中可能有访客的。用 --forget-push orphans 清掉。

暂停的如果是你自己的手机,等手机上的 gtmux 用这台 Mac 仍然认可的凭证重新注册一次就会恢复: App 和这台 Mac 的通知都开着、Mac 连得上时,打开 gtmux 或把它切回前台就行。设备已被撤销的手机 要先重新配对,光重新打开 App 恢复不了。这些 --push 都会列出来。orphans 不会删 这台 Mac 自己的 token;all 全部删除。仅主机可用(本地主 token),远端设备和访客会被拒绝。

gtmux share:给协作者的受限、可吊销访问

gtmux share new --label Alice --view %1,%2 --type %1 --expires 24h
gtmux share set a1b2c3d4 --type %2            # edit ONE link (omitted flags untouched)
gtmux share link a1b2c3d4 [--json]            # 把已有的链接两种形式再给你一次(带二维码)
gtmux share on|off                            # consent master switch for ALL guest typing
gtmux share status [--json]                   # per-link scope summaries
gtmux share revoke a1b2c3d4

share 是 pair/share 模型里的协作者轨:一条最小权限的访客链接。每条链接带自己的范围: 访客可以看哪些 pane(--view)、可以输入哪些(--type,必须是 view 的子集),外加 可选的过期时间(--expires 45m|24h|7d,默认永不过期;过期的链接和被吊销的一样认证 失败)。输入还额外需要主人的同意(share on,默认关)。这一切由服务端强制,网页和 app 只是镜像。

不带 --view/--type 生成的链接会复制当前的全局列表(模板)。历史上的全局写法 (share add/remove、share view add/remove/clear)仍然能用,但会扇出到每一条已有 链接;要改单条链接用 share set。status --json 带每个访客的 view_panes/panes/expires_at,永远不含裸 token,还有谁用过这条链接:last_seen、 platform(Chrome 141 · macOS)、last_ip,有人用过之前都不存在。链接地址在生成时 gtmux share link <id>(或菜单栏那行的复制按钮)随时能把已有的链接再给你一次 (只对全权调用方)。

一条分享链接是什么,两端各自会发生什么

一条分享链接就是一个协作者对这台 Mac 的访问权:能看哪些 pane、能往其中哪几个里打字、以及 一个可选的期限。权限跟着链接走,所以你可以发出去三条,单独吊销其中一条。

链接末尾是一串短码,那串码就是链接本身:

https://tunnel.example.dev/p35047#code=4F7K-Q9X2

发过去,对方点开就行。碰上粘不了的场合(电视浏览器、被锁死的机器,或者你正在电话里),就把 它拆成两行念给对方:

https://tunnel.example.dev/p35047
4F7K-Q9X2

两种走法到的是同一个地方。码和链接同生共死,所以要记的只有一样,要吊销的也只有一样。猜码这 条路在门口就被掐住了:错得太多,那个接口会有一分钟不再回应。

浏览器这一端:他打开链接,或者打开地址、把码输进门口页的那个框。之后凭证就留在这个浏览器 里,明天再来直接就进。他看得到「可见」清单里的那些 pane,在你的总闸开着的时候(gtmux share on)能往更短的那份「可输入」清单里打字。owner 设置和 HQ API 不向他开放;但获准 pane 里的输入能以该程序的权限控制 shell 或 agent,pane 清单不是系统权限沙箱。

终端这一端做不到:gtmux attach <链接>,或者链接是念出来的、写成 gtmux attach <host> --code 4F7K-Q9X2,都会被拒绝,因为终端碰到的是整个 tmux 会话,而不只是这几个 pane。请把链接 发给对方,用浏览器打开。

gtmux share revoke <id> 两端一起断:浏览器下一次请求就退回门口页,旧版 gtmux 为这条链接 存下的 token 也会立刻失效。到了期限也一样,只是时间由期限说了算。你其他的链接不受影响。

浏览器存的东西可能会丢:清了网站数据、开了无痕、换了个浏览器,或者 Safari 那条「一周没人来 就清掉站点存储」的规则。对方重新打开那条链接就回来了,不用你再做什么。终端把 token 存在 ~/.config/gtmux/remotes.json 里,你不吊销、他不删,它就一直在。

以前发出去的老链接照常可用,它们带的是那串长 token,不是短码,打开的是同一份访问权。

安装与配置

安装与更新命令

命令用途
gtmux doctor [--fix [--yes] | --bundle]体检这台 Mac,然后把缺的配上
gtmux install [hooks|app]安装 agent hook 或菜单栏 app
gtmux uninstall [hooks|app]把 gtmux 装过的东西撤掉
gtmux update [--check]更新 gtmux,CLI 和菜单栏 app 一起
gtmux app打开菜单栏 app

gtmux doctor --progress 在非交互输出中也显示检查进度;--bundle 收集诊断。安装步骤见安装文档。

命令行更新成功后,由新版程序运行只读检查 doctor --no-prompt --progress, 最多等待 90 秒。发现环境问题不会把安装成功改报为更新失败。 菜单栏更新后,由重启的 App 检查一次并保存结果,每个版本只自动检查一次。 从齿轮菜单的「运行环境检查」或偏好设置 → 诊断可查看报告、重新检查。 点击「查看修复方案…」会在终端打开 doctor --fix,逐项说明并确认操作;完成后重新检查。

doctor --json 返回 schema 1、CLI 版本、checked_at(Unix 秒)、分组检查项 (status、label、value、note,以及可选的 description)及各状态数量。 description 说明检查内容,note 保留技术详情。JSON 中的名称面向用户展示,不是固定的机器标识。 状态为 ok、recommended、blocking、info;存在阻塞项时退出码为 1,报告仍完整返回。 --json、--no-prompt 不能与修复或打包参数组合。 doctor --fix --terminal 在终端打开交互修复,不能搭配 --yes;打开终端不代表修复成功。

检查是否完成,与结果分类是两回事:

状态含义
ok:正常已满足这项检查的条件。
recommended:建议处理配置或维护项可改进,部分功能可能受限。
blocking:影响功能问题会影响相应功能,检查退出码为 1。
info:信息供参考的状态,可包含未启用或未核实项,不计入需关注事项。

检查结束会明确显示「检查已完成」,即使仍有建议项。重新检查读取当前状态, 不会自动修复。终端报告末尾列出全部影响功能项和建议项,优先显示影响功能项, 保留所属分组、当前结果及处理说明。修复后也用同一清单汇总剩余事项; 有建议项不等于修复失败,部分事项需要在 doctor --fix 之外处理。

Mac 报告末尾也有「需要关注」汇总,顶部和底部可直接跳转过去。 选择事项会定位、高亮原检查项并展开完整说明;信息按钮可查看四类结果的含义。

检查项展开后只展示一次结果,补充说明放在下方。长结果可展开为全文,同一段摘要不再重复出现。

gtmux awake:合上盖子也继续跑

gtmux awake on       # 请求管理员授权,然后读回确认生效
gtmux awake          # 查看状态、已开启时长和电源
gtmux awake off      # 请求守卫恢复睡眠;通常不用密码

合上 MacBook 的盖子系统就睡了,隧道断掉,每个 agent 冻在回合中间。gtmux awake on 让 Mac 合着盖子也不睡(旧名字 gtmux server-mode 仍然能用)。开启需要在 Mac 上进行 管理员授权。要让 gtmux serve 持续运行:它的心跳告诉守卫仍有服务需要保持;agent 完成任务本身不会结束服务器模式。

正常关闭只写一个免特权的请求,由守卫恢复睡眠;CLI 最多等八秒,也可能返回「已请求关闭」 而尚未完成。守卫缺失时,CLI 会改为请求管理员授权来恢复睡眠,内核读回睡眠已恢复才算成功, 否则报失败。读不到内核的睡眠设置时,状态是 unknown:不说开也不说关,保留 gtmux 的记录 和关闭请求,也不当成失效。

同一次授权里会装下一个 root 属主的小守卫。它唯一的权力是把睡眠还回来。以下条件会触发 恢复尝试:

触发意思是
你把它关掉写下免特权标记,由 launchd 的路径监视唤醒守卫
用电池时电量掉到 20%守卫每 30 秒检查;serve 在 30% 时请求桌面提醒
serve 心跳停止心跳超过 120 秒未更新,且已过开机宽限期时,触发恢复
重启后没人登录有五分钟开机宽限期,让按用户运行的服务有机会在登录后恢复

守卫读回确认睡眠已恢复后才删除自己;恢复失败或读不到结果时,会保留请求,留待下一轮 重试。守卫结束服务器模式时,除非是你在这台 Mac 上自己执行的 gtmux awake off(包括 菜单栏开关),gtmux serve 会在下一次慢节拍发现,并发一条提醒写明原因;发现得晚的话 还会写上时间。是不是你自己关的,按时间判断:在你执行之后十分钟内由守卫完成的关闭才算; 恢复拖得更久的,仍可能收到提醒。(刚升级后,升级前就有、超过一小时的结束记录不会补发。)这条提醒和 30% 的预警都是尽力送达,需要菜单栏 App 运行且通知已开启。手机预警和结束通知 仍是规范要求;服务器模式尚未接通这些手机推送。

没有按时长自动过期的计时器。 它会持续到你关闭或护栏触发结束;开启期间,菜单栏图标 带着一个缓慢呼吸的红点。

用电池是被支持的场景:合着盖子在房间之间走动照样工作。终结它的是剩余电量,拔掉适配器 不会。

状态命令遇到没有 gtmux 所有权记录的 disablesleep,会报告并给出手动撤销命令,不改动 设置。gtmux doctor 呈现同一个发现,在从没碰过这个设置的机器上保持沉默。

状态从哪儿读:

来源能不能用
pmset -g / -g custom / -g live❌ 两种状态下都从不报告 disablesleep
电源管理 plist⚠️ 落后于写入;它回答的是「重启后还在不在」
ioreg -r -c IOPMrootDomain → SleepDisabled✅ 实时、不需要特权的真相

gtmux awake --json 报两种读数,外加 owned_by_gtmux、guard 和一个 platform 结论。在项目没有验证过的 macOS 上,on 会直说;机制根本不存在的地方,它在要密码之前 就拒绝。guard.healthy 只表示守卫的两个文件都在,不等于已经确认 launchd 正在运行它。

两条边界:

  • gtmux serve 是按用户的 LaunchAgent,重启之后要有人登录它才会起来。在一台开了 FileVault、没人在场的 Mac 上,心跳不会恢复,睡眠会被还回去,所以服务器模式扛不过 一次无人值守的重启。gtmux 不会去动 FileVault 或自动登录来「修好」这件事。
  • 底层那个设置苹果没有文档。已有真机记录来自 M4 Pro、 macOS 26.5.2。CLI 的 platform.verified 把 macOS 26.x 归在一起,不代表所有机型和系统 配置都已实测。运行时检查发现设置或读回机制不可用时,on 会拒绝。

手机界面只展示状态,没有开关。owner 可以通过 POST /api/awake 免密码请求远程关闭;成功回包只说明请求已写入,不等于睡眠已经恢复。重新开启仍需在 Mac 上授权。

gtmux whatsnew:对你来说变了什么

gtmux whatsnew                 # everything newer than the version you're running
gtmux whatsnew --since v0.36.0 # from a specific version
gtmux whatsnew --all           # 发布接口返回的最近 30 个版本中的说明

每个版本里写给用户的那几行。gtmux update 装完之后打印开头几条,这里显示发布接口 返回的最近 30 个版本中符合版本范围的完整说明,不会翻页读取更早的发布。 来源是发布 tag 信息里的一个 user: 块,goreleaser 会把它拷进 release 正文。可选的 user-zh: 孪生块用中文写同样的内容:

更新成功后,命令行还会提醒运行 gtmux doctor 检查本机配置。 gtmux update --check 只检查新版本,不运行 doctor。

git tag -a v0.40.0 -m "v0.40.0 — …

user:
- spawn --title now names the session
- restore returns you to the window you were on

user-zh:
- spawn --title 现在会命名会话
- restore 会把你带回原来的窗口
"

gtmux update 和 gtmux whatsnew 都打印与你语言匹配的那一块(GTMUX_LANG):中文 优先 user-zh:,英文优先 user:,某个 tag 只写了一块时另一种语言回退到它。两块先后 顺序随意,每一块在空行、标题行或另一块的标记处结束。两种块都没有的发布不贡献条目。

gtmux config:少数几个不按次给的设置

gtmux config agent-proxy [<url>|off]   # proxy applied when gtmux LAUNCHES an agent
gtmux config tab-alert  [on|off]       # mark the terminal TAB of a session that needs you
gtmux config lang       [en|zh|auto]   # machine-level output language (auto = system locale)

不带参数调用时,每一条都打印当前值。

lang:让每个 gtmux 进程说同一种语言

launchd 起的 gtmux serve 和 hook 子进程既没有 GTMUX_LANG 也没有你 shell 的 locale, 没有机器级选择的话,它们发出的唤醒后缀和桌面通知可能和你终端里的语言不一致。 gtmux config lang zh 给它们一次性做了选择;auto 跟随系统 locale。按进程的 GTMUX_LANG 和按次调用的 --lang 仍然优先。

tab-alert:不用挨个点开就找到对的标签页

tab-alert on 会在任何有 agent 在等待的会话的标签页标题前面放一个 ●。

  • 只有 waiting 会标;working 和 idle 永远不标。
  • 标题由 tmux 渲染、终端只负责显示,所以 Ghostty、iTerm2、Warp、Apple Terminal 上都 一样有效。它是一个字形,因为给标签页上色是各家终端各自的能力。
  • 保留你的标题格式。打开时它读你的 set-titles-string 存下来,只在前面接上自己那一段; off 原样还原。如果你在这期间改过格式,off 会拒绝覆盖你的修改,那种情况下请自己 把开头的 #{@gtmux_alert} 删掉。
  • 由 agent 自己的 hook 事件驱动:agent 一报告等待,标记立刻落下,serve 的节拍做兜底 对账。HQ 不在这个回路里。
  • 也可以在菜单栏 app 的偏好设置 → 通知里开关。

debug:追问题的时候多记一点

gtmux config debug            # 现在在记什么
gtmux config debug on         # gtmux 的每个部分都写 debug 条目
gtmux config debug serve,hook # 只记这几个
gtmux config debug off        # 回到常规条目

它写在 config.json 而不是环境变量里,因为真正需要调高的那几个进程,shell 够不着: launchd 拉起的 serve、隧道客户端、hook。CLI 进程下次启动时生效;菜单栏 app 在切换设置 或读取日志统计时更新自己的开关。gtmux logs --stats 会说它是不是开着,菜单栏里的「多记一些细节」就是这个设置。

hqWake:调 HQ 的唤醒通道

~/.config/gtmux/config.json 里 "hqWake" 下面手写的键(全部可选;缺失或非法的键 保持默认)。每一项管什么,写在对应行为所在的地方: 唤醒通道、消费水位、 自轮换:

键默认管什么
done"unattended"done 唤醒模式:unattended | always | tick
paneMinGapSec120done 唤醒的按 pane 合并窗口(秒)
tickMinutes10摘要节拍的最小间隔
tickBurst5攒到多少个结果就提前触发节拍
unreadDebounceSec120未消费事件要压多久才发一次 unread 敲门
unreadRepeatSec300水位不动时 unread 的重敲间隔
selfRotateCtx0.75上下文占比的越界线(0 关闭)
selfRotateHours12会话年龄的越界线(0 关闭)
selfRotateTurns300HQ 回合数的越界线(0 关闭)
selfRotateRepeatSec1800越界持续期间的重敲节奏
selfRotateFloorSec43200一个没有变化的越界最长能沉默多久
selfRotateCheckSec300自轮换传感器多久评估一次

tmux 集成

gtmux 就是一个 CLI,你在 tmux.conf 里想绑什么键都行。推荐:

set -g set-titles on
set -g set-titles-string '#S — #W'
bind g run-shell -b "gtmux overview --popup"
bind a display-popup -E -w 80% -h 60% "gtmux agents --watch --popup"
bind J run-shell "gtmux focus --last"

gtmux status:把舰队放进 tmux 状态栏

set -g status-right '#(gtmux status)'

一小段带色的计数(●2 ✓14)给 tmux 状态行用,你本来就有的那条栏会告诉你谁需要你。 --plain 去掉 tmux 的颜色转义,给别的状态栏(或 shell 提示符)用同样的数字。

把 pane id 放进标签页标题(可选)

gtmux 在每一块屏上都用 tmux 的 id(%23)称呼一个 pane,gtmux focus %23 直接吃这个 id。两行配置能让这个 id 在另一边也可见,app 里的一行和你终端里的一个标签页说的就是 同一个东西:

set -g automatic-rename-format '#{b:pane_current_path} #{P:#{pane_id} }'
set-hook -g pane-exited 'set-window-option automatic-rename off ; set-window-option automatic-rename on'

窗口名于是会列出那个窗口里的每一个 pane(gtmux %23 %24),因为 set-titles-string 是 #S — #W,标签页会继承它。标题格式没变,focus 的标签页匹配不受影响。

  • #{P:…} 遍历的是这个窗口的 pane,所以名字不跟随焦点(原因见 TROUBLESHOOTING)。

  • 那个 hook 是必需的:加一个 pane 会立刻重算窗口名,关掉一个不会。

  • 在分屏窗口里,如果你还想让每个 pane 在屏幕上也戴着自己的 id:

    set -g pane-border-status top
    set -g pane-border-format ' #{pane_id} #{pane_current_command} '

    doctor 不会建议这一条:pane-border-status 默认是 off,打开它的代价是每个分屏里 每个 pane 永久占掉一行屏幕。

  • gtmux doctor 会报这一行,--fix 会提议。如果你已经有自己的 automatic-rename-format(哪怕里面带着前台命令),--fix 会把 id 追加上去;只有 tmux 自带的默认格式才会被替换。gtmux 不会重命名你的窗口: rename-window 会把那个窗口的 automatic-rename 关掉,并永久覆盖你的格式。

让打印出来的链接可点(可选)

程序把超链接打印成 OSC 8 转义,tmux 只把它转发给声称支持这个能力的终端。默认没有终端 声称,链接就渲染成纯文本。

set -as terminal-features ',*:hyperlinks'
  • 需要 tmux 3.4+(terminal-features 从 3.2 起就有,hyperlinks 这个 feature 是 3.4 才有);更老的 tmux 上这一行是启动错误,所以 doctor 在低于它的版本上不建议这一条。
  • 只有生效之后打印的输出才带链接。
  • Ghostty 和 iTerm2 都处理 OSC 8。链接还是点不动的话,下一个嫌疑是终端自己的点击 修饰键(有些要 ⌘ 或 ⌥ + 点)。

给普通 pane 一个值得读的标题(可选)

agent 会自己写 pane 标题(一行 agent 读作 整理这周的发布清单和回归结果);shell 什么都不写,gtmux 回退到命令名,每一行普通 pane 都读作 bash。两个 shell hook 能解决: 跑命令时标题是那条命令,回到提示符时是目录名。

# bash — in the file your LOGIN shell reads (see below)
if [ -n "$TMUX" ]; then
  trap 'printf "\033]2;%s\007" "$BASH_COMMAND"' DEBUG
  PROMPT_COMMAND='printf "\033]2;%s\007" "${PWD##*/}"'"${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
fi
# zsh — ~/.zshrc
if [ -n "$TMUX" ]; then
  autoload -Uz add-zsh-hook
  gtmux_pane_title_preexec() { print -rn -- $'\e]2;'"$1"$'\a' }
  gtmux_pane_title_precmd()  { print -rn -- $'\e]2;'"${PWD:t}"$'\a' }
  add-zsh-hook preexec gtmux_pane_title_preexec
  add-zsh-hook precmd  gtmux_pane_title_precmd
fi
  • tmux pane 跑的是登录 shell,登录 bash 读的是 .bash_profile / .bash_login / .profile 里第一个存在的那个,从不读 .bashrc,所以 bash 版本要写进那三个之一。 doctor --fix 会挑你的 shell 真正会读的那个文件,而且只往已经存在的文件里追加 (在你用 .profile 的机器上创建一个 .bash_profile 会把它盖住)。
  • 用 add-zsh-hook;定义裸的 preexec() 会替换掉你(或 oh-my-zsh)已有的。
  • 两者都用 $TMUX 把关:在 tmux 之外这会和终端自己的标签页标题打架,而 gtmux 正靠 那个标题跳转。
  • gtmux doctor 数有多少个普通 pane 的标题说出了点什么,不管是哪个 hook 产生的。 只对新开的 shell 生效,已经开着的 pane 保持原标题。

通知 hook

⏸ waiting、✓ latest 和点通知跳转都依赖一个把状态文件写到 ~/.local/share/gtmux/ 下的 hook。gtmux 内置了这个 hook:

gtmux install                       # asks: hooks | app | all
gtmux install hooks                 # Claude, one-time setup (macOS)
gtmux install hooks --agent codex   # or cursor|gemini|copilot|kiro|opencode|kimi
gtmux uninstall [hooks|app|all]     # reverse it (asks when no target)

gtmux install hooks 把 gtmux hook 注册到 ~/.claude/settings.json 的回合、授权、 会话、压缩及工具事件上(幂等;保留其它 hook,给文件留备份)。 gtmux hook 是产出方(由 Claude Code 来跑,你不用跑)。通知载荷区分授权请求和空闲提醒; 没有通知类型时走旧版路径。hook 也会读取提示或回复内容,写入事件日志和支持的对话状态。

其它 agent:--agent codex|cursor|gemini|copilot|kiro|opencode|kimi 改成接那个 agent 自己的 hook 文件。Codex 用的是可叠加的 hooks 系统(~/.codex/hooks.json + features.hooks),所以和你已有的 notify(比如 computer-use)共存。opencode 没有 命令 hook 文件,gtmux 装一个转发它事件的小 JS 插件(~/.config/opencode/plugins/gtmux.js)。 Kimi Code 的 hook 是你自己 ~/.kimi-code/config.toml 里的 [[hooks]] 条目,gtmux 只在 文件末尾追加一整块带标记的内容,其余一个字节不动;卸载也只删这一块。块的首尾标记对不上时(比如 手动删掉了一个),gtmux 什么都不改,只告诉你要修哪一行。 gtmux doctor 检查已存在的八款 agent 的 hook 配置,--fix 可补齐事件、修复绑定和更新桥接插件;先备份,保留其它 hook。Codex 和 Kimi 的用户中断会结束工作状态,不发送完成通知。Kiro 兼容 CLI 2.x profile 和 V3 全局 hook。配置升级后重启相应 agent;Codex 在 /hooks 中检查并信任变更。详见 hook 契约与边界。

菜单栏 app 装在 ~/Applications 或 /Applications 都能识别;已经装好的系统级 app 不会再被 doctor --fix 提示重装。

doctor 需要读取运行中的 tmux 设置。读取失败时会显示连接错误,--fix 会在改动前 停止;请启动 tmux,或在能访问其 socket 的终端重试。配置已保存但未在当前 tmux 生效时,会报出失败命令并返回非零退出码。结尾的未通过项会带上当前值和处理建议, 其中可能有跳过的配置步骤,也可能有需要 HQ 实际完成的工作。

通知由菜单栏 app 投递,不需要 terminal-notifier。hook 把请求排到 ~/.local/share/gtmux/notify/ 下,Gtmux.app 弹一条原生横幅(显示为 Gtmux,带 agent 图标和一个 Jump 动作;「跑完了」安静无声,「需要你输入」会响)。点它就落到那个确切的 pane 上。第一次运行时允许通知,并让 app 一直开着才收得到。

备份 HQ 的档案

HQ 的档案(整个 HQ 目录)是 gtmux 手上唯一不可再生的东西:态势板、知识库,以及一份 种一次、永不覆盖的 LOCAL.md(丢了不会自愈)。

gtmux hq --export ~/gtmux-hq.tar.gz   # the whole records folder as one LOCKED file (asks for a passphrase → .tar.gz.age)
gtmux hq --import ~/gtmux-hq.tar.gz.age   # put one back (asks for the passphrase)
gtmux hq --export ~/gtmux-hq.tar.gz --plain   # the unlocked form

gtmux hq --records [--json](--memory 是旧写法)说档案有多大、备份线索和上次导出。 导出记录不能证明文件已经离开这块盘。菜单栏的阅读器显示同一行、来自同一条命令,两块屏不会对同一个数字说 不同的话。

导出的是普通 tar.gz,用 age 格式加口令上锁(1.0.21 起), 任何 age 工具都能解开。锁加在归档上,Mac 上的知识库本身照旧不动。 口令在终端里输两次、不回显、至少 8 位;脚本用 GTMUX_HQ_PASSPHRASE,app 用 --passphrase-stdin 从标准输入第一行递,永远不走命令行 参数(那儿 ps 看得见)。口令丢了文件就打不开:gtmux 不留副本。--plain 写不上锁的 tar.gz。--import 看文件头就知道是哪种,需要时才问口令,口令不对什么都不动。 --records 还会说上次导出是什么时候、有没有上锁;菜单栏读窗口在一张表单里问口令, 可以记进 Mac 的钥匙串。

--import 绝不就地覆盖:已有的档案会被挪到旁边一个名为 hq.replaced-… 的文件夹,路径会打印出来。

gtmux serve 每天快照到 ~/.local/share/gtmux/hq-snapshots/,保留 14 份,只在档案 真的变了时才写。快照放在状态目录里,在 HQ 目录之外。

快照挡的是误操作,挡不住硬盘坏掉。 它们和数据在同一块盘上。gtmux doctor 的 HQ 档案 那行会说有多少东西、积累了多久和备份线索;导出到同一块盘的文件仍要另行复制出去。

权限

gtmux 只要它需要的:

  • 自动化(控制你的终端:Ghostty / iTerm2 / cmux / Warp),focus / restore / new 和 点通知跳转需要它。gtmux 第一次通过 AppleScript 驱动终端时 macOS 会弹窗,点允许。
  • 通知,菜单栏 app 才能弹 agent 横幅。第一次运行时允许。
  • 开机自启(可选),只在你到偏好设置里打开时才要。

下面这些它不需要;macOS 要是弹了,可以放心拒绝,不损失任何功能:

  • App 管理(「修改你 Mac 上的 app」)。gtmux 从不修改别的 app,它的代码只碰自己的 bundle(更新/卸载时)。这个弹窗可能出现在 macOS 通过 responsible-process 链把另一个 app 的自更新归到 gtmux 那个长跑的后台进程上时。拒绝对 gtmux 没有影响。
  • 文件与文件夹(下载 / 桌面 / 文稿)。gtmux 不读这些。弹窗可能出现在 restore 重建 一个工作目录恰好在其中之一的 tmux 会话时,那是 tmux(由 gtmux 启动)在打开那个 文件夹。可以放心拒绝。
事件与自动化

gtmux events:会话事件流(订阅)

22:50:40  working          api:0.0        Claude Code (%7)
22:51:02  waiting·permission  api:0.0     Claude Code (%7)
22:53:19  idle             web:1.0        Codex (%11)

hook 把每个会话的生命周期事件(开始 / 结束 / 等待 / 后台)追加进一个会轮转的日志;工具跑完但没有解除任何等待的那种不记,它只是遥测,不算新情况 (~/.local/share/gtmux/events.jsonl,活动 20 MB + 1 个轮转 ≈ 40 MB 上限,配置项 eventsCapMB,0 关闭)。gtmux events 打印最近一小时;--since 10m|2h 给一个 时间窗;--follow 先打印这一小时(或 --since 给的窗口,--since 0 只看新事件), 再逐条流式输出新事件,认得轮转。--since-seq N 是一次性的增量读取 (留存记录中序号大于 N 的部分,最旧的在前,可与 --severity/--json 组合)。它没有 上界,唤醒之后新来的事件也可能读到;带 --since-seq 时,--since 和 --follow 都不改变 这次一次性读取。App 的 /api/events 提供雷达变化信号和提醒,不回放这份生命周期日志。

从 HQ 根目录或它的子目录发起、不带过滤的 --since-seq 读取,在没有序号断档、起点也 没有跳过原水位时,把水位推进到这批留存增量的末尾(见消费水位)。--ack N 显式回写水位,用于那条流以别的方式对上账的场合,比如一次完整的 gtmux digest。 两者都认 HQ 根目录及其子目录(notes/、knowledge/);从别处读不会推进 HQ 的水位。 --ack N 只往前推,不回退,也不超过日志已经分配的序号。出现断档告警时水位不动, 先对账,再回写已经核对过的位置。

从 HQ 根目录拉增量时,略掉那些从来不计数的记录(HQ 自己 pane 的行、没有 pane 的闪烁、 gtmux 的 gtmux:audit:* 轨迹),并在 stderr 上说明扣掉了多少条。--all 拿回原始 视图;两种不过滤的形态都算消费。子目录读取也算消费,但目前显示原始视图。 HQ 目录以外的读取同样显示原始视图,但不算消费。

--severity <tier> 过滤出那一档及以上。档位排的是紧急度,所以它们是三种不同的读法: 不带过滤的 --since-seq 增量用来对账;--severity notable 是舰队变化流(一条指令 抵达某个会话,origin:"instruction",加上回合结束和生命周期);--severity important 是升级子集(卡住 · 在问 · 崩了),用来先分诊。

--acts 只留 HQ 自己做的事(派活、回收、记知识、轮换、自检、蒸馏),去掉唤醒的投递 记录(wake-delivered / wake-dropped)。这跟手机「HQ 的工作」一节走的 GET /api/hq/events?acts=1 是同一道分割,菜单栏「HQ 做了」那一行数的也是这一批; 「HQ 今天干了什么」就是 gtmux events --since 24h --acts。和所有过滤读法一样, 它不算消费,加不加 --all 都一样。配 --since-seq 时照样列出这些动作,虽然普通的增量读取 会把它们当作 gtmux 的审计留痕藏起来。

这条流里还带着 gtmux 自己的控制记录,也就是它替 HQ 发起的周期维护触发,渲染成 [CONTROL <event>] 并带上理由:

09:57:16  [CONTROL gtmux:self-check]  due (daily) — review feed/ledger/memory health…
04:33:49  [CONTROL gtmux:distill]     due (weekly) — distil the period into the KB…

这些记录只表示发出了请求。HQ 完成后,从自己的根目录运行 gtmux hq --maintenance-done distill 或 gtmux hq --maintenance-done self-check。 蒸馏回执要求候选队列已清空。完成时另记一条带 kind、request_at 的 gtmux:maintenance-completed 事件和一条 act.hq.maintenance 诊断记录。 gtmux doctor 与 gtmux capture --list 会把未回执的请求显示为待完成; 只有触发记录不能证明 HQ 已经做完。

来源:docs/cli.zh.md(v1.0.114)· 在 GitHub 上编辑

← ccy.dev