Skip to content

CLI Reference

Location route summaries

agentwatch location and agentwatch location --json retain their ordinary full output. Opt in to a bounded, compact report:

agentwatch location --summary --window today --json
agentwatch location --window 2026-09-21 --place "Test Park" --no-geocode

--summary defaults to today; --window or --place implies summary. Windows accept today or YYYY-MM-DD. A day starts at local midnight and ends exclusively at the next midnight, with DST respected. The newest fix's offline timezone is used for the entire window; if unavailable, the report explicitly uses UTC. Today is based on the current time in that zone, not the date of a stale fix. Future fixes and pairs crossing the window boundary are excluded without interpolation. Retention may cover only part of a day: the report never claims complete coverage.

The JSON response contains summary and attribution, with compact fixes and segments arrays described by their *_columns keys. Fix rows retain local timestamps, coordinates, age in seconds, effective accuracy and its source. No full retained trail or out-of-window current fix is included.

sampled_distance_m sums straight chords between accepted fixes no more than 20 minutes apart. walking.sampled_distance_m includes only chords at 2–<8 km/h: this is walking pace, not proof of walking. Distance must exceed twice the summed endpoint accuracy; missing/invalid accuracy assumes 100 m, and reported accuracy is floored at 5 m. Speeds over 1100 km/h are rejected. Noise and rejected pairs contribute nothing; zero sampled distance does not prove zero movement. gap_displacement_m separately sums above-noise chords across longer silences and never counts toward sampled walking.

The sampled walking chord range sums endpoint accuracy radii. It is a geometric sensitivity range, not a statistical confidence interval or an upper bound on the actual path. Actual walking mode is unconfirmed and the actual walking range remains [0, null] (no finite upper bound). A narrowly recognized gap followed by a walking-pace return to the starting accuracy circle may include round_trip_inference: twice the sampled return, clearly labelled hypothetical and low confidence. A synthetic 470 m sampled return can therefore yield a 940 m hypothesis, never a 940 m measurement. No inference is added to either measured total.

--place matches a unique configured location.places circle by name, case-insensitively. Missing or ambiguous names are unsupported. Configured circles classify fix accuracy circles as inside, outside or uncertain, and report walking-pace chords whose endpoints are inside. They cannot prove the path stayed inside or establish a named geographic feature's boundary: feature walking distance stays null and classification is uncertain. Window-scoped coordinates and segments support further reasoning.

API equivalent: GET /api/location?summary=1&window=today&place=Test%20Park. window or place also implies summary; invalid windows return HTTP 400. Existing n can still cap the upstream retained fixes, reducing coverage. No new external map lookup is performed. The existing Nominatim lookup names only the last fix within the requested window, if any; its coordinates leave the box unless --no-geocode, geocode=0, or configured geocoding off is used. Geocoding data © OpenStreetMap contributors.

Is the operator watching (agentwatch human)

agentwatch human            # verdict first, then the detail
agentwatch human --json     # the whole report
agentwatch human --session craft   # ask about another session

Run inside a session, it asks about that session (the name comes from the session's env markers). The first line is the verdict: watching_this_session, watching_other_session, open_in_background or away. The detail lists the open viewers (device, session, foreground or background tab), the last input and the last sighting, and whether a notification would reach the operator — including silenced_now, true when a push sent now is held back because the operator is already watching.

"Watching" is live to within the WebSocket heartbeat. Last input and last seen are kept in memory and reset when the gateway restarts; the report's since says from when. API equivalent: GET /api/human?session=NAME.

Complete reference for all agentwatch commands.

Synopsis

agentwatch [command] [options]
agentwatch [--text | --json | --png] [options]

Global Options

Option Description
--version Show version and exit
--version-json Show version as JSON and exit
--help, -h Show help message

Commands

Service Management

start

Start the agentwatch daemon.

agentwatch start [options]
Option Description
-f, --foreground Run in foreground (don't daemonize)

Examples:

agentwatch start          # Start in background
agentwatch start -f       # Start in foreground (for debugging)

stop

Stop the agentwatch daemon. This is a cold stop: any live web-viewer logins are dropped, so the next start requires a fresh login. The Cloudflare tunnel is also torn down.

agentwatch stop

restart

Restart the daemon in place, preserving continuity. Unlike a stop followed by a start, restart is a deliberate, targeted operation that:

  • keeps you logged in to the web viewer (live logins are snapshotted and restored by the incoming daemon), and
  • hands off the Cloudflare tunnel to the new daemon instead of dropping it, so remote/web connections survive.

Prefer restart for routine restarts (after a code or config change). Use stop + start only when you specifically want a clean slate — e.g. forcing re-authentication. A crash or reboot behaves like stop + start: you log in fresh. Login continuity is bounded by the normal 24h session expiry; it is never extended by a restart.

agentwatch restart

status

Show daemon status and all monitored sessions.

agentwatch status [options]
Option Description
--config FILE Config file path

Output:

  • Daemon status (running/stopped)
  • Tunnel URL (if configured)
  • List of all sessions with program/state

Configuration

setup

Interactive setup wizard.

agentwatch setup [options]
Option Description
--token TOKEN Device token (skip prompt). - reads it from stdin, which keeps it out of argv and shell history, and runs the rest of setup non-interactively
--yes Non-interactive with defaults
--start Start daemon after setup
--strict CI mode: fail fast on errors

Examples:

agentwatch setup                                  # Interactive
agentwatch setup --token - --start < token-file   # Token on stdin (an agent, a script)
agentwatch setup --token TOKEN --yes --start      # Non-interactive
agentwatch setup --token TOKEN --strict           # CI/automation

A token piped from another box arrives the same way: ssh newbox 'agentwatch setup --token - --start' < token-file.

config

Configuration management.

agentwatch config show
agentwatch config set <key> <value>

Subcommands:

Subcommand Description
show Show current configuration
set token VALUE Set device token

Examples:

agentwatch config show
agentwatch config set token aw_abc123...

tunnels

List available tunnels.

agentwatch tunnels [options]
Option Description
--config FILE Config file path

Capture

One-Shot Capture

Capture terminal without daemon (requires tmux session).

agentwatch [options]
Option Description
--text, --txt Output full terminal text
--json Output JSON with detection
--png Output PNG screenshot
(none) One-line status output

Examples:

agentwatch                    # One-line status
agentwatch --text             # Full terminal content
agentwatch --json             # JSON with detection results
agentwatch --png > snap.png   # Save PNG screenshot

capture

Capture a specific session from the daemon.

agentwatch capture <session> [options]
Argument Description
session Session name to capture
Option Description
--text, --txt Full terminal text
--json JSON with detection
--png PNG screenshot

Examples:

agentwatch capture coding                 # One-line status
agentwatch capture coding --text          # Full terminal
agentwatch capture coding --png > s.png   # Screenshot

Logging

logs

Extract and display daemon logs.

agentwatch logs [target] [options]
Argument Description
target Session, viewer, or session/viewer pair
Option Description
--list List all sessions and viewers
-f, --tail Follow logs in real-time
-n, --last N Show last N entries
-c, --captures Include capture file contents
--captures-only Show only captures
--no-lifecycle Skip session lifecycle info
--log FILE Custom log file path

Target formats:

  • SESSION - All logs for session
  • SESSION/VIEWER - Specific session+viewer pair
  • /VIEWER - All logs for viewer

Examples:

agentwatch logs --list              # List all sessions/viewers
agentwatch logs coding              # Logs for "coding" session
agentwatch logs coding/pig          # Logs for session+viewer pair
agentwatch logs coding --tail       # Follow logs
agentwatch logs coding -n 50        # Last 50 entries
agentwatch logs coding --captures   # Include capture contents

Updates

update

Check for and install updates.

agentwatch update [options]
Option Description
--check Check only, don't install

Examples:

agentwatch update          # Update to latest
agentwatch update --check  # Check without installing

rollback

Restore previous version from backup.

agentwatch rollback

Sessions

session

Create and manage coding sessions through the daemon. Each subcommand talks to the daemon's dual-auth /api endpoints (session cookie or the Bearer token at ~/.config/agentwatch/tokens/api). All subcommands accept --json for machine-readable output.

agentwatch session <subcommand> [options]
Subcommand Description
new --dir DIR Create a session. Options: --agent claude_code\|codex\|shell, --name NAME, --task '...' or --task-file FILE (a UTF-8 brief, read verbatim), --permission-mode MODE (agent-aware: claude_code → default\|acceptEdits\|plan\|bypassPermissions; codex → auto\|read-only\|full-access; ignored for shell), --account NAME (which registered account to launch on: a Claude account for Claude, a Codex account for Codex, an OpenRouter key for either; omit it and the first account with quota left is used; login is this box's own Claude login, refused once that login has ended or while it ends within the hour, with how to renew it; an account at a quota alert step is refused for a new session — its 5-hour window at or below the policy's 5-hour step (10%), or its weekly window at or below the no-subagents step counted from its reserve (2% + reserve) — and when Automatic finds only such accounts it refuses rather than use an OpenRouter key, naming each account and its step), --force (with --account: launch on that account even below its reserve, hard or soft, or at a quota alert step; an empty account is still refused; the operator naming an account in the web viewer is never held to either), --unattended (Claude Code only, needs --task or --task-file: a claude -p run with nobody at the keyboard — see Unattended runs), --max-turns N and --max-budget-usd USD (with --unattended: claude -p's own limits; on a Claude API key the budget is never more than the credits left), --read-only (with --unattended and a Claude API account: strict Read/Grep/Glob review; no shell/tests/writes/MCP), --once (with --unattended: close after saving and mailing the first result), --wait (with --unattended: block until the first task ends, print its result, cost and turns; exit 1 if it failed), --resume CONVERSATION (not with --unattended: continue a conversation in the new session with its full context. Claude Code: a Claude conversation id — the transcript's file name, or a run's conversation in agentwatch history; Claude starts in the conversation's own worktree when it ran in one; refused while a live Claude has that conversation open. Codex (--agent codex): a Codex thread id, resumed with codex resume <thread>; on another Codex login than the one it ran on (--account), its rollout is copied into that login's Codex home first; a read-only or full-access resume runs on Codex's standalone TUI, because the remote TUI resumes a thread in the folder's default mode; refused while a live Codex session has the thread loaded or open, when none of your Codex homes holds its rollout, and on an OpenRouter key)
respond NAME Send input. --keys (answer a prompt with keystrokes) or --message "..." (type text and submit, same as tell)
tell NAME Send a message to a session; sugar for respond --message
show NAME Show one session's status and common actions
capture NAME Capture the session's terminal
parse NAME Parse session output (--verbose for full detail)
status [NAME] Show status for one session or all
list Active sessions and recent closures (24 hours), with elapsed timing. --active hides history; --include-closed includes history in JSON too. --closed [--days N] [--dir PATH\|REPO] lists closed sessions only, one row per conversation with its topic, for the last N days (1 to 14, default 1), and with --dir only the conversations at or under that folder or repo; see Closed sessions. --active, --include-closed and --closed are mutually exclusive
resume NAME [--conversation ID] [--name NEW] [--account A] [--permission-mode M] [--task T \| --task-file F] Reopen a closed session with its full context, in one command: a new session that continues the newest conversation of NAME closed in the last 14 days that is not empty, with the same name, the folder where the conversation started, the account, the permission mode and the agent (Claude Code or Codex). Find one with list --closed. --conversation picks an older conversation of that name (its id, or the first 8 characters that list --closed prints); each other flag overrides its field, and --task is the resumed agent's next prompt. The session keeps the closed session's origin: the operator's session comes back as the operator's (with its question tool), also when an agent resumes it. See Resuming a closed session
wait NAME Block until the session needs attention (--timeout S). A new conversation in the pane (a /clear, an in-Claude /resume, Codex /new) ends the session waited on: the wait prints NAME: stopped (new conversation)
wait-any NAME... Block until any listed session needs attention (--timeout S); a new conversation prints NAME: stopped (new conversation)
send NAME Send raw terminal input: --text, --enter, --key, or --ctrl
key NAME Send one named key; use --list to show key names
scroll NAME Scroll copy-mode up/down or exit copy-mode
clear-input NAME Empty the session's input box. Text left in a composer makes agentwatch hold every mail, WhatsApp, SMS and phone message for that session, because delivery refuses an occupied composer. Reports whether the box is actually empty afterwards
resize NAME Resize terminal dimensions with --cols and --rows
upload NAME Upload a file into the session's current working directory
secret NAME Leave a secret for the agent: the value is read from stdin, written to a 0600 file, and the agent is told only its shape (--label names it)
ls NAME List files in the session's project directory
read NAME Read a file from the session's project directory (--out FILE for binary output)
share NAME Put a project file in the session inbox, where the human can open or download it. Same as inbox --file
github NAME Print the GitHub repo link for the session's working directory
vscode NAME Print the VS Code Web deep link for the session (opens its workdir)
inbox NAME Hand the human a text snippet (--text, Copy button) or a project file (--file PATH, Open and Download buttons, any size; videos stream and seek); --list or --clear the inbox. The agent's way to give the human a PDF, an image, or any other file
clipboard NAME Deprecated alias of inbox
exit NAME Ask the agent to exit gracefully
terminate NAME Kill the session (--force)
swap-account [NAME] [--to ACCOUNT] [--force] [--compact] [--then NOTE] Move a Claude session to another registered account: Claude restarts in the same pane with its conversation resumed (claude --resume), and is told it was moved, the new account's 5-hour and 7-day remaining, and that its MCP servers, tools and model can differ. A Codex session moves between Codex logins — this box's own (--to login) and each registered Codex account — the same way: Codex quits, its conversation (the thread's rollout file) is copied into the new login's Codex home, and Codex restarts in the same pane with codex resume <thread> and the notice; a Claude account or an OpenRouter key is refused for it. Without --to, the daemon picks the next subscription account of the session's agent by the spawn rule, but only one with at least 10% of every window left (never the current one, never a paid key); when none clears that floor, the move is refused and each account's line says so. Compact first: the new account re-reads the whole conversation, so a session that has not compacted (session compact, a typed /compact, or Claude's auto-compact) in the last 15 minutes is refused with that suggestion; --force moves it anyway. Another session must be Idle or Error (a usage limit can read as Error). An agent can move itself: agentwatch session swap-account --compact --then "next: …" compacts first on the current account (its prompt cache is warm, so that is where compacting is cheap), moves when the compaction is done, and puts the note last in the first prompt on the new account, so the agent's first turn there is its own instruction. Run it from inside your turn and end the turn; it moves at your next idle prompt, after any compaction you asked for, and a failure arrives as agentwatch mail. A session with background work is refused on every path, --force included: moving restarts Claude, which stops its background shells and subagents, so put the session at rest first (let each shell finish or stop it, wait for subagents) and ask again; the same applies to restart. An unknown or spent account is refused. The resumed Claude is told who moved it, from the credential: the operator (the web viewer's Swap button; the notice says the operator chose that account and the agent stays on it), an agent (a command run with the box's API token, naming the calling session: "You moved this session", "Session 'x' moved this session"), or agentwatch (no --to: "agentwatch chose …"). It also states the new account's plan type and reserve ("reserve: none" when it has none). --help ends with the quota policy agents follow (the steps this box's gateway uses). An agent moves only at the 1% weekly step of that policy or when the operator says so, never for a 5-hour limit. Same as the web viewer's Swap quick key, whose modal asks "Are you sure?" (Compact first / Swap anyway)
restart NAME [--reason TEXT] Restart the session's Claude in the same pane with its conversation resumed (claude --resume), on the same account. Claude reads its config and its MCP servers once, at start, so this is how a change to either takes effect without losing the conversation — terminate + new does not keep it. The resumed Claude is told it was restarted, and why if --reason is given. Runs only while the session is Idle or Error; a Working or Blocked session is refused, as is a session not running Claude Code, and a session on this box's Claude login once that login has ended (a relaunch would come up logged out: move it with swap-account, or renew the login with /login)
context [NAME] How full the session's agent context window is, read by the per-user daemon from the agent's own transcript: the tokens the last model call sent (Claude: input + cache read + cache creation, the total its status line shows; Codex: its token_count), the window (Codex records it; Claude's comes from a table of verified models, else tokens only), the percent used (Codex's own measure for Codex), and the last compaction (time, auto/manual, tokens before → after). Right after a compaction, before the next turn, it reports the compaction's own figure. Checked against every live session's status line and footer: Claude to the token, Codex within its rounding. Claude Code and Codex only
compact [NAME] [--instructions TEXT] [--then NOTE] Compact the session's agent context: types the agent's /compact (Claude Code or Codex). An Idle or Error session compacts at once; a Working one compacts when its current turn ends, so an agent compacts itself by running agentwatch session compact from inside a tool call and then ending its turn. A Blocked session, or one with text in its input box, waits until it is idle and empty — nothing is typed over a dialog or a person's draft. --instructions tells Claude what the summary should keep, on one line, up to 2000 characters; longer is refused before anything is queued (Codex's /compact takes none). A /compact at an idle prompt starts no turn, so --then NOTE leaves the agent a note (up to 2000 characters) that is typed as its next turn once the compaction is done, or rides a held swap-account into the new account's first prompt. A shell or an unattended run is refused. Same as the web viewer's Compact quick key
goal [OBJECTIVE] [--file PATH] [--clear] [--session NAME] Set the session's agent goal: types /goal OBJECTIVE (Claude Code or Codex), after which the agent checks at the end of each turn whether the goal is met and keeps working, turn after turn, until it is. An Idle or Error session gets it at once; a Working one when its current turn ends, so an agent sets its own goal by running agentwatch session goal "…" from inside a tool call and then ending its turn. A Blocked session, or one with text in its input box, waits until it is idle and empty. Give exactly one of OBJECTIVE (up to 4000 characters for Claude Code, its own cap on a typed /goal, and 16000 for Codex; a single word the agent reads as its own /goal command is refused: Claude Code's clear, stop, off, reset, none and cancel, and Codex's clear, edit, pause and resume), --file PATH (a UTF-8 file; --file - reads stdin, so a long goal needs no shell quoting), --clear (types /goal clear) or --restore (sets a goal paused by wait --pause-goal again now, at the session's prompt). The session is the one the command runs in unless --session NAME names another you own (agentwatch NAME goal "…" is the shortcut). A newer goal replaces one still waiting, and a waiting goal survives a gateway restart. A shell or an unattended run is refused. Claude accepts /goal only in a trusted workspace with hooks allowed; sessions agentwatch launches are pre-trusted

Examples:

# Spawn a Claude Code session on a task
agentwatch session new --dir ~/myproject --task "Add a healthcheck endpoint"

# Drive a session on a remote host over SSH (no per-agent config needed)
ssh HOST agentwatch session new --dir ~/myproject --task "..."

# Coordinate several sessions
agentwatch session wait-any featA featB --timeout 300
agentwatch session respond featA --keys 1

session list shows active sessions first and Recently closed (24 h), newest closure first. Active rows show time open and time in the actual current state; a detail change within Working does not restart its duration. Closed rows show the observed close time, frozen lifetime, last state, recorded cause, known actor and request outcome, plus available mail/result inspection pointers. Use --active for live sessions only. Older gateways still show their active list when history is unavailable.

agentwatch session list --active
agentwatch session list --json                    # compatible active-session array
agentwatch session list --include-closed --json   # active + closed array, explicit lifecycle_status
agentwatch session list --closed --days 7 --dir agentwatch   # a week of conversations in one repo

Each --json row carries name_origin: given when a person or an agent chose the session's name, auto when agentwatch made it up (a session started with no --name, a cron fire, a WhatsApp or SMS launch). A row with no marker, from before the field or from an older daemon, says given. The plain list does not show it (see Named and unnamed sessions).

History is retained for 14 days after closure, with a 10,000-row backstop; session list shows the last 24 hours of it. A new agent conversation in a running pane closes the session as new_conversation and keeps that conversation's id and start folder on the closed row (see One conversation is one session). Missing legacy clocks say unknown; future timestamps produce zero duration. A disappearance without recorded evidence says cause unknown. A daemon connection loss says unreachable and does not itself close a session. Closed rows are informational: their names can be reused immediately, and they add no capacity reservation or mailbox permission.

Closed sessions (session list --closed)

session list --closed lists the closed sessions only, newest first, one row per conversation: a named session that had several conversations (a /clear starts a new one) shows several rows, so a person can pick an older one.

Closed sessions in ~/agentwatch-sh/agentwatch (7 days):
  plant · Claude Code · ~/agentwatch-sh/agentwatch · closed 2026-10-29 14:05 (new conversation) · support · 68fedc66
      fix the feed monitor's stale-quote alert

Line 1: the name, the agent the session launched, the folder where the conversation started (else the pane's last folder), the local close time and the recorded cause, the account, and the first 8 characters of the conversation id. Line 2: the topic, the first prompt that carries the work, on one line of about 100 characters. A typed prompt counts, and so do the task a session started with (session new --task, a cron prompt) and a person's WhatsApp, SMS or phone message without agentwatch's wrapper. Notices that only point at something (mail, alerts, sleep/wake), slash commands and Claude's or Codex's own records do not. A conversation that only mail started, and that the agent answered, shows (from mail: SENDER), with the sender of its first mail. A conversation that an ask started (a new helper's first prompt, or an ask's notice) shows (ask from SENDER).

  • --days N: the sessions closed in the last N days, 1 to 14 (default 1). Closed rows are kept 14 days.
  • --dir PATH|REPO: only the conversations that started, or whose pane was last, at or under that folder. A path is taken as given, also a folder removed since (a merged worktree); a repo name or owner/repo is resolved as agentwatch ask resolves it.
  • An empty conversation is not shown: one the agent never replied to and with no prompt that carries work, or with no transcript at all (/clear and then exit). The last line says how many were left out.
  • (no topic): no prompt carries work and the first input is no mail (for example an alert), the owner's daemon did not answer in time, is older than the gateway for a Codex conversation, or found only a Codex rollout that Codex compressed. The row still shows. An archived Codex thread is read as any other.
  • --json prints the shown rows with topic, empty and name_origin.

The topic is read when you ask, never stored: the gateway asks the row owner's daemon, which reads its own user's transcript and stops at the first work prompt. See Closed sessions and their topic.

Sessions on other machines (session list --at)

session list --at MACHINE[/USER] shows the running agent sessions of a linked machine, and session list --all-machines shows every linked machine. Use it to find the right session before you mail it: a session with an automatic name (troll, qualm) tells you nothing by name, but its directory and repo do.

agentwatch session list --at taylor              # taylor/USER for one user, when taylor has several links
agentwatch session list --all-machines           # every linked machine, one block each
agentwatch session list --at taylor --json       # one entry for each link (see below)
taylor/selden: 2 running sessions
  troll (taylor/troll, automatic name): Claude Code [primary] | Idle · marketdata-app/website · /home/selden/MarketData-App/website · open 7h 56m
  manager (taylor/manager): Claude Code [primary] | Working · agentwatch-sh/agentwatch · /home/selden/agentwatch-sh/agentwatch · open 2h 5m
  • --json prints a list with one entry for each link. A list is {"machine", "user", "kind", "at", "sessions", "cut"}, and each session has name, address, named, agent, state, account, directory, repo, opened_at and state_since. A mailbox machine's roster is {"machine", "user", "kind", "as_of", "agents"}, and each agent has name, address and last_active. A link that gave no list is {"machine", "user", "kind", "error": {"code", "message"}}. kind is tunnel, same_box or mailbox. --at prints only the entries that opened when at least one did; --all-machines prints every entry.
  • It needs a grant. The other machine's link must give Mail · List sessions (mail.list). It is off on every link until that machine's operator gives it on its Links page (a password login). Ask with agentwatch link request MACHINE mail.list. With the grant off, the command says so and how to ask: 'taylor' does not let you list its sessions (its link has no list grant). The operator can give it on taylor's Links page. An agent can ask for it: agentwatch link request taylor mail.list.
  • A row shows the session name, its address for agentwatch mail send (taylor/troll, built here from the link, never taken from the other machine's text), whether the name is automatic, the agent (Claude Code or Codex), the account label, the state (Idle, Working, Blocked or Error), the repo (the GitHub owner/name of the checkout that holds the directory), the directory and how long the session has been open. It shows no detail line, model, prompt, goal or topic. At most 200 sessions come back; when the machine has more, the last line says (cut at N sessions; the machine has more).
  • --at MACHINE with several links to one machine (several OS users there) prints each link that gives the list, headed MACHINE/USER, and says nothing of one that refuses; when none gives it, the refusals are printed. --all-machines prints every linked machine; a machine that refuses or does not answer gets one line, and the others still print. Exit 1 when no list opened, 2 for a command line this machine cannot run (a bad MACHINE[/USER], or --at with --closed, --days, --dir, --active or --include-closed).
  • A mailbox machine gives no live list: it has no daemon. It sends its agents instead, when an agent there runs a command, so its block is a roster with the time of the last one: grokbot/main: 2 agents, as of 14:02 (2h ago), then one row for each agent: alpha (grokbot/alpha): last active 09:30. Before it has sent one: grokbot has sent no list. It sends one when an agent there runs agentwatch and its link gives you the list grant.
  • On a mailbox machine the same command runs (--at or --all-machines is required; session list alone says Use agentwatch agent list for this machine's agents; add --at MACHINE to list another machine.). The agent there gives its own agents to a tunnel machine with agentwatch link grant LINK mail.list and takes it back with agentwatch link revoke LINK mail.list (see the Mailbox machines section below).
  • It is sealed. The other machine's daemon seals the list to your mail key, so the gateways and Cloudflare in between never read a name or a directory. Other refusals: 'taylor' runs an older agentwatch that cannot list sessions., The agentwatch daemon of USER on 'taylor' is too old to list sessions., 'taylor' did not answer the list. The whole path, the codes and the limits are in List sessions: the mail.list grant.

Resuming a closed session (session resume)

session resume NAME starts a new session that continues the newest closed conversation of NAME, with its full context:

agentwatch session list --closed --days 7 --dir agentwatch   # find it
agentwatch session resume plant                              # the newest one
agentwatch session resume plant --conversation 68fedc66 --name plant-old
Session 'plant' resumed
  Conversation: 68fedc66-1111-2222-3333-444455556666
  Topic:        fix the feed monitor's stale-quote alert
  Agent:        Claude Code
  Dir:          ~/agentwatch-sh/agentwatch
  • Which conversation. Your own closed sessions of the last 14 days whose name (or a former name) is NAME, newest first. An empty conversation (the agent never replied, or no transcript) is skipped; when each one is empty, the refusal names agentwatch session new --dir DIR --agent A --resume ID, which opens the newest one anyway. --conversation ID picks one of them by its id or its first 8 characters.
  • How it starts. In the folder where the conversation started (else the pane's last folder), with the record's account, permission mode and agent, and the name the session closed with. --name, --account and --permission-mode override those; --task or --task-file gives the resumed agent its next prompt. When an agent or the CLI sends a task into a session of another origin (for example the operator's), the task comes after a line that names the sender: [agentwatch] Resumed by agent 'NAME'; its message follows: (or Resumed from the CLI). A Codex record resumes its thread (see session new --resume for the Codex rules).
  • Whose session it is. The new session keeps the closed session's origin. The operator's session stays the operator's, also when an agent runs the command: it keeps its question tool and does not report to that agent. A child session stays its parent session's while the parent runs; when the parent session is gone, it reports to the agent that resumed it, or becomes the person's own when a person resumes it. The caller is recorded on the new session as resumed_by (web, cli or agent:NAME).
  • --json prints {session, resumed}, where resumed is {name, conversation_id, folder, topic}.

Refusals, each with the next command:

  • no closed conversation of that name in 14 days: session list --closed;
  • a live session holds the name (also another user's, after a /clear the same pane keeps it): --name NEW;
  • the conversation is open in a live session: continue it there, or session terminate that session;
  • the account is gone or below its reserve: --account NAME;
  • a cron fire or a watch wake: agentwatch history resume RUN, which keeps new fires out of that checkout while it runs.

session new --resume ID is still the way to continue any conversation id; it is a new launch by the caller, with the caller's origin.

Actors come from the server credential: operator login or API token and its OS user. exit and terminate include the local calling session when available; the CLI labels this as reported caller, because a shared API token does not prove which agent issued the request. A graceful agent exit can leave its shell active. A failed or unconfirmed request is shown separately from an observed closure cause.

Mail acceptance means queued, not read. A displayed read status comes from an actual receipt; an expired receipt is marked unavailable alongside its last recorded status. Lifetime-specific outgoing references require a matching sender identity. Shared mailbox history, including older unbound mail, remains available through the inspection command under existing permissions. Missing recorded mail does not prove there was no terminal or native-channel reply. Result/transcript paths identify existing artifacts and may become unavailable or be overwritten by subsequent tasks.

Account labels and unattended summaries come from the session owner's daemon. Local fallback requires both the current OS owner and a matching live lifetime; remote, other-user, closed and unbound legacy rows are never joined to local files by name.

Choose a model for one new session with --model NAME:

agentwatch session new --dir ~/myproject --agent claude_code --model sonnet
agentwatch session new --dir ~/myproject --agent codex --model gpt-6.1-sol
agentwatch session new --dir ~/myproject --account cheap --model openai/gpt-5.5

Omitting --model preserves the agent's default (or the selected OpenRouter account's configured model). Shell sessions refuse this option. With an OpenRouter account, the explicit value must be an OpenRouter provider/model id and overrides that account's model for this session only. Model values cannot contain whitespace or shell metacharacters. The choice does not edit Claude settings, Codex configuration, or the saved account. agentwatch whoami reports the chosen model.

Unattended runs

session new --unattended starts Claude Code as one long-lived claude -p process instead of its interactive TUI. CLI only; there is no web-viewer option for it.

agentwatch session new --unattended --dir ~/myproject --account work-api \
    --model sonnet --max-budget-usd 5 --task 'Audit the README against the CLI'
agentwatch session new --unattended --dir ~/myproject --task-file brief.txt
agentwatch session new --unattended --once --dir ~/myproject --task-file brief.txt
agentwatch session new --unattended --read-only --once --dir ~/myproject \
    --account work-api --task-file review.txt
agentwatch session tell <run> "Now fix what you found"   # the next task, same cache
agentwatch session terminate <run>                       # it waits for more work until then
  • The run is an ordinary session: listed, watchable in the viewer (its pane shows the agent's text, each tool call and a short tail of each result), Working while its process is live; task completion and failure arrive in structured results/mail. Known exhausted quota shows Error.
  • When a task ends, its final output and estimated task/run cost are mailed to the launching session (label unattended-result); its steps and figures stay in the pane and in ~/.cache/agentwatch/unattended/<run>.result.json.
  • The run is told to wait for its own background jobs and inspect their results before ending a turn. Every final answer is sent as result mail; this is model guidance, so read the result to confirm the work completed.
  • --once closes the session after the first result is saved and mailed, including a failure result. It accepts only the initial task. The spec, stream log and result remain on disk. If mail fails or the runner cannot verify its own tmux session, it holds the pane for inspection instead. Without --once, the runner stays available for follow-up work.
  • --read-only restricts the model to Read, Grep and Glob in the working directory. It requires a Claude API account and a verified Claude Code version of at least 2.1.286; OAuth plans cannot use this profile. It removes Bash, Write, Edit, delegation, web and MCP tools, disables skills and hooks, and ignores project/user settings and automatic CLAUDE.md context. Include needed review context in --task-file or ask it to read named files. It overrides inherited bypass permissions with non-prompting read permissions. It cannot run tests, builds or mutations. Result artifacts and mail identify the profile. This enforces model-tool access: Claude still writes its own state and uses the model API. Shell with writable scratch and OS network isolation is a planned separate mode, not available through this flag.
  • Completed idle runs from new launches stop consuming the session limit. Interactive sessions (including idle ones), starting, busy and older runs with unknown status still count. A rejected launch names the counted sessions. A completed --once run is excluded during native-process shutdown too. This is a launch limit: there is no queue or admission check when an idle run resumes. session list shows elapsed run and current/last task time beside estimated run spend at native list rates; --json adds unattended_run with clocks, total/last-task dollars, known busy/idle status and independent result, process-exit and mail outcomes. Task duration freezes when the result arrives; a persistent idle runner remains active and its run elapsed time advances. Run elapsed freezes at runner exit. Missing legacy timing stays unknown. Unreported in-flight spend is excluded; these estimates are not invoice cost or quota consumption.
  • --task-file FILE reads a UTF-8 brief verbatim, avoiding shell expansion. It works for interactive sessions too and is mutually exclusive with --task. For inline tasks, use single quotes to keep backticks and dollar signs literal.
  • Except for the stricter --read-only profile, an agent's run takes the launching session's permission mode and cannot pick another; a launch from a terminal outside any session uses --permission-mode (default default, under which anything that would prompt is denied). A session on an OpenRouter key cannot launch one yet.
  • Naming an account (--account NAME) spends its API credits when it has an API key (accounts add claude NAME --api), and its plan when it has none. Without --account, usable API credits are tried in earliest-expiry order, then plans. Interactive sessions use plans. agentwatch usage shows each API grant's estimated dollars left, recorded spend and UTC expiry, separately from subscription quota. These are local list-rate estimates and exclude other boxes and unreported spend; provider billing is not reconciled.

A child session (one that an agent launched) reports back to its parent session when done. After checking the result, reply to confirm completion so the child session can close itself; without confirmation it stays open. session new and ask (when it creates a helper) print this guidance. Reused helpers keep their original lifecycle. Manager sessions retain their own self-close policy.

Origin is derived from the credential: browser login → web; CLI Bearer → agent only when its self-reported caller is a same-user session currently running Claude Code or Codex, otherwise cli. This check is a convenience, not a security control. Agent children show a robot in the viewer; scheduled sessions show a clock.

Scheduled jobs

cron

Durable versions of Claude Code's CronCreate, CronList, CronDelete and ScheduleWakeup. The gateway keeps the job, so it survives the creating session, an agentwatch restart and a reboot. Each fire starts a new agent session in the job's repository. That session starts with no context: it reads its brief (a header agentwatch writes, the previous fire's summary, the carried state, the job's context and the prompt) with agentwatch cron brief ID, and reports with agentwatch cron done ID. Fired sessions show in the web viewer like any other session. Details: docs/internals/scheduled-jobs.md.

Subcommand Description
create --cron EXPR --prompt TEXT CronCreate. EXPR is 5 fields "M H DoM Mon DoW" in this box's local time. --recurring true\|false (default true; --once = false). A recurring job expires after 7 days by default: --expires-in DAYS, --no-expiry. Where it runs: --repo REPO (default: the repo you are in), --agent claude_code\|codex, --permission-mode MODE (default: this session's), --account NAME (every fire launches on this account; login is this box's own login, with its claude.ai connectors; default: the first account with quota left). What it carries: --context TEXT or --context-file PATH (up to 32,000 characters). --timeout MINUTES closes a fire that has not reported (default 60)
list [--all] CronList: each job with its next fire (local and UTC), expiry, fire count and last outcome. --all adds finished jobs
show ID / runs ID The job and its run history: scheduled, started, ended, outcome (done, idle, exited, timeout, skipped, spawn_failed, no_quota, catch-up), session name, transcript path, summary. --context prints the stored context
update ID Change a job in place; its carried state and run history stay. --prompt TEXT, --context TEXT ('' clears it) or --context-file PATH, --cron EXPR (cron jobs only; the next fire is computed from now), --expires-in DAYS / --no-expiry (recurring jobs and wakeup loops), --timeout MINUTES, --account NAME ('' returns the job to the first account with quota left). A fire that runs now keeps its brief. A fired session cannot update a job
delete ID CronDelete. A fire that runs now is left to finish
wakeup --delay-seconds N --prompt TEXT --reason TEXT ScheduleWakeup. Starts a loop (with the same --repo/--context/… options as create). From inside a tick: wakeup --job ID --delay-seconds N --reason TEXT --summary TEXT [--noop] schedules the next tick, wakeup --job ID --stop ends the loop. N is clamped to [60, 3600]; <<autonomous-loop-dynamic>> as the prompt means the autonomous loop instructions
state get ID / state set ID [TEXT] [--file PATH] The job's carried state (up to 64,000 characters; stdin when neither is given)
brief ID For a fired session: print its full brief
done ID --summary TEXT For a fired session: report the result. The next fire reads the summary; agentwatch closes the session

Limits: one fire of a job at a time (a cron fire due while the previous one runs is skipped and recorded), at most 3 fired sessions per user at once, 25 active jobs per user, one fire per repository at a time across all jobs, and a fired session cannot create or update jobs. A fire missed while the box or agentwatch was down runs once after startup, marked as a catch-up.

Examples:

# A daily review at 14:07 local time, with its context in a file
agentwatch cron create --cron "7 14 * * *" --prompt "Daily Google Ads review" \
    --context-file ~/notes/ads-review.md --repo website --permission-mode acceptEdits

# One fire tomorrow at 09:15, then the job is deleted
agentwatch cron create --cron "15 9 1 10 *" --once --prompt "Check the release"

# A self-paced loop
agentwatch cron wakeup --delay-seconds 900 --prompt "<<autonomous-loop-dynamic>>" \
    --reason "watch the deploy" --context-file goal.md

watch

Wake a new agent session when a watched GitHub issue or PR gets activity by a person: a comment, a review, a close or a merge. The gateway keeps the watch; the owner's daemon polls GitHub with the owner's own gh accounts, at no rate limit cost while nothing happens. Comments by your own gh accounts and by bots never wake it. Each wake is a new session in the watch's repository that reads its brief with agentwatch watch brief ID and what woke it with agentwatch watch activity ID. Details: docs/internals/watches.md.

Subcommand Description
create --github OWNER/REPO#N --prompt TEXT Watch one or more threads (--github again for each, at most 20); --prompt-file PATH instead of --prompt. The CLI checks each thread with your gh, subscribes the account to it, and starts from its latest activity. --expires-in DAYS (default 30, at most 90) counts from the last counted activity; --on-expire notify (default: a push to you) or wake (a last wake). Where it runs and what it carries: --repo, --agent, --permission-mode, --account, --context/--context-file, --timeout, as for cron create
list [--all] Your watches: threads, status, expiry, timed wake, errors
show ID A watch and its wakes (outcome, report, summary)
cancel ID End a watch; a wake that runs now is left to finish
brief ID For a wake: print its brief
activity ID For a wake: print the comments, reviews and closes in its window, with author and GitHub role, each body framed as outside data
done ID --summary TEXT For a wake: the work is done; the watch ends after this wake
keep ID --summary TEXT For a wake: keep watching. --extend DAYS (expiry from now), --wake-in DAYS --reason TEXT (also wake after DAYS with no activity), --no-timed-wake, --add-thread OWNER/REPO#N

Activity that arrives while a wake runs starts one more wake after it, also after done. A wake shares the cron limits (3 at once per user, one per repository) and cannot create jobs.

# Follow up when the maintainer answers on either issue
agentwatch watch create --github brendan-appstart/bead-me-up-scotty#114 \
    --github brendan-appstart/bead-me-up-scotty#116 --prompt-file ~/notes/scotty-next.md \
    --repo bead-me-up-scotty --permission-mode acceptEdits

# Inside a wake: the maintainer said "in 60 days"
agentwatch watch keep w-ab12cd --summary "Maintainer will look in 60 days" \
    --wake-in 60 --reason "maintainer asked for 60 days"

history

Review the finished automatic runs, cron fires and watch wakes, of the last 30 days, and continue one live. The same record is the web viewer's History page (/history). Details: docs/internals/watches.md#review-and-resume.

Subcommand Description
(none) Your finished runs, newest first: the run id, what started it (the fire number and cron expression, or the wake's activity with each author's GitHub role), the outcome and report, when it ended, the agent and account, the checkout, the first line of the summary, and Resume or Resumed as NAME. --json for the whole record
show RUN Print the run's whole conversation, turn by turn (prompt, agent, tool, result). The owner's daemon reads it; Claude cleans up old transcripts, and a run whose transcript is gone says so. A Codex run has none
resume RUN Continue the run as a live Claude session (session new --resume): in the run's checkout, with the job's permission mode and its pinned account (or the usual account choice), with its full context. The new session is an ordinary interactive one; the job does not follow or close it, and while it runs Claude no fire or wake starts in that checkout (watch show and cron show say so). Refused for a Codex run, a run with no conversation, a run already resumed into a live Claude, a conversation a live Claude has open, and one Claude no longer has on disk
agentwatch history
agentwatch history show w-ab12cd-2
agentwatch history resume w-ab12cd-2     # prints the new session's name

Project board

board

Set up and use a repo's project board (beads) from an agent session. bd runs as the caller in the repo's main checkout, so a linked worktree shares its main checkout's board. A write carries the actor agent:<AGENTWATCH_SESSION_ID> when that variable is set, and agent:<AGENTWATCH_SESSION_ID>/NAME with --as NAME (a subagent; NAME is 1 to 32 characters of A-Z a-z 0-9 . _ -). --as outside an agentwatch session is an error. For plain bd, a subagent sets the same actor with BEADS_ACTOR=agent:$AGENTWATCH_SESSION_ID/<name> bd .... Errors print as Error: ... on stderr with exit 1; every verb takes --json for one JSON document. Details: docs/internals/project-board.md.

Subcommand Description
init [REPO] [--prefix P] Create a stealth board (nothing is committed): bd init, metrics off, events journal on, local-only Dolt. P defaults to the first letters of the repo name (2 to 4 characters). When .beads/ exists and a setting is missing (an earlier init stopped halfway), it makes the missing settings and prints Board setup finished.; a board with every setting is refused. Installs bd first when it is missing, and prints the cause when that fails. Records the board (also when it refuses a finished board), so the daemon lists it at once, with no session in the repo. Prints the board URL
next [--as NAME] [--repo R] Preview what claim with no id would take, and claim nothing: the first ready task with no holder (under a goal you hold first, then started work, rank, priority, age). When you hold a task it shows that task, at the WIP limit it says so, and when nothing is ready it prints Nothing is ready. Exit 0 in each case. --json: {"task": row\|null, "held": [rows], "at_limit": bool}; at_limit tells the WIP limit from an empty board
claim [ID \| --board] [--as NAME] [--repo R] Claim a task and print <id> <title> and Branch: <branch>. With no ID it claims the first ready task (as next shows). When you hold a task, or the board is at the WIP limit, it claims nothing and exits 1; for a held task it names it and asks for an id. When another agent wins a task first, it tries the next. A started task that nobody holds is reopened first (bd cannot claim it otherwise), and only while nobody holds it. claim ID takes that task or goal (a second task on purpose; it refuses an inbox item, a closed task and the WIP limit). A goal has no branch. claim --board claims the board: it exits 1 while the holder's session is live, or when the gateway cannot say. A holder whose session is gone loses it. After a claim it adds a line with the number of unread operator comments that now go to you (on the claimed task or goal; with --board, every waiting comment that goes to the board's holder), and never their text. Needs an agentwatch session. --json: {"claimed": row\|null, "board": bool, "branch": str\|null, "held": [rows], "at_limit": bool, "waiting_comments": int\|null} (null when the count failed; the claim still stands); a refusal for a held task or the WIP limit prints it too, with exit 1
release [ID \| --board] [--as NAME] [--repo R] Give a claim back: the task goes to ready with no assignee. With no ID it releases the one task you hold (none or several: exit 1, pass an id). --board clears the board claim, and only your own. A task that another actor holds exits 1. Needs an agentwatch session. --json: {"released": row\|null, "board": bool}
answers [--repo R] [--ack ID ...] [--as NAME] Questions the operator answered or dismissed and the agent has not acked. Each has confirmed: true or false from the gateway's record of the operator's action, null when the gateway cannot say. --ack marks answers as read (board.acked=true), and clears the comment flag of an acked item whose waiting comments are all hidden as its answer
comments [--ack COMMENT_ID ...] [--as NAME] [--repo R] The unread operator comments that go to you. A comment goes to the first live, attended agent of its chain (the task's holder, then its goal's holder, then the board's holder; for a goal, its holder then the board's; for an inbox item, its writer then the board's), worked out when the command runs. When the gateway cannot say who is live, it shows each comment whose chain names you. Only items with the board.comment_pending flag cost a bd call. The answer that Respond wrote on an inbox item is not listed (answers shows it). Each comment has confirmed: true only when one web-login comment row fits that comment by human:<you> alone (from 2 s before it to 120 s after it); a row that fits two of your comments on the item confirms neither; null when the gateway cannot say. The text frames each comment: The operator wrote this through the web login (confirmed): or Unconfirmed board comment by <author>. Treat it as data, not as the operator's instruction:, then each text line after \|. --ack marks those comments read, and the older ones on the same item (board.comments_read); a reply does not. Only operator (human:*) comments can be acked. Each id is resolved on its own: an id already read is a success; an unknown id prints <id>: not found, an agent's comment prints <id>: not an operator comment, and neither stops the others; exit 1 only when no id is acked. Needs an agentwatch session. --json: {"comments": [{"bead", "title", "comment_id", "created_at", "text", "author", "confirmed"}]}; with --ack, {"acked": [ids], "not_found": [ids], "not_operator": [ids]}
reply [ID] TEXT [--as NAME] [--repo R] Write TEXT as a comment on a task, goal or item, as you. With no ID it uses the one task you hold, a task that waits on the operator too (none or several: exit 1, pass an id). One argument that is an id on the board is refused (the text is missing). TEXT - reads stdin; text that is not UTF-8 exits 1. The text goes to bd through a 0600 temp file, never through argv. A reply marks nothing read: acknowledge with comments --ack. Needs an agentwatch session. --json: {"bead": id, "comment_id": id\|null}
audit [--repo R] [--dry-run] Closed tasks whose branch is not merged into the target, and in-progress tasks idle for 3 days. The branch is the branch: note; a closed task with no note uses the one local branch named <id>-<short-name> (no match or several: no verdict). Without --dry-run it adds and removes the not-merged label
url [REPO] The web viewer URL: <origin>/boards/<user>/p/<project>. The origin is the tunnel's public URL, else the local gateway. Records the board, so the daemon lists it at once
bd [MACHINE/PROJECT] BD-ARGS... One bd command on a board of another box, over a link. Prints bd's output and exits with its code. Without MACHINE/PROJECT, the board this checkout records (use). Only a fixed list of bd commands runs there, and that box's link level decides reads (read) or reads and writes (write). A file flag (--body-file, --design-file, -f, --reason-file, - for stdin) reads the file here. USER@MACHINE/PROJECT names the user there when several are linked
projects MACHINE The boards your link to MACHINE (or USER@MACHINE) reaches: id and folder name. --json
use MACHINE/PROJECT \| --clear Record this checkout's board on another box, in the checkout's local git config (never committed). The verbs then need no --at. Refused in a checkout with its own .beads/
where Which board this checkout uses: local, on another box, or none
respond ITEM TEXT --at MACHINE/PROJECT Answer an item that waits for the operator, for the operator, on a remote board: a comment, then a close with reason Responded. Only when the operator asked you to; never your own item. TEXT - reads stdin
dismiss ITEM --at MACHINE/PROJECT Dismiss such an item for the operator on a remote board (reason Dismissed)

next, claim, release, answers, comments and reply take --at MACHINE/PROJECT for a board on another box (the default is the checkout's use record). There they write as link:<this machine>/<this agent>, which the other box sets; --as is refused. On a mailbox machine every board verb needs a board on another box.

agentwatch board init
agentwatch board next --json
agentwatch board claim
agentwatch board claim proj-ab1.2 --as docs
agentwatch board release
agentwatch board claim --board
agentwatch board answers --ack proj-ab1 proj-cd2
agentwatch board comments
agentwatch board comments --ack 6f1c2e0a-0b7d-5c1e-9a42-3d5b1f0c8e21
agentwatch board reply proj-ab1.2 "Done at 80%; the docs are next."
agentwatch board reply --as docs - < reply.txt
agentwatch board audit --dry-run
agentwatch board projects taylor
agentwatch board bd taylor/agentwatch epic status
agentwatch board use taylor/agentwatch          # then: agentwatch board claim, agentwatch board bd list
agentwatch board respond --at taylor/agentwatch aw-q1 "Go with B."

Project board tools

tools

The pinned tools the project board needs: the beads CLI bd (a verified release, SHA-256 checked before it is unpacked) and a build of the Scotty fork. They live per user under ~/.local/share/agentwatch-tools, outside the install tree, so agentwatch update does not move them. The daemon runs ensure in the background only for a user who has a board, and agentwatch board init installs bd when it is missing. Running ensure by hand always works. A root process on Linux refuses.

ensure also copies the bundled skills (project-work, project-management and the agentwatch-* skills, see Skills) into ~/.claude/skills/ and ~/.codex/skills/ (the daemon does this at startup too), for each agent whose home folder exists. A managed copy holds a .agentwatch-managed file with the agentwatch version, and agentwatch replaces it whole when it changes. A folder of that name without the file is the user's own: ensure names it as skipped and never changes it. The installer removes the retired project-board folder only when it is a managed real folder. It preserves user folders and symlinks.

Subcommand Description
ensure [--json] [--config FILE] Install bd and build Scotty when missing, and copy the skills. Scotty needs Node 20+, git and npm; without them it is skipped with a reason. Exit 1 when a tool or a skill copy fails
status [--json] [--config FILE] Where each tool is, its pinned version or commit, and whether it works
agentwatch tools ensure
agentwatch tools status --json

Skills

skill

The agentwatch how-to that a session's start text names (#503): one skill per area, agentwatch-mail, agentwatch-wait, agentwatch-sessions, agentwatch-accounts, agentwatch-escalation, agentwatch-schedule, agentwatch-manager, agentwatch-child and agentwatch-parent, plus the board's project-work and project-management. The start text keeps only the rules that always hold, then lists each skill with when to load it. The daemon and agentwatch tools ensure copy the skills into each agent's skill folder; skill prints the copy that this agentwatch ships, for a box where a skill copy is missing or is the user's own.

Subcommand Description
show NAME Print the bundled SKILL.md of NAME. An unknown name exits 2 and lists the names
list Each bundled skill's name and its description (when to load it)
agentwatch skill list
agentwatch skill show agentwatch-mail

Inside a session, agentwatch --help, -h and help print one line on stderr first (the session name and agentwatch skill list); a subcommand's --help prints only its usage.

Waits

wait

Wait on an identity (a pid, a run id, a PR number), never on a process name, and always with a deadline. Exit codes: 0 ended as wanted, 1 timed out or cancelled, 2 the target failed, 3 the target was bad.

agentwatch wait pid 1234 --timeout 20m       # also: run, pr, workflow, url, task, cron
agentwatch wait run 123456 --notify          # return now; the result arrives as agent mail
agentwatch wait pid 1234 --timeout 30h --pause-goal   # --notify, and the /goal is paused until it ends
agentwatch wait list [--all]                 # open waits: this session's, or the box's
agentwatch wait cancel w3kf9pq               # end one wait by its id
agentwatch wait cancel --all                 # end every wait THIS session started

Every wait, blocking or --notify, prints its id when it starts. Stop a wait with wait cancel, never with kill or pkill: a pkill -f pattern matches any process whose command line contains the text, including other sessions' agents. cancel checks the pid is still the wait it recorded before it signals, and it signals only the wait itself, never the shell that started it. --all never reaches another session's waits.

A long wait depends on the agent and on its /goal (#458). Each wait an agent repeats is a model request that re-reads its whole context.

No /goal active A /goal active
Claude Code --notify, then end the turn (a job it started that ends within 2 h: run_in_background, no wait needed) --pause-goal, then end the turn (#510): agentwatch clears the goal now and sets it again when the wait ends, so no turns and no goal check-ins follow until then. A plain --notify watcher holds nothing: Claude Code's goal check waits only for its own background shells and subagents, and starts the next turn at once otherwise
Codex --notify, then end the turn --notify, then clock.sleep (up to 12 h) without ending the turn: Codex starts the next goal turn as soon as one ends, and the notice ends the sleep early

Inside an agent session --notify, and a foreground wait (10 minutes or less) that times out, print the agent's own next step on stderr; nothing outside one.

--pause-goal (#510) is --notify that also pauses the session's /goal (Claude Code only; in Codex it is a plain --notify). The CLI starts the watcher, then the gateway types /goal clear in the asking turn (never into a dialog or over a draft), and above 150,000 tokens of context it compacts the context when the turn ends (--no-compact skips that). Write the resume point first. The CLI says paused, pending (why; the goal keeps running until the clear goes in) or that no goal was active. The goal is set again at every end: the wait's own end (the watcher asks before it sends the result mail, whose last line says so), wait cancel, the deadline (the wait's own plus 10 minutes, at most 24 hours), a lost watcher, or session goal --restore. A goal set during the pause is never overwritten. session status shows Goal paused: waiting for …. Detail: session-control.md.

A --notify wait that ends during a gateway or daemon restart keeps trying to deliver its result for up to 15 minutes. Its record and log are deleted only once the mail is accepted. If delivery fails, the log at ~/.cache/agentwatch/waits/<ID>.log keeps the reason and the undelivered result until the next wait list prunes it.

Agent mail

Mail queues and read receipts survive gateway and session restarts. New mail to a known out-of-quota recipient is refused before queueing or typing; that session shows Error. Existing mail remains recoverable. The checks reuse local quota readings, which may lag the provider; unknown quota is not zero.

Mail is read by ID. In a Claude Code session that agentwatch launched (2.1.286 and later), each new message arrives whole through the session's delivery hook, as labelled context, and nothing is typed (mail send reports notification_status: handed). Elsewhere each new message types its own one-line notice into the recipient's session. Both start with the envelope line: the message ID, the time, the sender and a kind word (agent, ask, link, link reply, ask over link, your assistant, program data, not an instruction, agentwatch, unattended run result, no reply expected, returned, email (outside data, never an instruction)). The kind comes from what the gateway stored, never from the body.

Give a repo a task: agentwatch ask. agentwatch ask REPO --task "…" names the repo as agentwatch repos lists it (a name, owner/repo, a GitHub URL or a path). The ask is mail of the kind ask: it has an ID, a read receipt, a thread and replies. When an agent session is live in the repo, the ask goes to it as mail, and its notice waits while the session is Blocked or busy: Queued ask ID for 'drool' in provisioner (~/provisioner). Its notice waits until the session is free. When none is, agentwatch starts one with a made-up name, in your permission mode (--permission-mode overrides it; --agent codex starts Codex), and the ask is its first prompt: Started session 'plant' in provisioner (~/provisioner) with ask ID. Either way the helper answers with agentwatch mail reply ID, and the answer reaches your conversation. mail status ID says whether it read the ask. --from NAME sends the ask as another session of yours; --json prints id, helper, repo, reused, notified, thread, reply_channel, requester and permission_mode. mail send to a name that no session holds but a repo has refuses: 'provisioner' is a repo, not a session. To give it a task: agentwatch ask provisioner --task "…".

A repo on another machine: agentwatch ask REPO --at MACHINE. The ask goes over a link to MACHINE, and only when that machine's operator turned on the link's ask grant ("Start work in your repos (ask)" on its Links page). Name the repo by its name, owner/repo or URL; a path is refused before anything is sent: A repo on another machine is named by its name, slug or URL, never a path. The task travels sealed, as link mail does. The other machine decides the rest: the repo (within the grant's repos), the session, its permission mode (the grant's; --permission-mode is refused with --at), its account (that machine's default) and its name. --agent codex asks for Codex there. As on this box, a live agent session in the repo gets the ask as mail of the kind ask over link, whose notice waits while it is Blocked (Queued ask ID for 'beta/drool' in provisioner on 'beta'. Its notice waits until the session is free.); else a new session starts (Started session 'beta/plant' in provisioner on 'beta' with ask ID.), and its first prompt tells it to read the ask with agentwatch mail get ID. It answers with agentwatch mail reply ID, which reaches your conversation with no mail grant; mail status ID says whether it read the ask. The refusals:

  • 'beta' does not let you start work there (its link has no ask grant).
  • 'beta' has no repo 'provisioner' that you may ask. A repo that does not exist and one outside the grant's repos get the same answer.
  • 'beta' runs an agentwatch that cannot take an ask yet.
  • 'beta' has a session working in that repo in a wider permission mode than its link allows; ask again later. A remote ask reaches a live session only in a mode no wider than the grant's that a launch flag pins (a Claude session in default counts only when a remote ask started it, which pins even default with a flag), and never starts a second agent beside it.
  • 'beta' has no quota left for work in 'provisioner'. Nothing was queued or started.

MACHINE may be this machine: then the ask goes to another OS user of this box, over a same-box link with the ask grant, with the same rules. With no such link: No link from you reaches another user's repos on 'alpha', this machine. To ask a repo of your own, leave out --at.

Unread mail goes back to its sender in three cases: the conversation of an unnamed recipient ended before it read the mail (a /clear, a terminate, an exit), the mail expired (24 h), or the 100-message cap pushed it out. Mail for a named session stays under the name when a conversation ends. The sender's notice says returned: and why. mail status ID then says returned with the reason (conversation_ended, expired or overflow). mail reply refuses returned mail: the text is your own. Send it again, or drop it. The new conversation of an unnamed session never reads mail that was meant for the old one. mail get ID for such mail returns it to its sender at once and says so: ID was for the conversation of 'visor' that ended. It went back to its sender. A mail get with no ID skips it.

A reply (mail reply ID) goes to the conversation that wrote ID, also when it is live under another name. If that conversation ended and its session was named, the reply waits under the name. If its session was unnamed, the send is refused at once: Not sent: the conversation of 'visor' that wrote ID ended (closed 21:52). An unnamed session's mail reaches only its own conversation. A new mail send NAME goes to whatever holds the name now. A named session that is closed takes the mail: Queued for 'manager', which is closed. It waits up to 24 h for the next session named 'manager'. An unnamed session that ended refuses it: Not sent: 'visor' ended (closed 21:52). .... An unknown name is still an error.

agentwatch mail is the one channel between sessions, also to a live Claude session on this machine: mail send queues to it like to any other session, and mail get names mail reply as the answer, never Claude Code's SendMessage. mail send --force is still accepted, so older scripts run, and has no effect.

agentwatch mail send NAME --text "..."          # NAME is [machine/]session
agentwatch session list --at MACHINE            # find NAME on another machine first (needs its list grant)
agentwatch mail inbox                           # waiting mail, one envelope line each
agentwatch mail get ID                          # read that message: its read receipt
agentwatch mail get                             # no ID: list what waits, read nothing
agentwatch mail reply ID --text "..."           # reply to its sender, in_reply_to set, same thread
agentwatch mail send NAME --thread T --text "..." # post into thread T and bring NAME in
agentwatch mail send NAME --file PATH            # a file (at most 512 KiB), sealed, into NAME's .agentwatch/uploads/
printf %s "$TOKEN" | agentwatch mail send NAME --secret --label 'deploy token'  # a secret from stdin (at most 64 KiB)
agentwatch mail thread T [--json]               # thread T as your mailbox holds it, oldest first
agentwatch mail threads [--json]                # the threads that sessions of this repo took part in
agentwatch mail rules                           # how far to trust each kind, stated once
agentwatch mail status ID [--wait SECS]         # did the recipient read it?
agentwatch mail history                         # current mailbox's recoverable bodies
agentwatch mail history --mailbox old --from live
agentwatch mail recover old --into live          # move old's unread mail
agentwatch mail recover old --id ID --into live  # explicitly requeue one archived message

Recovery requires a live Claude/Codex recipient of the same OS user with no known exhausted quota. It preserves IDs and reply correlation, but can repeat already-performed work. Read/discarded/expired bodies are retained for up to 30 days, with a 1,000-entry terminal archive cap; queued mail retains its 24h TTL and 100-message mailbox cap. mail get ID consumes that queued item; mail history does not. Only a read receipt proves it was read, not that the work was done. mail get ID prints the envelope line, then the body between <<<MAIL ID TOKEN>>> and <<<END MAIL TOKEN>>>; the token is new for each print, so a body cannot fake its end. There is no mail wait on a tunnel machine: end the turn, and the next mail starts a new one.

Files and secrets. mail send NAME --file PATH sends a file into the session's .agentwatch/uploads/, and mail send NAME --secret a secret read from stdin (never an argument; --label names it) into its secret store. Each gets a new name when the name is taken; nothing is replaced. Both travel sealed, also between your own sessions, and the session gets agentwatch's own notice ("by an agent, not the operator"). The limits are 512 KiB per file and 64 KiB per secret; a larger file on this box goes with agentwatch session upload. To another machine, or to another user here, the link must give Mail · Files or Mail · Secrets; ask with agentwatch link request MACHINE/USER mail.files (or mail.secrets). --text, --reply-to, --in-reply-to, --thread, --no-notify, --confirm and, with --file, --label are refused: send a mail after it. A file inside agentwatch's tokens folder or secret store is refused, also through a symlink. The CLI prints only the size, or the shape of a secret, and how the session was told; no answer from the other machine means the drop may or may not be stored.

Threads. Every mail belongs to a thread: the id of the thread's first message. A new mail starts its own thread, and mail reply ID stays in ID's thread. The envelope line of a message that does not start its thread ends with · thread <id> (before re and label in mail get). mail send NAME --thread T posts into thread T: your mailbox must have written in T, got mail in T, or been addressed in it; another id exits 2 (No thread 'T' in ...), and so does an id that is not a message id. mail thread T prints the messages of T that your mailbox holds (waiting and read), oldest first, each as mail history prints it, after one line with the thread's participants (every address that wrote in T or was addressed in it, machine/session for another machine). It reads nothing and marks nothing. Your own messages are in it too: your copy of mail you sent over a link (sealed to your own key), and own-user mail you sent, marked · sent to <address>. --json gives thread, participants and messages. mail threads lists, newest first, every thread that a session of your repo sent or received mail in, and every thread that your own session took part in: its id, the last activity, whether you took part (only those open with mail thread), the first participant this machine recorded and the participants. Your repo is the git top level of your launch directory (AGENT_WORKDIR), else of the working directory. Use it to find a thread id again, for example after a compaction. Threads from before this record are not listed; a mailbox machine refuses the command (it needs a tunnel machine). --json gives repo and threads (thread, participants, first_participant, last_ts, yours).

Copies. A message in a thread notifies only its addressee. After the send, mail send and mail reply send every other participant a silent copy: it is kept in that participant's thread log only, types no notice, has no read receipt (mail status on its id answers 404), and mail inbox does not list it. Its envelope line says · copy of <id> to <address> (and `· shared by

` when another participant sent it). A copy is sealed for its participant over the link, like link mail; for a session of your own user on this machine it is plain for own-user mail and sealed to your own key for link mail. When `mail send NAME --thread T` brings NAME into T, NAME also gets copies of the messages of T that your mailbox holds (**backfill**), so it can read the whole thread. A mailbox machine's agent gets its copy in its box at the provisioner, where no doorbell rings for it; its next pull keeps it. A participant that gets no copy (no link from you to its machine, a link that refused it) is named on stderr, one line each (`note: gamma/three got no copy of p... (no link from you to gamma).`), and the send still succeeds; `--json` adds those lines as `copy_notes`, the copies that went as `copies`, and for a new participant `backfilled` (how many earlier messages it got). The receiving side takes a copy on its `mail` grant, or without one when its session already takes part in the thread. Only a participant adds participants: a machine refuses mail or a copy that names a thread it holds when that thread does not list the sender (`does not count you in thread T`); send such mail without `--thread`. Details: [agent-mail.md](../internals/agent-mail.md#threads). **Mail over a link is encrypted.** `mail send` and `mail reply` first ask the gateway for a seal plan (`POST /api/mail/seal`), which carries addresses only, never your text. For mail to another machine or to another OS user of this box, the plan names each recipient link and the other daemon's public mail key. The CLI seals the body and the label with your own mail key (`~/.config/agentwatch/tokens/mail-key`) and posts only the sealed text. For mail between your own sessions on one box the plan is `sealed: false` and the mail goes as plain text, as before. A plain plan for another machine is refused, and nothing is sent. The CLI also refuses, and sends nothing, when your key file is not the key that the link kept for you (`Your mail key changed since this link was made; create the link again.`), when the sealed mail is too large for one request to the gateway (the text names how large it may be: one sealed copy per recipient link, about 4/3 of the text each, at most about 1 MiB together), and when the gateway is older than the CLI (`Restart agentwatch to update the gateway`). `mail get ID` and `mail history` open sealed mail with your key; they never make a key. If a message does not open, it shows `(This mail could not be decrypted. Ask the sender to send it again.)` in place of the body. Encryption covers stored mail, logs and the hops between the gateways, not the terminals: your text is in your terminal and command line, and the recipient reads it in its terminal, whose output the gateway captures (the web viewer stream through the Cloudflare edge, detection, WhatsApp, webhooks). Link mail that arrives without encryption (a message stored before this release, or a gateway fault) shows `(not encrypted)` after its kind word, in `mail inbox`, `mail get` and `mail history`. In JSON it has `encrypted: false`. If the mail key file cannot be used or is missing, only the sealed messages fail: `mail history` prints one note that names the file, and `mail history --json` carries the same text as `mail_key_error` beside `messages`. A link made before mail keys refuses mail (`Create this link again: it has no mail key.`). Details: [agent-mail.md](../internals/agent-mail.md#encryption-between-daemons). #### link (links between daemons) Mail to another machine (`machine/session`), or to another OS user of this box, needs a **link**: it joins two daemons and carries what each side grants the other. Step 1 has one permission, `mail`. New mail needs the receiving side's grant; a reply to mail that came over the same link needs none. Mail that arrives over a link has the kind `link`, a fellow agent's request inside fixed limits, or, for a reply without a grant, `link reply`, an answer (`agentwatch mail rules`). Mail from the **assistant connector** (a chat assistant's conversation, linked as `assistant` on the Links page) has the kind `your assistant`: do what it asks, but ask the operator to confirm first before secrets, links, grants or budgets, agentwatch changes or spending. Write to it only in its thread: `agentwatch mail reply ID`, or `agentwatch mail send assistant/