User guide
For installation, follow the Linux or macOS quickstart. This guide covers configuration, day-to-day commands, modes, Telegram control, and troubleshooting.
Install
Section titled “Install”my-ai-team is distributed as a release tarball. After purchase you receive a download link; unpack it anywhere and run its installer:
tar xzf my-ai-team-*.tar.gzcd my-ai-team./install.shThe installer:
- copies a frozen runtime into
~/.local/share/my-ai-team - links the
matcommand and its helpers into~/.local/bin, pointing them at that frozen install root, and writes a version stamp you can print withmat --version - writes no content under your personal
~/.claude; each agent’s home is built by the launcher on first launch, so nothing personal is rewritten - on systemd hosts, installs and starts the Telegram relay service automatically (and restarts it on every later install so the running daemon picks up new code). Where no per-user service manager is reachable — some SSH and container hosts, and Windows Git Bash — it prints a note and skips the step; a failed service step never fails the install.
On the first interactive Linux or macOS install, it opens the platform quickstart. Upgrades, reinstalls, and non-interactive installs skip it.
Set AVAILABLE_AGENTS=claude,codex before running ./install.sh to provision
only selected backends. When AVAILABLE_AGENTS is unset, the installer scans for
existing backend config directories and provisions the ones it finds.
Agent homes are provisioned on first launch, not at install time, so an upgrade takes effect the next time you start a session. Provisioning is idempotent and self-repairing: it renders the role’s prompt, writes the settings a long-running unattended agent needs, installs mat’s safety guards for backends that support them, and marks the launched checkout trusted so the agent does not stop on a trust prompt.
One of those guards shapes the action requests an agent sends you: a
notify-user --action is held until its message either lists what the agent
already tried under a Tried: header or asks for a choice under Decision:, so
an action notification states the attempts or the decision it needs from you. It
covers Claude, Codex, Copilot, and pi agents; OpenCode and freebuff are not
covered. An agent can bypass it for one command with MAT_ALLOW_BARE_ACTION=1.
To upgrade later, run mat upgrade (see Upgrading); you can delete
the unpacked tarball directory once the install has completed.
Backend and user config
Section titled “Backend and user config”mat reads optional user config from ${XDG_CONFIG_HOME:-~/.config}/mat/.
backends.jsonadds or overrides backend registry entries.credentials.conflets two same-kind backends share one login.user.confis a deprecated fallback forMAT_DEFAULT_BACKEND/MAT_USER_NAME.
Your default backend and display name are read from git config — the canonical store:
git config --global mat.defaultBackend claudegit config --global mat.username "your name"git config resolves system → global → local, so a per-repo override applies inside that repo while global values apply everywhere else.
One-command setup: mat setup
Section titled “One-command setup: mat setup”mat setup --backend <kind> --make-default writes the canonical registry
entry for one supported backend family and makes it your default — the
first-run path used by the agent-assisted quickstart.
It is non-interactive and safe to rerun:
- The entry is inserted, or reconciled in place if a backend with that
nickname already exists. Every other entry in
backends.json— including unrelated credential variables — is preserved untouched, and the generated file is the same human-readable JSON the installer writes. - Token/API-key families (
claude,opencode) name the environment variable that will hold your token or key (CLAUDE_CODE_OAUTH_TOKEN,OPENCODE_API_KEY) and never read, print, or store a secret value. Native-login families (codex,copilot,pi,freebuff,grok,commandcode) get the native-login shape; authenticate later with the CLI’s own login flow (codex,gh auth loginfor Copilot,piwith/login,freebuff login, the Grok Build TUI,cmd login). --make-defaultsetsgit config --global mat.defaultBackend <kind>after the registry write succeeds.- Fail-closed: an unsupported kind or a malformed existing
backends.jsonexits non-zero with a diagnostic and changes nothing.
After setup, run mat doctor for the short list of steps only you can do
(backend login/token, GitHub auth, license activation, Telegram credentials).
The zero-config path. The built-in registry contains a single Claude
backend. Run claude setup-token, export the result as CLAUDE_CODE_OAUTH_TOKEN
(mat sources ~/.bashrc.secret at launch, so exporting it there is enough), and
you are ready — no backends.json required. Edit backends.json only for
multi-account setups, third-party endpoints, or other backend families.
A native Claude backend is token-auth only: with no token it fails fast at launch rather than starting unauthenticated.
To reuse a multi-account setup, copy examples/mat/backends.json into
~/.config/mat/backends.json and trim the entries you do not want.
examples/mat/user.conf shows the supported user.conf exports.
backends.json format
Section titled “backends.json format”backends.json is a single object with a backends array. Each element is a
backend object:
{ "backends": [ { "nickname": "ccw", "config_dir": "claudew", "auth_var": "CLAUDE_CODE_OAUTH_TOKEN_CCW", "prompt_file": "CLAUDE.md", "kind": "claude" }, { "nickname": "ccd", "config_dir": "clauded", "auth_var": "DEEPSEEK_API_KEY", "prompt_file": "CLAUDE.md", "kind": "claude", "base_url_var": "DEEPSEEK_CLAUDE_BASE_URL" } ]}| Field | Meaning |
|---|---|
nickname |
Required. The CLI alias and the slot name used in commands and locks. |
config_dir |
Required. Home/config directory stem under $HOME/.<config_dir>. Kept independent from nickname on purpose. |
auth_var |
Optional. Name of the environment variable holding the auth token or API key. Omit it (or set null) when the backend uses its own login flow. A claude or leg backend is token/API-key auth only. |
prompt_file |
Required. Prompt filename written into that backend’s home (CLAUDE.md for Claude, AGENTS.md for Codex, and so on). |
kind |
Required. Backend family: claude, codex, copilot, pi, opencode, freebuff, grok, commandcode, agy, or leg. The family decides which modes a backend can run — see the notes below and each family’s subsection. |
base_url_var |
Optional. Name of the environment variable whose value becomes the backend’s API endpoint. Omit it for the default endpoint or a native login flow. |
base_url |
Optional literal endpoint for an explicit Codex or pi provider, or a Grok BYOK backend. Use base_url or base_url_var, not both. |
tier |
Optional. strong or weak. A weak backend is refused for explore and audit (those roles produce the tickets everyone else works from) but stays usable as a Developer under a strong reviewer. Defaults to strong. |
default_model |
Optional. Model applied at launch, passed through verbatim. Omit it to launch with the backend’s default. |
default_effort |
Optional. Reasoning-effort level applied at launch. Omit it or set it to an empty string and effort falls back to high. Claude changes it in-session with /effort; leg applies it through LEG_THINKING. |
context_window_size |
Optional. Effective context window for auto-compaction: an integer (250000) or a k/m shorthand (350k, 1m). Omit it and mat sets nothing, except on leg, which then defaults to 200000 (see leg workers). |
max_output_tokens |
Optional for kind: "pi". Positive integer or k/K shorthand (base 1000), such as 96000 or 96k. Defaults to 64000 for pi model entries; an invalid value stops pi from launching. |
The auth and base-url variables name environment variables; mat reads their
values from ~/.bashrc.secret when they are not already exported. mat records
the variable name, never the value.
A malformed backend object (a missing required field, an unsupported kind, an
invalid value) is warned about and skipped; the rest of the file still loads.
Invalid JSON, or a missing backends array, falls back to the built-in Claude
backend with a warning rather than aborting.
MAT_USER_NAME is rendered into prompt files anywhere {{MAT_USER_NAME}}
appears; when unset it falls back to the user.
Codex backends
Section titled “Codex backends”Codex has five routes. The classifier walks them in order — gateway_selector
wins over every other field, then endpoint, then auth_var alone, then legacy.
Every route ends up writing a real role-local Codex config (gateway,
official-auth-only, direct custom) or keeps the native auth.json /
config.toml credentials (legacy native).
-
Gateway-selected. A non-empty
gateway_selectorfield routes the entry through the gateway origin:MAT_CODEX_GATEWAYwhen set and non-empty, withMAT_ANTHROPIC_GATEWAYas the fallback — an operator running one gateway for several protocols exports the origin once. The selected value becomes the role’sbase_urlwith only one trailing/stripped and, when enabled, the worker namespace inserted: the gateway owns URL handling and joins Codex’s/responsesrequest path onto its pool member’s base URL, so any origin shape the gateway accepts works, path prefixes included. Only when both variables are unset or empty does the launch fail, with a diagnostic naming both. The role is provisioned with a fixed mat-owned provider identitymat-codex-gatewayand the selector value carried asMAT_CODEX_API_KEY. The declaredauth_var,base_url,base_url_var,model_provider,wire_api, andmodel_catalog_jsonare deliberately ignored on this route — set them only as comments documenting the upstream subscription.MAT_GATEWAY_QUOTA_URLis scrubbed on every Codex route; the gateway route re-establishes it from the fallback origin so the statusline queries the right pool’s quota. Team/duo launches also scrub any inheritedOPENAI_API_KEY,OPENAI_BASE_URL, andCODEX_ACCESS_TOKENfrom the pane environment at the supervisor boundary, before the Codex TUI starts.{"nickname": "codex-gateway","config_dir": "codex-gateway","prompt_file": "AGENTS.md","kind": "codex","gateway_selector": "chn"}export MAT_CODEX_GATEWAY=http://gateway.local:8080 -
Official auth-only. A non-empty
auth_varwith no endpoint and no other provider metadata uses the official Responses endpointhttps://chatgpt.com/backend-api/codexwith the provider identitycodex-team. Nobase_url/base_url_varis required and no model catalog is copied.{"nickname": "codex-account-a","config_dir": "codex-account-a","auth_var": "CODEX_TOKEN_ACCOUNT_A","prompt_file": "AGENTS.md","kind": "codex"} -
Direct custom. Any entry with an endpoint (
base_urlorbase_url_var) is provisioned as a role-local Responses provider with the operator’s chosen endpoint,model_provider, andwire_api. Declare the key source and endpoint rather than putting the token inbackends.json:{"nickname": "deepseek","config_dir": "deepseek","auth_var": "DEEPSEEK_API_KEY","base_url_var": "DEEPSEEK_BASE_URL","prompt_file": "AGENTS.md","kind": "codex","model_provider": "deepseek","wire_api": "responses","model_catalog_json": "~/.codex/models.json"} -
Personal-subscription legacy. An entry with neither
auth_varnor provider metadata keeps Codex’s own login: putconfig.tomlin the backend’s bare canonical home~/.<config_dir>/, and every role home links to it automatically. Add one entry per account to run multiple accounts. -
Invalid direct. An entry with provider metadata but no endpoint is refused before launch with an actionable diagnostic. Either set an endpoint or strip the metadata so the entry resolves to a valid route.
Every Codex launch starts in Standard mode; /fast on inside a running session
switches that one session to Fast.
For interactive launches, mat disables Codex’s automatic TUI conversation recaps
with a process-scoped setting, so recap requests are not scheduled and do not
consume additional model tokens. Manual /recap remains available. This does
not disable automatic context compaction; a configured context_window_size
still controls the Codex compaction threshold.
pi workers
Section titled “pi workers”pi is a multi-provider AI coding assistant. Add a kind: "pi" entry:
{ "nickname": "pi", "config_dir": "pi", "prompt_file": "SYSTEM.md", "kind": "pi" }The prompt_file must be SYSTEM.md — pi loads its system prompt from that
filename and ignores any other.
pi backends have three credential modes:
- Shared login (default). An entry with no provider metadata links every
role to the bare agent dir
~/.<config_dir>/agent: log in once there (run pi with no role and use/login) and every pi role inherits that auth, model, and settings config. - Explicit provider. An entry that declares any of
auth_var,base_url,base_url_var,model_provider, orwire_apiis provisioned per role instead. mat writes a role-localmodels.jsonwhen an endpoint is declared and detaches the sharedauth.jsonlink — an explicit entry gives up the shared/loginauth set, while the shared settings file stays linked. The key is never written to disk or the command line:models.jsonstores only the$VARIABLEreference pi resolves at request time, and mat exportsauth_varfor it. - Gateway. An entry with a non-empty
gateway_selectorroutes through the companion gateway (see Light Anthropic gateway) with no vendor key on the worker. The selector is the only credential: mat exports it asMAT_PI_API_KEYand writes a role-localmodels.jsonprovider block whosebaseUrlisMAT_PI_GATEWAYor, when that is unset,MAT_ANTHROPIC_GATEWAY, with the worker namespace inserted when enabled.auth_var,base_url,base_url_var, andmodel_providerare ignored.wire_apiis optional and defaults toanthropicon this route.default_modelis required. The recommended form is a bare model id (claude-sonnet-4-5): mat names the blockmatand launches pi with--model mat/<model-id>. A<prefix>/<model-id>names the block<prefix>instead; only the first/splits, somat/vendor/modelroutes modelvendor/model. A prefix that matches a pi built-in provider also routes to the gateway, but a credential stored for that provider by an in-pane/loginwould take precedence, andmat doctornotes any prefix other thanmat. An entry with nodefault_model, or with an empty model id (gw/), fails before pi starts, because pi would otherwise pick a model from a vendor provider. As with an explicit provider, the sharedauth.jsonlink is detached and the shared settings file stays linked.
{ "nickname": "pi-proxy", "config_dir": "pi-proxy", "auth_var": "PI_PROXY_KEY", "base_url_var": "PI_PROXY_URL", "wire_api": "anthropic", "default_model": "myproxy/claude-sonnet-4-5", "context_window_size": "256k", "max_output_tokens": "96k", "prompt_file": "SYSTEM.md", "kind": "pi"}wire_api names the wire protocol, not the vendor: anthropic (Anthropic
Messages), openai (OpenAI Chat Completions — the format most
OpenAI-compatible proxies clone), responses (OpenAI Responses), or google
(Google Generative AI). When an endpoint is declared without wire_api, mat
defaults to openai; a gateway entry without wire_api defaults to
anthropic. One provider per entry: model_provider, when set, and
the provider/ prefix of default_model must agree — that agreement picks
the provider id written into models.json. model_catalog_json is not
supported for kind pi.
For gateway and endpoint-backed entries that set default_model, mat writes numeric
maxTokens from max_output_tokens, defaulting to 64000, and writes numeric
contextWindow from context_window_size when configured. As a session grows,
pi can clamp each request’s output allowance to fit the remaining context.
The selected endpoint must support the configured output cap. On a responses
wire, mat also writes compat.supportsMaxOutputTokens: false unless the entry
declares max_output_tokens, so pi sends no max_output_tokens and the upstream
default governs; some gateway-pool upstreams reject that parameter outright. These limits are
written only on a model entry: an endpoint without default_model keeps its
baseUrl-only provider block, and shared-login or auth-only routes do not gain one.
Each mat-managed model entry with default_model also sets pi’s reasoning flag
so the --thinking effort selected at launch reaches pi’s native request
builder. The control pi sends depends on the selected API:
wire_api |
pi API | Thinking control |
|---|---|---|
unset on a gateway, or anthropic |
anthropic-messages |
Anthropic thinking (budget-based unless pi selects adaptive thinking) |
unset on an explicit endpoint, or openai |
openai-completions |
pi’s detected compatibility format, commonly reasoning_effort |
responses |
openai-responses |
reasoning.effort |
google |
google-generative-ai |
Google thinkingConfig |
mat does not override pi’s thinking format. If an endpoint rejects the controls pi selects for its API, pi reports the request error and mat does not disable thinking as a fallback.
{ "nickname": "pi-gateway", "config_dir": "pi-gateway", "gateway_selector": "chn", "default_model": "claude-sonnet-4-5", "prompt_file": "SYSTEM.md", "kind": "pi"}default_model and default_effort apply at launch like on any other backend:
the model forwards as --model <provider/model-id> (pass the model id exactly
as pi expects it, e.g. deepseek/deepseek-v3), and the effort forwards as
--thinking <effort> — always, falling back to high when default_effort is
unset. mat never passes --provider or an API key.
If pi is configured with an Anthropic-compatible provider, you can instead
register it as kind: "claude" with a base_url_var pointing at pi’s endpoint —
no new config, reusing Claude’s auth wiring.
opencode workers
Section titled “opencode workers”OpenCode workers support both OpenCode v1 and v2. mat detects the installed
major version at each launch (an unrecognised version is treated as v2) and
mat doctor reports it. On v2 mat starts the TUI with --auto --standalone
(a private server that exits with the pane) and selects the model and effort
through the role config as <provider>/<model>#<effort> from default_model
and default_effort. On v1 mat starts it with --auto --agent build --model <model> and applies the effort through agent.build.reasoningEffort.
OpenCode workers are interactive-only. Register one with an API-key variable and the nested prompt path OpenCode reads as its role instructions:
{ "nickname": "opencode", "config_dir": "opencode", "auth_var": "OPENCODE_API_KEY", "prompt_file": "config/opencode/AGENTS.md", "kind": "opencode", "default_model": "opencode/deepseek-v4-flash-free"}Each OpenCode role gets an isolated config tree, so its own config, skills, and
AGENTS.md stay per-role. mat writes the provider config with an env-backed key
and never puts the key on the command line. For an OpenAI-compatible BYOK
endpoint, add base_url_var:
{ "nickname": "opencode-byok", "config_dir": "opencode-byok", "auth_var": "OPENAI_API_KEY", "base_url_var": "OPENAI_BASE_URL", "prompt_file": "config/opencode/AGENTS.md", "kind": "opencode", "default_model": "opencode/my-model"}OpenCode has no write guard: the restricted modes (explore, audit, review)
continue after a warning that the agent has unrestricted write access, and the
headless delivery modes refuse an OpenCode backend outright. Keep OpenCode on
interactive modes, or leave worktree isolation on (the default) so a stray write
lands in a throwaway checkout rather than your main tree.
freebuff workers
Section titled “freebuff workers”freebuff workers are interactive-only. Register one with:
{ "nickname": "freebuff", "config_dir": "freebuff", "auth_var": "-", "prompt_file": ".AGENTS.md", "kind": "freebuff", "tier": "strong"}The prompt_file must be .AGENTS.md — the leading dot is load-bearing;
freebuff reads a home-level dotfile and never a plain AGENTS.md. There is no
auth_var: run one interactive freebuff login from the operator’s canonical
home (~/.config/manicode/), and every role links that credentials.json.
The release files (freebuff, its metadata, tree-sitter.wasm, and rg) are
also converged and hardlinked between the canonical home and each role, so a
role that downloads an update promotes it on the next launch without making
every other role download another copy. Role settings, history, projects,
analytics ID, and instance-owner state remain independent real files.
Three limits are worth knowing before you register one:
- One active session per freebuff account. Run only one freebuff instance at a time; concurrent-instance safety is not provided. Two panes on the same account otherwise evict each other — the later one takes over and the earlier stops responding.
- No write guard. Like OpenCode, freebuff runs restricted modes full-auto after a warning; keep it on interactive modes and leave worktree isolation on.
- Hardlink/symlink degradation. On different filesystems, or on Windows without symlink privilege, mat leaves role-local copies/files in place rather than copying stale credentials. Log in separately in that degraded case. The wrapper’s rename-based binary update briefly breaks hardlinks and is repaired on the next launch.
freebuff’s model catalog is re-tuned faster than a mat release, and a model can
be metered or closed for part of the day, so pin capability to the tier field
in backends.json rather than to a model name. Declare weak where the catalog
is limited — explore and audit gate on it.
Command Code workers
Section titled “Command Code workers”Command Code workers run the cmd CLI headless-first: the duo, team, and
caucus delivery relays serve it, and mat doctor checks it like any other
backend. Register one with:
{ "nickname": "commandcode", "config_dir": "commandcode", "auth_var": null, "prompt_file": ".commandcode/AGENTS.md", "kind": "commandcode", "tier": "strong"}The equivalent compact registry entry is
commandcode:commandcode:-:.commandcode/AGENTS.md:commandcode.
There is no auth_var: authenticate once with an interactive cmd login in
your own home. Every role home links that auth.json, so one login serves all
roles. A worker’s constitution renders at <role home>/.commandcode/AGENTS.md,
its skills project to <role home>/.commandcode/skills, and its session
transcripts stay under the role home — so a worker never reads or writes your
interactive Command Code history.
Two limits to know before you register one:
- No write guard. Like OpenCode and freebuff, Command Code runs restricted modes full-auto after a warning: mat has no hook surface to install a policy into. Keep worktree isolation on (the default) so a stray write lands in a throwaway checkout.
- Interactive panes are not wired. mat drives Command Code headless. The
tmux and local pane drivers, including their relay send paths, are not
adapted to the
cmdTUI in this release — use Command Code for the headless delivery modes, and pick a pane-backed backend for interactive drivers.
agy workers
Section titled “agy workers”agy workers run the Google Antigravity CLI (agy) headless-first: the duo,
team, and caucus baton relays serve it. Register one with:
{ "nickname": "agy", "config_dir": "agy", "auth_var": null, "prompt_file": ".gemini/GEMINI.md", "kind": "agy", "tier": "strong"}The equivalent compact registry entry is
agy:agy:-:.gemini/GEMINI.md:agy:-:strong.
There is no auth_var: sign in once with agy’s own login. mat stages no token
for agy and does not copy or link its Google account file into role homes; each
role runs with $HOME set to its own role home, so its .gemini state and
conversations stay separate from yours and from other roles.
Every agy turn passes --dangerously-skip-permissions, so agy never stops to
ask for tool approval, and one --add-dir for each workspace the turn may
use. agy only grants access to directories passed with --add-dir: starting it
in a directory or naming a --project is not enough.
Restricted modes (explore, audit, review, duo-review, live) get a
PreToolUse write guard. mat keeps one hook named mat-role-guard in
<role home>/.gemini/config/hooks.json. It refuses tracked-file writes and
delivery commands, and read-only tools and unrestricted roles keep their
normal behavior. Other hooks in that file are left as they are. If the file is
not valid JSON, mat refuses to launch the role and leaves the file in place.
A workspace .agents/hooks.json is not used as the role guard.
This release is headless/Baton-first. The tmux and local pane drivers are not adapted to agy’s interactive TUI, so choose a pane-backed backend for interactive drivers.
leg workers
Section titled “leg workers”leg workers run the leg CLI headless-first: the duo, team, and caucus baton relays serve it. Register one with:
{ "nickname": "leg", "config_dir": "leg", "auth_var": "ANTHROPIC_API_KEY", "prompt_file": "AGENTS.md", "kind": "leg", "tier": "strong"}The equivalent compact registry entry is
leg:leg:ANTHROPIC_API_KEY:AGENTS.md:leg:-:strong.
leg is token/API-key auth: auth_var is required, and mat refuses to start a
turn when it is missing or its value is empty. The credential reaches leg only
through its environment, never its command line. mat doctor checks that
auth_var resolves and reports how to fix a missing or empty value without
showing the credential. Two credential types work:
- An Anthropic API key: name the variable
ANTHROPIC_API_KEY, and its value is passed as an API key. - An Anthropic-compatible provider’s key: use any other variable name together
with a
base_url_varnaming the provider’s endpoint. The key is passed as a bearer token (ANTHROPIC_AUTH_TOKEN) and the endpoint asANTHROPIC_BASE_URL.
A Claude Code subscription token (from claude setup-token) is not supported:
leg is passed such a token as a bearer token, but every turn fails with a
rate_limit_error. Other Anthropic credential variables in your shell do not
reach leg.
Every leg turn carries the role’s instructions (the entry’s prompt_file in
the role home) as its system prompt. A turn whose role instructions are missing
or empty fails before leg starts.
default_effort reaches leg through LEG_THINKING. Leg accepts off,
minimal, low, medium, high, xhigh, and max; when the field is
omitted or empty, mat uses high. An already-exported LEG_THINKING value
wins over the registry. If the registry contains another non-empty value, mat
reports it once and leaves LEG_THINKING unset so leg uses its own default.
Each role keeps its own leg session store under its role home, so roles never share a conversation. A task’s later turns continue its session; if leg reports that session is gone, mat starts that turn in a new session once.
The backend’s context_window_size sets leg’s compaction budget
(LEG_CONTEXT_WINDOW_TOKENS, required by leg 0.34.1 and later): omit the field
and mat exports 200000, the standard Claude window. mat sets the budget
without clamping requests to the model’s real limit — on a 1M-window model a
value this small only compacts earlier — and a value you export as
LEG_CONTEXT_WINDOW_TOKENS yourself wins over both the field and the default.
Restricted modes (explore, audit, review, duo-review, live) get a
write guard through leg’s pre-tool hook. It refuses tracked-file writes and
delivery commands from leg’s bash, write, and edit tools; reads and
unrestricted roles keep their normal behavior.
This release is headless/Baton-only. The tmux and local pane drivers are not
adapted to leg, so choose a pane-backed backend for interactive drivers.
mat setup does not write leg entries; add one to backends.json by hand.
HOME-mapped worker paths
Section titled “HOME-mapped worker paths”freebuff, Command Code, and agy use HOME as their native home variable. For these
workers, mat sets $HOME to the per-session role snapshot so each role has an
isolated native configuration. That snapshot is not the operator’s real home,
and a ~/ path must not be assumed to reach it.
Use these pinned anchors for operator-live paths:
GIT_CONFIG_GLOBAL— the operator’s Git global configuration.GH_CONFIG_DIR— the operator’s GitHub CLI configuration.MAT_GH_AUTH_CONFIG_DIR— pins the same directory asGH_CONFIG_DIR(or your explicit value), so theghcredential gateway can find your GitHub CLI accounts from the worker. mat sets it only when that directory is readable; without one, the worker’sghrefuses with the usualgh auth loginremedy.MAT_HOST_SECRET_FILE— the operator’s host secret file.MAT_STATE_HOME— mat’s operator state directory.MAT_WORKTREE_ROOT— the operator’s worktree root.
Non-git directories copied into the snapshot may be stale. Git-tracked snapshot content refreshes with the snapshot contract; live operator content belongs in git or behind an explicit anchor. This boundary does not change snapshot copy, credential linking, or HOME mapping behavior.
grok workers
Section titled “grok workers”Grok workers run the interactive Grok Build TUI. Register one with native per-role login:
{ "nickname": "grok", "config_dir": "grok", "auth_var": null, "prompt_file": "AGENTS.md", "kind": "grok", "tier": "strong"}The equivalent compact registry entry is
grok:grok:-:AGENTS.md:grok:-:strong. Native login stores one identity in the
canonical ~/.grok/auth.json; role homes link that file, so authenticate once
in the canonical Grok home rather than logging in separately for every role.
Grok supports three distinct authentication paths:
- Native login: omit
auth_var,base_url, andbase_url_var(or setauth_vartonull). - xAI API key: set
auth_varto the key variable, such asXAI_API_KEY, and omitbase_urlandbase_url_var. - BYOK: set
auth_varplus either a literalbase_urlor abase_url_var. mat exports both values asXAI_API_KEYandGROK_MODELS_BASE_URL.
For example, an xAI-key backend can use "auth_var": "XAI_API_KEY"; a BYOK
backend can use "auth_var": "MY_GROK_KEY" with either
"base_url": "https://provider.example/v1" or
"base_url_var": "MY_GROK_BASE_URL".
Set context_window_size to configure Grok’s auto-compaction window when
default_model is set. Grok applies the value to that selected model; without
default_model, mat leaves the native configuration unchanged.
The key and endpoint are read from the environment or ~/.bashrc.secret when
needed and never appear on the command line. Ambient XAI_API_KEY and
GROK_MODELS_BASE_URL are scrubbed unless the backend declares the matching
path; a declared endpoint without a usable key, or an invalid endpoint, fails
closed rather than falling back to native login.
Grok workers serve both interactive sessions and the headless delivery modes (the duo, team, and caucus baton relays). Headless grok turns are cold: each turn runs on its own, with no session trail carried between turns, so a task rebuilds context from the durable artifacts named in its prompt rather than resuming an earlier conversation. Restricted interactive modes use a PreToolUse write guard: tracked-file edits and delivery commands are refused, while unrestricted roles keep their normal behavior. Each Grok role keeps mat’s skills in its own native skills directory, while Claude and Cursor compatibility scans are disabled for the role so their operator-level instructions, skills, hooks, and rules are not inherited.
Light Anthropic gateway
Section titled “Light Anthropic gateway”Set the MAT_ANTHROPIC_GATEWAY environment variable to route native Claude
backends through a local companion gateway instead of talking to
api.anthropic.com directly. The gateway is a separate companion service, not
bundled with mat — this is only the integration point. Export it where the
launcher will see it (e.g. ~/.bashrc.secret):
export MAT_ANTHROPIC_GATEWAY=http://127.0.0.1:9949Set it to the root origin of your gateway — no /v1 or sub-path (Claude Code
appends /v1/messages itself).
- A backend with a
gateway_selectorfield routes through the gateway (the selector is sent as the auth token, so abase_url_varon that backend is ignored). Point several workers at the same selector — most usefully the pooledautoselector — to let the gateway handle failover across them. - A backend with a
base_url_varand nogateway_selectorignores the gateway and uses its vendor endpoint directly. - A backend with
auth_var: nullis selector-only: valid on the gateway path, and it fails fast at launch when the gateway is unset.
With the gateway unset, every backend behaves exactly as it does without this feature.
MAT_ANTHROPIC_GATEWAY serves Claude and Copilot backends directly, and is
also the fallback origin for Codex and pi backends on the gateway route (see
the Codex backends and pi workers sections above): a Codex
gateway_selector entry uses MAT_CODEX_GATEWAY when it is set and
MAT_ANTHROPIC_GATEWAY otherwise, and a pi gateway_selector entry uses
MAT_PI_GATEWAY the same way. The variables carry a gateway origin, not a
wire path — each client appends its own protocol path (/v1/messages for
Claude Code, /responses for Codex, the path of its wire_api protocol for
pi) and the gateway joins that inbound path onto the selected pool
member’s base URL, so one gateway origin serves every protocol at once.
A claude setup-token credential cannot read usage directly: its OAuth token
carries inference scope only, and Anthropic’s usage API requires user:profile.
mat quota reports this as error: token lacks user:profile scope and points
at MAT_ANTHROPIC_GATEWAY — configuring the gateway is the fix, since the
gateway’s own quota endpoint doesn’t need that scope.
mat quota (from your shell) and the agent statusline both read usage from the
gateway when it is active, printing per-window utilization and reset countdowns.
A gateway behind HTTP Basic auth is supported for Claude backends: declare the
credential in user:pass form as MAT_ANTHROPIC_GATEWAY_BASIC_AUTH next to the
gateway URL, and mat carries it through both the launch and the quota fetch
without ever placing it on a command line.
On this Claude Code path, mat pre-seeds the active Claude home’s remembered
rejection for the gateway selector, preventing the CLI’s selector confirmation
prompt while keeping ANTHROPIC_API_KEY available to Agent-SDK sub-agents.
A Copilot backend cannot traverse a Basic-auth-protected gateway: the Copilot
CLI’s BYOK provider has no custom-header channel to carry the edge credential,
so a Copilot pane on the gateway path fails fast at launch when
MAT_ANTHROPIC_GATEWAY_BASIC_AUTH is set instead of silently 401ing at the
edge. Relax the gateway edge to also accept Bearer selectors, or route the
Copilot backend outside that gateway. A pi gateway backend has the same limit
and fails fast the same way.
Per-worker gateway URL
Section titled “Per-worker gateway URL”The worker namespace is off by default: every gateway client URL is the plain
gateway URL. Set MAT_GATEWAY_WORKER_NAMESPACE=1 to enable it. Every client
URL that points at the gateway then carries the worker’s backends.json
nickname in the gateway’s reserved worker namespace, inserted between the
gateway origin and any path it has:
MAT_GATEWAY_WORKER_NAMESPACE=1MAT_CODEX_GATEWAY=http://127.0.0.1:8080/v1, nickname agent-a -> http://127.0.0.1:8080/_aqg/w/agent-a/v1Enable it only when the gateway supports the worker namespace. It is useful
only for a gateway pool configured to spread workers (concurrency > 1). A
gateway without that support forwards the prefixed path upstream, and every
gateway-routed agent fails. Any value other than 1 leaves the namespace off.
Changing the setting re-provisions a Codex or pi role’s provider file on its
next launch.
With the namespace enabled, the URL is derived at every launch — there is no
extra field to set and backends.json is never rewritten. The nickname is routing identity only: the
gateway uses it to keep each worker on its own pool member, while
gateway_selector still selects the pool and is still the only credential.
Claude, Copilot, Codex, and pi gateway launches all use the derived URL.
The statusline’s quota endpoint follows it on the backends that have one: with
the namespace active, a Claude, Codex, or pi launch adds &worker=<nick> to
the quota URL it exports, so the quota that statusline reports is that worker’s
instead of the whole pool its gateway_selector shares. With the namespace off
— and with a nickname the namespace leaves out — the quota URL carries no
worker parameter. mat quota, run from your shell, always reports the pool.
A Copilot gateway launch exports no quota endpoint at all, so there is nothing
to qualify there.
A nickname that is not a safe single URL segment — anything outside letters,
digits, ., _, ~, and -, a leading ., or longer than 64 characters —
is left out: that worker uses the plain gateway URL and the gateway’s shared,
non-per-worker routing.
Configuration reference
Section titled “Configuration reference”These mat.* git-config keys show their environment aliases, purpose, and
defaults. Unless noted, Git resolves them from local to global configuration.
| Key | Env var | What it does | Default |
|---|---|---|---|
mat.defaultBackend |
MAT_DEFAULT_BACKEND |
Backend used when a command names none | claude |
mat.username |
MAT_USER_NAME |
Your display name in prompts and notifications | — |
mat.enableWorktree |
MAT_ENABLE_WORKTREE (or --worktree / --no-worktree) |
Run each mode in its own worktree | on |
mat.autoStart |
-a / -n flags |
Start seeking work on launch | on |
mat.autoRefine |
MAT_AUTO_REFINE |
Deprecated and inactive; unready backlog refinement belongs to resident mat explore --auto-refine |
— |
mat.autoNotifyUser |
MAT_AUTO_NOTIFY_USER |
Emit the built-in PR/merge status notifications | on |
mat.verbose |
MAT_VERBOSE (or --verbose) |
Show full per-step launch progress | off |
mat.driver |
MAT_DRIVER |
Session driver: tmux, local, or baton (Windows Git Bash: local or baton only — see below) |
tmux (Windows Git Bash: baton) |
mat.ghAuthOwnerMap |
— | Repeatable global mapping from owner=login (or host/owner=login) to the GitHub identity used by Baton |
— |
mat.ghAuthTokenVar |
— | Repeatable global mapping from login=ENV_VAR_NAME to a static credential variable |
— |
mat.ghAuthTokenFile |
— | Repeatable global mapping from login@owner=/absolute/path or login=/absolute/path to a caller-refreshed credential file (Windows Git Bash also accepts drive-absolute paths) |
— |
mat.auditPollMinutes |
MAT_AUDIT_POLL_MINUTES |
Audit broad-sweep interval, in minutes | 300 |
mat.personalPromptOverride |
— | Enable user-global prompt overrides (see Prompt overrides) | off |
mat.personalSkillsOverride |
— | Let a personal skill override a product skill of the same name | off |
mat.pairIdleCheck |
MAT_PAIR_IDLE_CHECK |
Enable unified idle-recovery ladders for tmux and Baton; false/0 disables them | on |
mat.stallWatchdog |
MAT_STALL_WATCHDOG |
Enable unified idle recovery for tmux and Baton, plus tmux human-wait checks; false/0 disables them | on |
mat.devopsHost |
MAT_DEVOPS_HOST |
ssh host that also receives every notify-user --action message (see Forwarding action messages) |
— |
Worktree selection has its own precedence: MAT_ENABLE_WORKTREE first (the
--worktree and --no-worktree flags set this value for that launch), then
mat.enableWorktree from local or global Git config, then the deprecated
per-mode key for adhoc or explore, and finally the default of on. The flag
therefore overrides an inherited environment value; this is the deliberate
exception to the table’s usual Git configuration resolution.
Set a key locally for one repo (git config --local mat.<key> <value>) or
globally for the machine (git config --global …). The two override toggles
(personalPromptOverride, personalSkillsOverride) are git-config only — no env
var — so a stale environment value can never flip them.
GitHub credentials on Linux, macOS and Windows Git Bash
Section titled “GitHub credentials on Linux, macOS and Windows Git Bash”install.sh installs a small gateway in front of gh and the HTTPS Git
credential helper for github.com. Agents, Baton workers and background tasks
all reach GitHub through it, and it reads the selected account’s credential
fresh on every command, so a rotated token applies to the next command with no
restart. If the credential is missing, empty or unreadable, the command is
refused rather than run unauthenticated. Which account and credential source a
Baton session uses is chosen at launch; after changing that choice (for example
removing a token variable or remapping an owner), run
mat baton restart <session> to choose again without relaunching. On Windows, use Git Bash; setup also
supports GitHub CLI’s native gh.exe and Git’s Windows credential-helper paths.
Agents whose shell is Windows PowerShell or PowerShell 7 also reach gh
through the gateway, with its normal output and exit status.
mat doctor shows a gh gateway line. If it warns or fails (for example after
upgrading gh, or when it reports the PowerShell gh forwarder incomplete),
run install-gh-gateway again.
Refreshable GitHub App credentials for Baton
Section titled “Refreshable GitHub App credentials for Baton”Baton normally uses the GitHub account available to gh. For a GitHub App
installation, map each repository owner to the App identity and point mat at
its caller-managed token file:
git config --global --add mat.ghAuthOwnerMap 'team-one=your-app[bot]'git config --global --add mat.ghAuthOwnerMap 'team-two=your-app[bot]'git config --global --add mat.ghAuthTokenFile 'your-app[bot]@team-one=/absolute/path/team-one-token'git config --global --add mat.ghAuthTokenFile 'your-app[bot]@team-two=/absolute/path/team-two-token'chmod 600 /absolute/path/team-one-token /absolute/path/team-two-tokenLogin and owner matching is case-insensitive. A matching login@owner entry
outranks a login=/absolute/path entry; the login-only form is a fallback when
that owner has no specific entry. The first valid entry for the same key wins,
and later duplicates warn. Each file must contain only its current installation
token. The caller refreshes it; mat does not mint tokens or handle an App private
key. Baton reads the selected file when choosing the account and again before
every worker turn, so replacing it takes effect on the next turn. If the chosen
file is unavailable or denied access at launch, mat warns and uses its normal
user-account selection without trying a less-specific file. If it becomes
unavailable during a session, that turn is refused instead of reusing an expired
token.
Token-file paths must be absolute for the current platform. On Windows Git Bash,
drive-absolute paths such as C:/tokens/app-token and C:\tokens\app-token are
accepted; drive paths are rejected on POSIX hosts.
The App appears on commits and pull requests as your-app[bot]. Its installation
needs contents:write, issues:write, and pull_requests:write for the normal
Baton delivery workflow.
Your first project
Section titled “Your first project”-
cdinto the repo you want to work on. -
Start the relay session:
Terminal window team
The command is identical on Windows Git Bash. team derives the project identity
from the Git repository root; no project registration or name is required.
Project naming
Section titled “Project naming”When a name is shown (session labels, worktree paths), mat uses lowercase
letters, digits, and -. It normalizes inputs like my_project to my-project.
Recommended names read like my-project, customer-portal, or ops2.
Day-to-day commands
Section titled “Day-to-day commands”The primary verbs
Section titled “The primary verbs”adhoc [-n] [--worktree|--no-worktree] [backend]audit [--session] [backend]team [-n] [backends...]explore [--worktree|--no-worktree] [backend]live [--domain <name>] [backend]duo [-n] [dev [reviewer]]caucus [proposer [challenger]] [--topic-file <path>]Run adhoc, audit, team, duo, and caucus from inside the repo you want
to work on. explore and live can run anywhere. backend defaults to your
mat.defaultBackend. team accepts 0–3 backends mapped to dev, plan, and
review in that order; duo accepts a dev and a reviewer.
Every mode launches its own tmux session, even when invoked from inside another
mat session. Reattach by rerunning the same command from the same repo, or
attach to the concrete session with tmux -L mat attach -t <session>. Stop one
by ending its agent or with tmux -L mat kill-session -t <session>.
Auto-start is the default: a fresh session claims work and begins on launch.
Pass -n / --no-auto-start to start cold and wake it later; -a /
--autostart forces it on for one launch. For a persistent opt-out, set
git config --local mat.autoStart false.
Preview a launch with --dry-run at any position — it prints what would be
spawned and exits without creating anything:
mat --dry-run duomat team --dry-run claude codexStatus queries
Section titled “Status queries”Use these commands to inspect running sessions and their state:
mat agents # every live agent session, grouped by project, with its statemat agents --json # machine-readable: one JSON object per sessionmat idle # only the standby (idle) sessionsmat audit status # audit poller, queue, and latest-wake diagnosticsBoth mat agents and mat idle work outside a Git repository. Each row is
labelled by state:
- standby — idle with a poller available, waiting for ready work (shows a best-effort idle duration).
- ghost — the supervisor is still running, but its recorded tmux pane is
gone. The row names its session, backend, and missing pane, for example
ghost (pane %1497 gone). It is excluded frommat idleand cannot receive pane-based messages. Listing it does not stop the supervisor or release its ownership protection. Older sessions whose tmux hosting cannot be confirmed retain their local status until restarted. - busy — the agent is actively running a turn.
- interactive — an
exploreorlivepane running a manual REPL, with no idle/busy claim.
If a supervised backend is exhausted or unusable before it can invoke its own
/respawn, an operator can request the same clean-slate boundary from any
shell by naming the session:
mat respawn <session>This is available only for supervised explore and adhoc sessions. The
operator trigger validates the session state and published child pidfile,
writes the existing respawn sentinel, and then reaps the backend so the
supervisor launches a fresh child; it refuses live, audit, team, duo,
and caucus sessions. It also refuses the resident auto-refine worker
(explore --auto-refine) on every driver: that worker runs its own supervised
session with its own lifecycle, which this verb does not address. If the
backend reap cannot be arranged, the sentinel is removed without signaling the
backend, and the command returns non-zero.
A team (dev/plan/review) or duo (duo-dev/duo-review) session hosts several
supervised panes in one tmux session, so its respawn is per pane. Name the pane
with --role:
mat respawn <session> --role <role><role> is the supervisor role token — dev, plan, review, duo-dev, or
duo-review (not the developer/reviewer send-relay aliases). The trigger
rebirths only that pane: it writes the role’s restart marker, reaps that pane’s
backend, and confirms the supervisor has published a fresh child; the session’s
other panes are untouched. The reborn pane comes back the way its role’s cold
launch does — a coordinator (plan/duo-dev) self-driving, a passive pane
(dev/review/duo-review) idle — and reloads the currently installed runtime.
If the delayed reap cannot be arranged, the marker is removed without signaling
that pane’s backend and the command returns non-zero.
--role is only for live team/duo sessions. It refuses an
explore/adhoc/live/audit/caucus session, a role outside the allowlist,
and a role with no live pane in that session (an already-down pane, or a role
that mode never runs — for example --role dev on a duo session). Each
refusal is non-zero with the reason on stderr and leaves every backend
untouched. If the reaped pane’s supervisor does not republish a fresh child
within the verify window, the command fails loudly and leaves the marker and the
session event-log trail in place for manual recovery.
Baton console
Section titled “Baton console”Baton sessions (duo and caucus on the baton driver) run headless, so there
is no tmux window to look at. The console is a web view of the same
information mat baton status prints, plus the relay timeline, a live tail of
each role, and the operator controls:
mat baton console # serve on 127.0.0.1:7380 and open a browsermat baton console --no-open # print the URL onlymat baton console --port 7391 # pick another portIt needs Node.js 20 or newer on your PATH. If Node is missing or too old the
command prints a one-line hint and exits non-zero without starting a server.
The left rail lists every session mat baton status reports, grouped by
project, with its status, per-role queue depth, and how long ago its last
operator event was — no events for a session that has not had one yet. A
[!!] marker flags a session that is not ready to progress (a role or its
worktree is not ready, or the session is stranded); the rail’s legend spells
out what [!!] and every status mean. Selecting a session shows its participants, a merged relay timeline — each message once, labelled with
its lane (pending, claimed, done, or outbox), with operator messages
highlighted and routine system wakes collapsed — and the controls below. When
a message is a handoff file reference, the console inlines the file’s contents
(up to 64 KiB) if the file is still there and reports it as gone if it is not;
it will only read handoff files from the scratch directory, never an arbitrary
path a message names. New messages appear within a few seconds without a
reload.
Timeline messages are labelled with names rather than transport identities.
Each role message is named for the role that sent or received it (dev,
reviewer, proposer, and so on), including older sessions whose messages
carry the generic agent-a/agent-b labels — the console works the role out
from the mailbox the message is in, and says unknown role (agent-a) rather
than guessing when it cannot. Your own messages are labelled with your
configured name (mat.username); with no name configured they read as the operator.
Crashed and stale sessions are hidden until you turn on the dead-session toggle. The list of dead sessions is read when the page loads. A session that stops while the page is open, such as one you stop from the console, joins it after the next refresh and stays there, so you can still open it and tear it down, until a teardown from the console removes it. A teardown removes the session from every open console page, not only the one you used, and a session you create again under the same name shows up again on its own, with no reload. Reload the page to re-read the full dead-session list. Once shown, a dead session’s row is dimmed and its own view carries a distinct “dead” badge instead of a live status pill. Clicking one always answers: a session the runtime can no longer describe still opens, showing its name, the kind and status the session list knows, and a note saying its detail is unavailable — so a dead session reads as dead rather than as a click that did nothing.
The session header also shows why a session is degraded, when it is: whether its roles and worktree are ready, and the reason a probe reported trouble. Each role column’s header carries the same per-role detail — whether its serve is alive and any probe error — when the session kind publishes it.
Under the session header there is one column per configured role — two for a
duo or caucus session, three for a team. Each column tails what that role is
doing right now: its recent user and assistant turns, with every tool call
shown as a single line such as Editing src/app.js, Reading README.md,
Running bash test/run.sh, or Searching TODO. A tool the console does not
recognise is named outright (Calling <tool>), and a call it cannot describe
at all says Unknown tool call running. Tool inputs and outputs are never
shown — the one-line form is all a column carries. Columns update within a few
seconds of the role writing a new turn. Each column’s header also names the issue
that role is working on (issue #<number>), or says no task claimed while the
role is on a turn that has not claimed one.
Not every backend records a transcript. A role that has none — a caucus role, or a role that has not started one yet — says so and shows its latest sent message instead of an empty panel.
A role’s conversation belongs to one issue at a time, and the console holds each column to that: when a role moves on to a new task, the previous task’s turns are cleared from its column and the column says which conversation it is waiting for. That is what makes a column readable at a task boundary — an empty one means the new task’s turn has not recorded anything yet, not that the old conversation is still running.
Under whichever of those a column shows, every column carries a serve stderr section: the tail of that role’s own serve log, which is where a backend that is failing to start says why. Nothing has to be clicked to see it, and it refreshes along with the turns above it, so a failing serve can be read against the turns it did or did not produce. It scrolls in its own bounded area, and it always says which case it is in: still loading, empty, unavailable with the reason, or a long log shown as its tail only.
When a role does have a transcript, its column carries a Rewind link that
opens that conversation in a local Rewind transcript viewer, assumed to be at
http://localhost:7373. Set MAT_REWIND_URL if yours listens elsewhere.
Clicking it checks whether Rewind is already running and starts it for you if
not, then opens the link once it answers; if Rewind never comes up, you get a
message telling you to start it yourself instead of a dead tab. On a host with
no Rewind installed at all, that same tab goes to the Rewind product page
(https://shukelabs.com/products/rewind/) rather than asking you to start
something that is not there.
The columns fit the window rather than scrolling sideways: they share the width evenly, wrap onto another row when the window is too narrow for all of them, and wrap long lines inside their own panel, so nothing is hidden off the right edge. The relay timeline sits below them in its own bordered, independently scrolling panel — scrolling it moves the timeline, not the page, and its position survives the periodic refresh, so reading back through a long conversation is not interrupted. A timeline you have scrolled to the bottom stays at the bottom as new messages arrive.
Some of this takes a moment, and the page says so rather than sitting still. Picking a session shows its name and Loading session… until its detail arrives — never the session you were looking at before — and a fetch that fails says so in its place. A fleet refresh and a running action both show in the status line at the top right, next to the live-connection state.
A slow host — native Windows especially, where mat baton status can take
over a minute — does not make the console lose a session. Once the page has
shown a session list or a session’s detail, a refresh that is slow or never
answers keeps what was last shown and labels it instead of replacing it with an
error:
- refreshing — a refresh is still running behind the data on screen (refresh still running; showing last known as of …). The page updates on its own when the refresh finishes.
- stale — the last refresh failed or timed out, so the data on screen is the last good answer, with the time it was read and why the refresh failed (stale as of … UTC: mat baton status –json timed out after …). The next successful refresh clears it without a reload.
- error — there was nothing to fall back on (the very first load failed), so the page says the read failed rather than showing an empty list.
The fleet’s state is shown in the status line; a session’s is shown as a note
under the session header. A session disappears from the list only when a
successful mat baton status stops reporting it, never because a refresh
timed out. Each read is bounded — about 30 seconds for a status scan and 15
seconds for one session’s detail, raised to 3 minutes and 2 minutes on Windows —
and a read that hits its bound is stopped together with everything it
started.
You can also intervene from the page. Each role column has its own message box
at the bottom, so you type under the role you are talking to and there is no
dropdown to aim wrong. Enter sends that column’s message; Shift+Enter
and Ctrl+J insert a newline instead, and Enter while an input method is
composing commits the candidate rather than sending. What you have typed
survives the page’s periodic refresh, including your cursor position, and it is
cleared only once the send actually succeeds — a refused or failed send leaves
the text there to retry. Inject to inbox is a separate control below the
columns that bypasses topology routing; it keeps its own role selector, and
stays disabled until you acknowledge the same unscoped-injection warning the
CLI prints. The action bar offers wake, pause or resume, stop, restart, and
teardown: stop asks for a confirmation dialog, and teardown — which is
destructive, stopping every service and removing the session’s state — is
directly clickable and asks for two confirmations in a row, naming the session
it will tear down before the final one. Cancelling either dialog sends nothing.
Every control runs the same mat baton verb you would type in a
terminal, and the verb’s own output — including its refusals, such as a caucus
session refusing wake — is shown verbatim afterwards, labelled with the role it
was routed to.
While a verb is running the controls are disabled and say which action it is — the column that is sending reads sending…, and the others say what is holding them. One action runs at a time, and the console refuses a second one until the first answers, so a repeated Enter or a double click cannot send twice — including after you switch to another session, where the busy note tells you which session is still working. Some verbs take minutes, so nothing is lost by waiting; if the request never answers at all, that is reported in the same place the verb’s own output would have been.
The console is deliberately local: it binds the loopback interface only and refuses requests that do not come from it. It never edits a session’s state, mailbox, or transcript itself — every session mutation is the existing verb, which keeps writing the operator event trail. The one exception is the Rewind link’s own start-it-for-me step, which is not a session mutation at all. Because it is unauthenticated, do not expose the port beyond your own machine.
mat backlog — delivery queue view
Section titled “mat backlog — delivery queue view”mat backlog prints a read-only snapshot of open issues by lifecycle, followed
by open PRs. It never mutates anything; use mat next-work for work selection.
mat backlog # human-readable listingmat backlog --json # JSON object (repo, counts, issues[], prs[])mat backlog ready # show only the ready sectionmat backlog --repo owner/name # override repo slugEach issue lands in one bucket: umbrella (a tracker), delivery (claimed),
refining, blocked, ready (claimable now), backlog, or other. The
ready section is byte-identical to what mat next-work picks from.
mat doctor
Section titled “mat doctor”Use one read-only command to name every unmet prerequisite:
mat doctorIt checks, one report line each:
bash≥ 4.3, andtmux/git/jq/curlonPATH.- On Windows Git Bash,
jqmay be a nativejq.exe(preferred) or an npm/pnpm global-bin script shim. mat runs a pnpm shim’s JavaScript target throughnodewith the arguments untouched; any other script shim runs with Git Bash path conversion on and a warning that path-likejqarguments may be rewritten. Ifjqcannot be launched at all (for example aCannot find moduleerror), the backend check reports that withjq’s own error and tells you to reinstalljq, rather than blamingbackends.json. python3andPyYAML(used by the GitHub issue helper), with a platform install hint when python3 is present but theyamlmodule is missing.ghpresent and authenticated — an unauthenticatedghotherwise shows up later as an agent dying on its first issue or PR call.- The installed
ghversion against the floor the Reviewer’s merge PR read needs (>= 2.72.0) — below it, awarnnames the required version rather than letting the read fail as an unrelated-looking error. ~/.local/binonPATH, and every installed command resolving back to this install root — so a half-installed or stale link set is named.- The installed version and activation state (no network; the license key is never printed).
- The resolved default backend: which registry entry is in effect and whether its auth actually resolves — a token variable, or gateway routing.
- Telegram credentials and whether the relay service is loaded and active.
- The super-global
~/.claude/CLAUDE.md: benign (absent or empty) or populated. See the managed Claude file. - On Windows Git Bash: the
bash.exemat exports so Claude Code picks the Bash tool, not PowerShell — see Windows Git Bash: agents get the Bash tool, not PowerShell; whether the activegitcan honour the configured SSL backend — see Windows: one Git for Git Bash and the MSYS2 shell; and whether/tmpresolves to one directory across bash, native tools, and the agent’s Read/Write tools.
Three verdicts, one exit rule:
| Verdict | Meaning | Exit status |
|---|---|---|
ok |
the prerequisite is met | unaffected |
warn |
worth reading, not an unmet prerequisite (e.g. an unactivated install) | unaffected |
FAIL |
an unmet prerequisite, printed with a fix: line |
non-zero |
mat doctor exits non-zero if any check FAILs and never stops at the first
failure, so it is safe as a script gate. It is strictly diagnostic: it creates
no session, writes no config, prints no secret value, and never remediates — each
FAIL line tells you the command to run yourself. No launch path gates on it.
Scratch-file handoffs
Section titled “Scratch-file handoffs”Prompt guidance follows the host operating system. On Linux and macOS, a direct
/tmp/<name>.md handoff is permitted only when MAT_SCRATCH_DIR is unset and
the effective root is the default /tmp; the file must exist, be readable, and
use the .md handoff contract before it is sent. If MAT_SCRATCH_DIR is set,
use its printed root or mat scratch-file instead. Use mat scratch-file when
you need collision-safe reservation or atomic stdin-backed writing.
Windows Git Bash/MSYS2 and unknown hosts use the conservative rule: use the
printed scratch root and mat scratch-file for relay files. This keeps Bash,
native tools, and agent file tools on one path. The runtime scratch commands and
relay transport are the same on every platform.
A relayed handoff must be a readable, non-empty file: send-relay checks the
path before delivery and refuses — with a non-zero exit, nothing delivered —
when the file is missing, unreadable, or empty. Write the file first, then send
its path. The atomic writer form mat scratch-file <name> -- prints the path
only after the bytes are written, so it can never hand off a path without
content behind it; the reservation form mat scratch-file <name> prints a path
and creates nothing, so sending a reserved-but-unwritten path is refused.
To send a handoff directly to a pane, send-relay %<pane-id> <handoff-file>
also works outside a tmux client, such as from an SSH shell. Role-word targets
such as reviewer still require a tmux client.
Worktree isolation
Section titled “Worktree isolation”By default every worktree-creating mode — adhoc, team, duo, audit,
explore, caucus — runs in its own dedicated worktree or read-only snapshot,
isolating it from your main checkout. On a disk-constrained machine you can turn
this off for all of them at once:
git config --local mat.enableWorktree false # this repogit config --global mat.enableWorktree false # every repo on this machineLive still starts in the checkout you launched it from, and direct work stays there. Every patch task is placed automatically. An issue-backed patch task gets its own managed worktree from the latest verified default branch; uncommitted changes in the launch checkout move into it, so the launch checkout is left clean. Issue-free patch tasks are placed the same way, and a task already running in a linked worktree stays there. If the default branch or a safe task worktree cannot be resolved, the task stops instead of using the shared checkout. This live patch boundary remains in effect when the general worktree setting above is disabled.
With worktrees disabled, the write-capable modes (adhoc/team/duo) share the
one main checkout, so mat refuses a second write-capable session against a repo
that already has a live one. Remove an inactive, clean worktree explicitly with
mat remove-worktree <path>; this also accepts a preserved live issue-patch
worktree. It refuses dirty worktrees and any path still used by a live pane.
Choose a mode that matches the work:
- Team and Duo — multi-agent issue-backed delivery with an independent reviewer gate.
- Adhoc — single-agent issue-backed delivery.
- Explore and Audit — investigation that produces tickets, not code.
- Caucus — a two-agent deliberation that converges a decision into tickets.
- Live — a hands-on single-agent session with no issue queue.
Team mode
Section titled “Team mode”team is the 3-agent relay for issue-backed delivery in the current repo: a
planner, a dev, and an independent reviewer, each in its own pane. Only the
planner starts working on launch; the dev and reviewer panes start idle and wake
as work reaches them.
Each backend combination gets its own stable worktree, preserved when you close
the session and reused when you rerun the same team … command.
Duo mode
Section titled “Duo mode”duo is the 2-agent relay: a dev and an independent reviewer. It is the leaner
delivery loop when you do not need a separate planner — the dev plans and
implements, and the reviewer gates the plan and the implementation. Start it
with a dev and an optional reviewer backend:
duo # both roles on your default backendduo claude codex # dev on claude, reviewer on codexLike team, duo runs issue-backed delivery in the current repo and keeps a
stable worktree per backend combination. See the baton & duo
quickstart for a headless, tmux-free way to run it.
Adhoc mode
Section titled “Adhoc mode”adhoc is the single-agent delivery path: one agent claims a ticket, plans,
implements, opens the PR, and merges it. Name an existing #N on a direct turn
and adhoc handles only that issue; with auto-start it selects the next ready
ticket itself.
Each adhoc slot keeps a stable worktree, preserved for recovery when you close the session and reused when you rerun the same repo and slot.
Explore mode
Section titled “Explore mode”explore is the open-ended investigation mode: it runs with or without a repo,
in its own dedicated home, and hands its findings off as tickets rather than
code. Each explore pass ends a ticket in one of three states: ready once problem,
approach, and acceptance criteria are concrete; blocked, with the open question
written into the ticket body and one notification to you; or closed as a
duplicate. A ticket never rests on refining after the pass, and opening a
ticket ends the explore session instead of launching delivery.
Explore and Audit gather concrete source and runtime evidence, test the leading
conclusion, and report confirmed, not confirmed, or insufficient evidence
with a next action. They report findings; they never open a ticket themselves.
For a resident, repository-scoped explorer that walks every open unready issue
on its own, run mat explore --auto-refine [backend] (or -a). It selects issues
with refining or no workflow label, then launches a
fresh child per ticket, parks on relay wakes when idle, and never implements,
opens PRs, or merges. While parked, the pane shows an explicit
explore auto-refine [<backend>] parked status line — the finished child’s
last screen is cleared — and the pane border counts down ↻ mm:ss to the next
poll check, so a waiting worker is never mistaken for a dead one.
It skips an issue already associated with an open PR, and also leaves the issue alone when that association cannot be verified; a later pass can reconsider it. If stale-claim recovery does reclaim a lock, it leaves an issue comment naming the reclaimed lock and latest heartbeat. A comment delivery failure does not undo the recovery. A live session also receives a diagnostic after repeated heartbeat failures, while its claim remains in place for retry.
For a one-shot explore with a faster startup, use --lean; see The --lean
startup profile for the contract and the
prerequisite run.
Auto-refine runs under the baton driver too, and it is the one single-agent mode
that does: instead of a resident child in a pane, each ticket is refined over a
series of headless turns, every turn a whole process that starts and exits.
Continuity across those turns comes from the ticket’s own conversation trail —
each turn resumes the previous one — rather than from a living child, so a turn
boundary is already a context boundary. Manual renewal (/handover,
/respawn) is therefore unavailable there: it refuses rather than restarting
the ticket. A ticket that never reaches an outcome is bounded by a per-ticket
turn budget; when the budget runs out the ticket is marked blocked with an
## Open question explaining why, you get one notification, and the hold is
released, so it is not retried automatically. Set the budget with
MAT_EXPLORE_AUTO_REFINE_MAX_TURNS, and the per-turn time limit with
MAT_EXPLORE_AUTO_REFINE_TURN_TIMEOUT. Pick a backend whose CLI supports
headless turns; one that does not is refused at launch.
A headless refine pass can also stop and ask you something. When it does, you get the usual notification, the worker stops taking turns, and it waits — it will not carry on without your answer. Reply with:
mat baton send <session> explore "<your answer>"Your text becomes the next turn, on the same ticket and with the same conversation, so the pass picks up where it paused. The reply is only accepted while that worker is running; if it has already exited, the command says so and sends nothing.
Audit mode
Section titled “Audit mode”audit starts or reattaches a per-repo session in a dedicated worktree. Each
pass refreshes to the latest merged code and reviews it for quality problems,
filing tickets but never editing code, opening branches, or merging.
Audit runs on a cadence poller: after each pass it calls start-audit-poll and
waits, waking on a newly merged PR or on a periodic broad sweep (about every
5h by default; tune it per-repo with
git config --local mat.auditPollMinutes <minutes>). After five consecutive
cycles without a new merge, the poller becomes dormant: it stops broad-sweep
wakes but keeps watching the remote tip. A wake can file up to three quality-finding tickets and up to two CI-health tickets
(five total), then the session idles again. The auditor also watches the CI
surface and tickets red runs. Closing the session stops the pollers and
preserves the worktree for reuse.
The supervisor recognizes both direct child completion and non-ancestral Command Code shell/tool completion. The latter carries the current child PID and capability; the supervisor validates the generation and process identity, claims it once, boundedly reaps that child, and clears the handoff record. Copied, stale, malformed, or duplicate completion signals only re-arm the poller and cannot end another child.
mat audit status reports the poller state, queued-wake depth, current child
elapsed time, and the latest wake kind.
When a foreground (mat.driver local) audit’s supervised child keeps running
while a wake is queued behind it, the framework escalates with
notify-user --action, naming the session, the child’s elapsed time, and the
number of queued wakes. It never terminates the child on its own;
mat audit status shows the same stall. To recover, run
mat audit revive <session> — it re-checks the stall before reaping the
child, so it is a safe no-op when no stall is recorded.
Remote-tip/self-liveness heartbeat runs about every five minutes by default and
is tunable with MAT_AUDIT_POLL_HEARTBEAT_MINUTES. It independently checks
that the session is still alive and fetches the repository’s remote tip without
waking an agent when nothing changed. When a merge is found, the session
refreshes its worktree and performs checkpoint reconciliation before the next
audit pass.
Caucus mode
Section titled “Caucus mode”caucus starts a repo-scoped, 2-agent deliberation: a proposer and a challenger
converge one topic to a concrete conclusion, rendered as GitHub ticket(s) — no
delivery code. Each launch cuts a fresh read-only snapshot from the default
branch’s tip and is deleted on session exit.
caucus always launches idle. Seed the opening topic by typing it into either
pane, or non-interactively with caucus --topic-file <path>. Alternatively,
caucus --backfill gives both agents a standing backlog-grooming duty and opens
immediately with no typed topic.
Live mode
Section titled “Live mode”live starts a hands-on single-agent session and reattaches on later launches:
rerunning the command reattaches to the existing tmux live session when one is
usable. A second supervisor is still rejected while a live state owner exists
and no usable tmux session is available.
Unlike the delivery modes it has no issue queue, no reviewer, and no relay — it
is you and one agent working directly.
Live picks its own working posture and escalates it without asking:
- direct — hands-on in the current directory and branch: operational commands, testing, tracked-document updates, and other requested work, including edits on the default branch when the repository enforces no CI. It has no automatic branch, commit, PR, CI, or merge requirement.
- patch — for product-code delivery: it creates a task branch before tracked product-code edits (or any change to a repository that enforces CI), makes the change, validates, self-reviews, then makes one coherent commit with a clear message, pushes, and opens a PR. It merges only when the PR is mergeable and required CI is green. Missing or failing checks remain blockers: never merge around CI.
Live escalates to patch at the moment a tracked edit becomes necessary and
either the edit changes product code or the repository enforces CI, announcing
the switch rather than asking for it. Before substantial edits it records the
problem, investigation, and proposed solution; this is not an approval gate.
Live’s one queue-like carve-out is issue capture: when it spots an incidental,
out-of-scope concern, it may open a gh issue to record it rather than dropping
it or scope-creeping. Capture only — live never implements the ticket it files.
Use live --domain <name> claude, live --domain <name> codex, or
live --domain <name> commandcode to attach a configured domain toolbox home
(its own instructions, skills, and runbooks) while keeping the current
repository as the primary workspace. The attachment is writable and does not
require an extra consent gate. Claude discovers the domain home instructions
natively. Codex and commandcode receive --add-dir for access, and mat folds
the domain root’s non-empty AGENTS.override.md, otherwise AGENTS.md
(Codex), or .commandcode/AGENTS.md (commandcode) into one managed block in
the role instructions, because neither auto-loads the domain file from
--add-dir.
The --lean startup profile
Section titled “The --lean startup profile”explore and live accept a --lean flag. Ordinary single-agent launches
configured for baton collapse to the effective local path before lean
admission; lean trades the per-launch snapshot isolation and provisioning work
for faster startup, on the precondition that you have already prepared the
agent home with one normal launch (so the home, prompt, settings, and project
trust are already in place).
The prerequisite is per mode, and every refusal prints the exact command:
| lean launch | prerequisite (run once in this directory) |
|---|---|
mat explore --lean [backend] |
mat explore --no-worktree [backend] |
mat live --lean [backend] |
mat live [backend] |
mat live --domain <name> --lean [backend] |
mat live --domain <name> [backend] |
A plain mat explore is not sufficient: it runs inside its own detached
snapshot and grants trust to that snapshot, not to the checkout a lean launch
shares. mat explore --no-worktree prepares the checkout you actually run in,
and works for every backend.
Lean always runs Explore in the shared checkout, so an explicit
mat explore --worktree --lean combination is rejected before launch. Remove
--worktree or use a normal, non-lean Explore launch; --no-worktree is
accepted with lean but is redundant.
mat explore --no-worktree claude # once, to prepare the homemat explore --lean claude # thereafter
mat live claude # once, to prepare the homemat live --lean claude # thereafter
mat live --domain platform codex # once, to prepare the domain-aware homemat live --domain platform --lean codex # thereafterLean shares the worktree across runs and reuses the existing home verbatim without rewriting it, with one exception: if the base render context (directory, driver, host, or another render input) differs from the identity preserved in the render-cache sidecar, lean re-renders just the constitution so the home is admitted against an artifact that matches this launch. Live domain path, source, or content drift is a proof mismatch, not a render context change, so lean refuses before rewriting the constitution, sidecar, or provisioning records. A damaged prompt or render-cache sidecar also refuses before any re-render.
After the first lean launch validates the home, later launches with nothing changed are admitted by a fast warm-path check instead of repeating the full validation; any change to the home’s managed files, its provisioning records, or the launch context falls back to the full validation first, so the fail-closed behavior is the same on both paths. The warm check compares file metadata rather than file contents, so an edit that deliberately forges a file’s size and modification time — or an in-place edit to the install tree without a version bump — goes unnoticed until the next full validation runs. Anyone with write access to the home could simply edit the cached admission record instead, so this adds no new exposure.
A lean session runs without a separate supervisor process: the launcher
itself owns the session listing (mat agents still shows it), the backend
child, and the cleanup on exit. Role routing, credential handling,
restricted-role guards, and event logging are identical to the normal
profile.
Lean refuses up front with a one-line reason when:
- the mode is not
exploreorlive(e.g.mat team --lean), saying--lean is supported only for \explore` and `live` (got `team`)`; - the effective driver is not
localafter mode resolution (ordinary baton-configured explore/live launches have already collapsed to local), saying--lean requires the local driver (resolved driver is \tmux`); run `mat explore –no-worktree claude` without –lean`; - the backend kind does not deliver an initial prompt to its native CLI,
because lean’s contract reaches the agent only through that path,
saying
backend \X` (kind `Y`) does not deliver an initial prompt to its native CLI, and the lean contract reaches the agent only that way; run … without –lean`.
A lean launch then validates the home (the warm-path check, or the full validation whenever anything changed) and refuses with the same one-line shape whenever the home is missing, stale, or held by another live generation, so a lean launch is fail-closed: a refusal leaves the home and the operator’s session untouched. A correctly provisioned home is admitted regardless of which driver prepared it or how the install path is spelled; a launch from a different directory re-renders the constitution for that directory rather than refusing, while genuinely damaged managed files (deleted or edited credentials, skills, rendered prompt, settings, or onboarding state) always refuse with a message naming the stage.
Three more refusals protect concurrent sessions and your shared config:
- A normal launch refuses while a lean session is running on the same
agent home, saying
agent home <home> is held by a live lean session (pid <pid>); provisioning it now would rewrite state that session is already running on. Exit that session, then relaunch— a normal launch rewrites the prompt, settings, and trust a running lean session depends on. Two normal launches keep their existing last-writer-wins behavior. - A lean launch refuses any home another live session already holds,
saying
agent home <home> is busy — held by a live <profile>-profile session (pid <pid>). Lean reuses a home rather than provisioning its own, so it cannot share one; exit that session, then relaunch. Once the other session has exited, the stale lease is pruned on contact and the launch proceeds. - Lean never rewrites the shared super-global
~/.claude/CLAUDE.md. The normal profile resets a symlinked or non-empty copy of that file on every launch (keeping a timestamped backup); lean’s only write into the home is the constitution re-render above, and it touches nothing outside its own lease and that artifact. If the file would be injected into the agent’s constitution, the lean launch refuses and tells you to run the prerequisite once — which resets and backs the file up — then relaunch with--lean./handoverand/wrapuparchive the session as usual;/wrapup --continueand a barecontext_renewalare refused under lean, because a respawn would re-enter the launcher with the same reuse-only contract and no fresh proof of the operator’s intent. The explicitcontext-renewal --archive-only <doc>form stays available: it validates the archived document and stops there — no respawn, on any session shape.
A lean run does not expose credentials or other secret environment values to the agent; the initial prompt is generated locally and contains only the checkout path the operator themselves named on the command line.
Running a mode in the foreground
Section titled “Running a mode in the foreground”To run explore, live, or audit as a foreground process in the current
terminal without creating a tmux session, set
git config [--local|--global] mat.driver local. mat.driver baton runs those
same single-agent modes the same way, with one exception: the resident
auto-refine worker, which under baton has an engine of its own (see Explore
mode) and runs headless in the launching terminal. Only one foreground audit
runs per host at a time, and wakes are delivered into that terminal rather than
into a pane. If a later launch reports the singleton as already running and the
recorded supervisor’s liveness is unprovable, mat local-state clear <state-file> clears
the state file named in that message; a proven-live supervisor is never
bypassed.
Windows Git Bash has no tmux driver
Section titled “Windows Git Bash has no tmux driver”On Windows Git Bash the driver enum is local and baton only — MSYS2 tmux is
an unsupported transport, so the tmux driver is retired there. Leave
mat.driver unset and you get baton, the Windows default: team, duo and
seeded caucus run as baton serves, while explore, live, audit and
adhoc run through the same local headless path mat.driver local gives you.
Pinning tmux explicitly — MAT_DRIVER=tmux or git config mat.driver tmux —
is refused with a migration message before any session is created, so there is
never a half-started session to clean up. tmux is therefore not a Windows
prerequisite: the installer does not ask for it and mat doctor reports it as
n/a. Linux and macOS are unchanged and still default to tmux.
Telegram relay
Section titled “Telegram relay”The Telegram relay lets you direct and answer your agents from your phone: every agent notification arrives as a Telegram message, and your replies route straight back to the exact agent that spoke.
Enabling it
Section titled “Enabling it”Follow your platform quickstart’s Telegram step — it takes you from nothing to
an active message under a <host> · tg-relay header arriving in Telegram. On Linux, the relay user
service is installed and started for you at install time: systemd is preferred,
with OpenRC user services used when no systemd user manager is reachable. The
reference below applies once the relay is running.
mat doctor reports whether your Telegram credentials are present and whether
the relay service is loaded and active.
Managing the resident relay
Section titled “Managing the resident relay”mat relay status | start | stop | restart works the same way across every
platform (systemd, OpenRC, launchd, Windows) without needing to know which
service manager is in play:
mat relay status # detected manager, verified process, singleton-lock statemat relay start # start the registered relay (fails closed if unregistered)mat relay stop # stop it; the registration itself stays in placemat relay restart # stop then start under one lockstart/stop/restart only ever act on a process it has freshly verified is
tg-relay — never a stale or recycled PID — and stop refuses to report
success while a relay you started by hand is still running outside the
manager’s view, rather than silently leaving two pollers on the same bot
token. start refuses to launch an unregistered or out-of-date relay; re-run
./install.sh first if it does.
One relay per bot token. Telegram allows only ONE active long-poller per
token; a second relay (another host, a stray checkout, or a configured webhook)
makes both callers get HTTP 409 and your replies arrive late, out of order,
dropped, or duplicated. A same-host duplicate cannot start — it exits cleanly if
another relay already holds the lock — while a cross-host or webhook competitor
is surfaced as a throttled log line plus a one-shot ping. The fix is operator
action: ensure exactly one relay owns the token.
Replying from Telegram
Section titled “Replying from Telegram”Every message starts with one header line saying where it came from, and the message itself follows on the next line:
<host> · <project> · <role>/<backend> · <address>[ (<reachability>)]<message>For example:
xps · myproj · audit/cch · #5helloFields are always in this order, and a field that is unknown is left out
together with its · separator. The address says how to reach the sender:
| Sender | Address | Reachability |
|---|---|---|
| An agent in a tmux pane | #<pane-number> |
— (reply to it) |
| A background task | #<session> @<role> (or #<session>) |
(no reply) |
A Baton or local worker you can answer |
#<session> @<role> |
— |
| Any other session with no pane | <driver>:<session>:<role> |
(view only) |
| A session start notice | #<session> |
— |
| The relay itself | none (<host> · tg-relay) |
— |
| A framework action | <driver>:<session>:<component> (or <component>) |
(view only) |
Framework --action pages bind their session and component explicitly, so an
operator can identify the daemon, supervisor, or poller that raised the page
even when it was launched from another pane. If no session, role, or pane can
be resolved, the page remains deliverable but labels its role unknown-origin
instead of silently appearing as a host-only action.
(no reply) and (view only) messages cannot be answered by reply. A reachable
Baton or local worker prints a typed #<session> @<role> address, so you can
send that address at the start of a new message as well as reply to the message.
To talk back to an agent in a pane, reply to its message in Telegram — the relay routes your reply straight to that pane, regardless of which project was last active.
After a successful reply, plain unprefixed follow-ups stay on that same pane. If
that pane has exited, the relay refuses the follow-up with recovery guidance
rather than sending it somewhere else. Re-target with #<session> or
#<pane-number>, or reply to a live agent message.
You can target a session directly with #<session> @<role> message. This works
for tmux panes, Baton workers, and replyable local workers. @dev,
@planner, and @reviewer address their matching roles; @adhoc, @audit,
and @explore address the corresponding single-role local workers, while
@proposer and @challenger address caucus roles. A duo/team Baton target
without @<role> defaults to its developer (dev) role; a single-role Baton
or local target may omit it when exactly one role resolves. Caucus targets and
ambiguous local targets must name the role explicitly. An incompatible role or
dead headless session is refused by session and role rather than falling back
to tmux.
For a session running under tmux, a refusal tells you what tmux actually
confirmed. (Baton and local driver sessions are checked without tmux and keep
their own refusal wording.) “No session named …”, “has exited”, or “not running” means tmux answered and
that exact session is gone. “Couldn’t confirm session … is running” means tmux
could not be asked: the server is unreachable (the reason is named, including a
server whose socket file went missing), tmux did not answer in time, or tmux
returned an error. The session may still be alive, so try again shortly. An
Available: list is added only when the session listing itself succeeded; if
it failed, the reply names the cause and has no list, rather than saying none
are running. send-relay on tmux makes the same distinction: no session named versus cannot confirm session with the cause.
A team session announces its panes when it starts:
xps · myproj · session · #myproj_team_ccwstarted — dev #5, planner #6, reviewer #7Reply to that start message to hand the team its first task (it lands on the
developer), or address a specific agent with its pane number, e.g. #6 please review the API design to reach the planner.
Send /agents or /idle as a plain message to get the running-agents listing
back in Telegram without touching a pane.
When you are away and a directive arrives over Telegram, a routable mode (team,
adhoc, audit, explore, live) answers over Telegram — the substantive
reply comes back as a message, per follow-up, not into a pane you cannot see —
until you type into the pane directly or say you are back at the console.
Some messages come from a background task rather than a pane — a long merge or test run an agent submitted to run on its own. Those carry the session instead of a pane number, e.g.
xps · myproj · reviewer/cch · #myproj_duo_ccw_cch @reviewer (no reply)merge completedThe identity is the submitting agent’s own, and the project is the checkout the
task ran in — never whichever pane happened to be focused at the time. A
background task has no pane to answer on, so replying to one is refused. To
reach the agent itself, send a new message starting with the address as
printed (#myproj_duo_ccw_cch @reviewer ...), or with #<pane-number>.
If you reply to a message the relay can’t place — a control message, a
background task’s message, or one that has scrolled out of the index — it says
so and asks you to re-send with an explicit #<session> or #<pane-number>
prefix rather than guessing.
Replying to a session with no pane
Section titled “Replying to a session with no pane”A session started with mat.driver = local has no tmux pane. A replyable local
message carries a typed address instead of a pane number, e.g.
xps · myproj · adhoc/cch · #myproj_cch_adhoc @adhocneeds reviewWhether you can reply depends on whether that mode has anywhere to receive an
answer. A mode without a reply inbox keeps its diagnostic
local:<session>:<role> (view only) address.
adhocandauditaccept replies. Your reply becomes the input to that session’s next agent turn, on that exact session and role — never on whichever pane happened to be active. Plain unprefixed follow-ups then stay with that worker, the way they do after a reply to a pane.mat explore --auto-refineaccepts replies too. The resident refinement worker asks for a decision and keeps working, so your answer is handed to its next agent turn: the next turn on the same ticket while that ticket is still open, otherwise the first turn on the ticket it picks up next. Your reply is delivered once — and if the worker is restarted before it reads the reply, the reply is re-delivered rather than lost.- Interactive
exploreandliveare display-only. Those modes run the agent directly in the terminal you launched them from, with no queue to hand an answer to, so a reply is refused rather than delivered somewhere else. Answer in the terminal instead.
If the worker has exited — or cannot be confirmed to be still running — the reply
is refused by session and role rather than routed elsewhere, and you re-target
with #<session> or reply to a live agent message. Nothing is delivered on a
guess.
notify-user flags
Section titled “notify-user flags”Agents notify you with notify-user:
notify-user [--action|-a] [--message <text>] [--] "message"The message body is normally positional (notify-user "msg"). --action / -a
marks a message that needs you to act. Every other --* token is rejected with
a usage error rather than being sent as text.
The action-required marker
Section titled “The action-required marker”Most notify-user traffic is FYI — a PR opened, a cycle finished, a queue
idling. A few messages need you: a decision to make, a blocker to unblock, a
review only you can perform. Those carry a leading ❗ACTION: marker at the
start of the header line, so you can scan a stream of notifications and see at a glance which ones are waiting on
you. Reply to a marked message the same way you reply to any other — the marker
is a signal to you, not a change in routing.
Forwarding action messages
Section titled “Forwarding action messages”You can have a second agent, such as an oncall agent on another machine, see every action message too. Set an ssh host alias that works without a password prompt:
git config --global mat.devopsHost <ssh-alias> # or export MAT_DEVOPS_HOSTMAT_DEVOPS_HOST overrides the git setting. After every notify-user --action
send, mat runs ssh <ssh-alias> oncall-inbox and passes the message on standard
input, exactly as it went to Telegram plus a final newline: the ❗ACTION: marker
and header line, then the message on the following lines. Plain notify-user
messages are not forwarded. When neither setting is present, nothing changes.
Format change. Earlier releases sent a single line with a pane prefix and a trailing
(role - backend - project - host)group. Anoncall-inboxscript that parsed that shape must now read the first line as the header and the rest as the message.
oncall-inboxis yours to provide. mat does not ship it. It must be on the remote host’sPATHfor non-interactive ssh commands, which read neither~/.profilenor an interactive~/.bashrc. It decides how the message reaches the oncall agent.- The forward does not change the notification result. Telegram delivery
comes first and does not depend on the forward. Once accepted, the forward
continues after the agent’s tool call ends, with a 15-second SSH limit. If a
local session has no live owner to accept it, the notification log records a
carrier-unavailablefailure. Forward failures do not change the Telegram message ornotify-user’s exit status. - Set
MAT_DEVOPS_SELF=1on the receiving host. Agents there then never forward to themselves, even when a copied configuration also sets the host.
Advanced and customization
Section titled “Advanced and customization”Prompt overrides
Section titled “Prompt overrides”You can replace the shipped prompt for a given mode with your own markdown file.
Resolution order:
- Project override:
<project-root>/.my-ai-team/<mode>.md - User-global override:
${XDG_CONFIG_HOME:-~/.config}/my-ai-team/<mode>.md— skipped unlessmat.personalPromptOverride=true(opt-in) - Shipped default: the
<mode>.mdprompt installed with mat
The project .my-ai-team directory may itself be a symlink, for example to a
bundle kept in a shared dotfiles repository. mat reads it through the link and
still reuses the rendered prompt across launches.
Supported mode filenames: adhoc.md, dev.md, plan.md, review.md,
explore.md, audit.md, live.md, duo-dev.md, duo-review.md,
caucus-proposer.md, caucus-challenger.md.
Enabling user-global overrides. The user-global (XDG) tier is off by default.
To activate it — so your hand-maintained copies under
${XDG_CONFIG_HOME:-~/.config}/my-ai-team/ are used — set:
git config --global mat.personalPromptOverride trueDrift warning: a hand-maintained personal copy is not kept in sync
automatically — it drifts as my-ai-team ships prompt changes on upgrade,
producing hard-to-diagnose mismatches. Opt in only while you are actively
maintaining the copies. Project-local overrides (.my-ai-team/<mode>.md) are
unaffected by the toggle and always win.
The per-role personality hook (shared/personality-<role>.md, or
shared/personality.md for explore) is the exception: it stays overridable from
the user-global tier even when the toggle is off, because the built-in
personality files are empty hooks with nothing to drift from.
Operator extension points
Section titled “Operator extension points”To add shared house rules without replacing a role prompt, create a fragment in
<project-root>/.my-ai-team/shared/ or
${XDG_CONFIG_HOME:-~/.config}/my-ai-team/shared/. The project tier always
applies; the user-global tier requires mat.personalPromptOverride=true.
local-issue-rules.mdreaches the issue-acting roles.local-devops-rules.mdreaches every standalone role. Its shipped fragment is empty; add only the rules your deployment needs.
For a project override, .my-ai-team may be a symlink, but its shared/
directory and fragment must be real directories and files.
When an override is active, mat names it on the resolved launch line (the
(personal override) marker) so you can always tell which prompt is live.
Skills overlay
Section titled “Skills overlay”Product skills are runtime-owned: the launcher builds each backend’s skills directory from the install on launch. To add personal, environment-specific skills — internal DB-query or ticketing helpers that reference your own infrastructure and do not belong in the product repo — drop them into an operator skills overlay:
-
Source:
${XDG_CONFIG_HOME:-~/.config}/mat/skills/, one directory per skill. There is no configurable path. -
Non-colliding skills are always provisioned into every role home, regardless of any toggle — they carry no product-drift risk.
-
mat.personalSkillsOverride— an opt-in git boolean, off by default — governs name collisions with a product skill. Off: the overlay skill is skipped and the product skill wins (the safe default). On: the overlay skill overrides the product one for that role.Terminal window git config --global mat.personalSkillsOverride true
Adding, editing, or removing an overlay skill re-provisions role homes on the next launch, and a collision prints one line naming the outcome so the resolution is visible.
bg-run — background commands
Section titled “bg-run — background commands”bg-run launches a long-running shell command, returns immediately, and delivers
milestone and completion notifications through the current turn’s wake channel —
so an agent never blocks waiting for a slow build, test run, or deploy:
bg-run --cmd "make integration-tests" --milestones 15m,30m,45m --max 60mDefaults are --milestones 15m,30m,45m and --max 60m; durations accept s,
m, or h. After launching, do useful work or end the turn and wait for the
wake — do not sleep, poll, or re-inspect status. The wake carries the
result-file path, and the persisted result JSON (task id, status, exit code,
elapsed time, and stdout/stderr tails) can be read when the completion wake
arrives.
After the explicit milestones, bg-run adds an informational heartbeat every
15 minutes until the task ends or reaches --max (up to a bounded number of
heartbeats). Running milestone wakes identify themselves as heartbeats and say
that no action is required; act only on the completion wake or a failure
diagnostic. If an unusually long task exceeds the heartbeat bound, its normal
completion and timeout behavior still apply, but fresh heartbeat evidence is
no longer guaranteed.
Failure diagnostics. Wakes and results stay compact — they carry only a
short tail of each stream. When a task ends in any non-success terminal state
(failed, timeout, cancelled, or aborted), bg-run additionally
preserves the task’s full stdout and stderr in local 0600 diagnostic files,
so you can read the actionable failure without rerunning the command. The result
and wake report seven extra fields for this:
stdout_log/stderr_log— the diagnostic file path for each stream (empty unless the bytes were preserved).stdout_log_state/stderr_log_state— one ofclosed(full bytes preserved at the path),absent(the stream produced no output),unreadable(the source vanished or was only partially readable — a final state: the result still commits once, with an empty log path, and the raw provider output is kept until the retention lease), orskipped(a successful task — no diagnostic is written; success output is not preserved).stdout_log_truncated/stderr_log_truncated—truewhen a stream exceeded the per-stream byte cap and was head+tail truncated around an explicit marker,falseotherwise.log_max_bytes— the per-stream cap in effect (default10485760, i.e. 10 MiB; override withBG_RUN_LOG_MAX_BYTES, applied inclusively per stream).
Diagnostic files live under a private, per-user directory and share the result’s
lifetime: they are removed only once the result ages past BG_RUN_DEDUPE_LEASE,
and BG_RUN_DEDUPE_LEASE=0 keeps everything indefinitely. If a task is reclaimed
before it managed to publish a result, the diagnostics are materialized before
any raw source is deleted and re-anchored to the new result when one is finally
written; if the result write keeps failing, the raw source, diagnostic, and wake
are retained together while the publish is still retryable and removed together
only at the lease boundary. The full bytes always stay on the local host — only
the paths and the state fields above ever travel in a wake, result, or relay
payload.
Expiry cleanup. Removing expired records and files is best-effort maintenance that runs in the background. Each submission starts at most one cleanup worker and goes straight on to admit its own task; the submission never waits for cleanup to finish. The worker skips any record another process is using and leaves it for a later submission’s worker. A worker runs for at most about two minutes before it is stopped.
Session teardown. When a session ends while a Baton task is still running,
bg-run finalizes it from teardown rather than leaving it orphaned: it stops
the task and commits the durable result (and diagnostics) to the promised result
path. Because the originating turn is already gone, a teardown-finalized Baton
task records only the result — no completion wake, envelope, or relay is
sent. A local adhoc supervisor also stops its accepted tasks at teardown and
commits their results with status: aborted; it retains their terminal receipts
in the session inbox after clearing the rest of its temporary state. A local
task is not left running after its supervisor exits. Two Baton outcomes are
distinct here:
- The result commits, diagnostics may be
unreadable. If the diagnostic source cannot be read back at teardown, the result still commits once, withstdout_log_state/stderr_log_stateofunreadableand an empty log path (the raw source is retained until the lease so the failure can still be investigated). The result file itself is there to read. - The result write itself fails. If the result cannot be written at all (for example its path is briefly unwritable), there is no committed result. The task’s retry state, raw source, and diagnostics — and any compact wake — are retained together for a later pass to finish, bounded by the same retention lease. Do not expect a result file for this task until that retry succeeds.
bg-run admits at most one in-flight task per (caller, command) pair: a second
submission of the same command returns the existing task rather than launching a
duplicate. A milestone or completion wake is evidence of an existing task — do
not resubmit the same command on a wake. The duplicate status line keeps the
machine-readable task_id=... result=... deduplicated=true prefix and adds
age=<seconds>s state=<state>. age is the age of the matched admission
record; for a cleared tombstone it starts when that tombstone was written, not
when the original submission began. state=active means the classifier still
has a live owner (including a live Baton task); uncommitted_fresh identifies
the short reservation grace window before provider liveness is probed.
Use --retry for a deliberate rerun.
Retries, attempt identity, and stale wakes. --retry replaces the running
task behind the same admission slot. Every admission record carries three
identity fields — a lineage (derived from the caller and command, so a changed
command or a different caller is always an independent lineage whose wakes can
never supersede or be superseded by another lineage’s), a generation epoch
(fresh on first admission, lease reclaim, and post-cancel re-admission; kept by
--retry), and a monotonic attempt (bumped by --retry). Every wake and
result carries these fields plus the event kind, frozen at launch — so each
attempt’s notifications name their own attempt.
When a replaced attempt’s wake arrives after the replacement took over,
bg-run drops it by identity, not by age: a wake is rejected only when the
admission record is readable, its lineage and epoch match the sender’s, and the
sender’s attempt is behind the record’s. A dropped wake creates nothing — no
result event, no turn, no notification — and prints one superseded diagnostic
naming the rejected task and attempts; the replaced attempt’s wake and result
files stay on disk until their normal retention lease expires. Wall-clock age
never decides a drop: a delayed completion from the current attempt is
delivered after any delay. What age can do is quiet the notification when
nothing else is known: if the cycle a task was launched under has provably
finished, or a very old completion arrives with no evidence that its workflow is
still running, the result and wake are still written and kept — only the turn
that would have announced them is skipped. Redelivering the same event stays
idempotent — one turn per task/event, however many times it arrives. Records
from before this scheme (no identity fields) always deliver, and a task whose
record is missing or unreadable delivers too — rejection requires a live record
that positively names a replacement. When available, a background task keeps
the invoking Duo role’s issue association even when it runs from a worktree
whose branch has no issue number. Duo Baton dev and reviewer tasks resolve that
association from the first available source: an explicitly assigned issue key,
the launch branch, the validated active cycle, then the submitting role’s
uniquely verified live issue claim. If the claim cannot be verified as one
issue, the task stays keyless. A late terminal still wakes the role after ten
minutes when the issue is confirmed current; without positive current workflow
evidence, an old notification may be suppressed while its result remains
available.
A milestone that reaches Baton after its task has already ended does not create another wake; the completion event carries the task’s final outcome. A wake for an issue cycle that has closed is also withheld, while any queued handoff for a current cycle remains eligible for delivery.
Cycle close and delivery journal. Publishing a closed issue-cycle record also stops committed tmux and Baton tasks whose recorded workflow session and issue key both match that cycle. The close record is written first, so a late callback cannot wake the closed turn. The task publishing the boundary is spared so it can finish; a nested task has its own identity and is stopped with the other matching tasks. Tasks without an issue key and headless fallback tasks keep their existing lifetime rules.
Before a tmux wake is sent, bg-run checks that the pane still belongs to the
session that launched the task. It leaves the wake file in place and skips the
send if the pane has moved, disappeared, or cannot be resolved. Cycle reaps,
stale and superseded suppression, and wake-delivery failures are recorded in
${BG_RUN_DEDUPE_DIR}/bg-run.log (default /tmp/bg-run.log). The private log
contains no command output, is limited to 1 MiB, and keeps one rotated file at
bg-run.log.1.
bg-run is available on tmux and Baton sessions, and from a child running in a
live supervised local adhoc session. Local adhoc tasks are owned by the
supervisor; milestone and terminal receipts go to the session inbox, and the
submission returns the durable result-file path. The local provider rejects
direct and non-adhoc local invocations. For a standalone headless local command,
use nohup <command> >validation.log 2>&1 & disown from a foreground call and
confirm from the log instead.
To deliberately stop a task before it finishes, run bg-run --cancel <task-id>
(the task id from the original launch or a milestone wake, while the task is
still running). This delivers a status: cancelled completion wake (or a local
inbox receipt) instead of waiting for the command to finish or --max to
elapse; cancelling an unknown or already-completed task id fails closed with a
diagnostic and changes nothing.
win-kill-tree — stop a Windows process tree
Section titled “win-kill-tree — stop a Windows process tree”On Windows Git Bash and MSYS2, taskkill /T follows native parent links, which
an MSYS fork/exec can break, so a successful taskkill /T does not prove a
Bash-launched tree is gone. win-kill-tree stops a tree and then rescans until
no process it owned is left:
win-kill-tree --msys-pid 4242 # MSYS PID, as shown by pswin-kill-tree --winpid 13376 # native Windows PIDwin-kill-tree --tag run-abc12345 # every process whose command line contains the stringwin-kill-tree --winpid 13376 --term-grace 3 --deadline 20 --passes 4A bare number is treated as --pid, which is refused when it names both an MSYS
PID and a different native PID. Options:
--expect-generation <id:ticks>kills only if the target still has that creation identity, so a reused PID is never touched.--term-grace <seconds>sends a polite TERM to MSYS processes before the forced kill.--passes <n>(default 3) and--deadline <seconds>(default 30) bound the work; the command fails rather than loop forever.--detach-if-containedlets a caller that is itself inside the target tree start an independent executor instead of killing itself;--log <file>receives that executor’s output.--detachalways hands the kill to an independent executor, without first checking whether the caller is inside the target tree.
Limits: the tree is captured from the target while it is alive, so a process
that has already detached from a dead root cannot be recovered by PID — use
--tag for that. --tag matches a literal, case-sensitive substring of the
process command line (at least eight non-whitespace characters) and also stops
the matches’ descendants. The command never kills itself or its own ancestors.
Exit status: 0 the tree is gone (or was already absent); 1 the stop was
refused, the process table was unavailable or incomplete, a live process’s
identity could not be read, or owned processes survived;
2 usage error or ambiguous PID; 3 the work was handed to an independent
executor and has not been confirmed. On Linux and macOS the command is not
needed — ordinary process groups and signals apply.
Blocked cleanup commands
Section titled “Blocked cleanup commands”On backends with a shell-command hook, agent sessions refuse the cleanup forms
that this section warns about, before the command runs, and point at
win-kill-tree:
On Linux, literal kill, pkill, killall, and fuser -k commands are first
checked against session ownership. A target in the session’s cgroup subtree is
allowed, including a reparented process that remains in that cgroup. If the
session has no dedicated cgroup, only descendants of the session process tree
are allowed. A foreign target is refused with its pid, command line, and owning
systemd unit; a pattern that matches both owned and foreign processes is
refused, while a pattern with no matches is allowed. This ownership check does
not evaluate shell variables or command substitutions to obtain operands, and
non-Linux hosts keep their existing cleanup behavior.
taskkillwith/T(or//T), in any option order.- Outside the Linux ownership check (and for unresolvable Linux operands),
image-name kills:
taskkill /IMor/FI "IMAGENAME eq <name>",Stop-Process -Name <name>,pkill <name>, and aWhere-Object Name -eqselection piped toStop-Process. These kill every process with that name, including a CI watchdog’ssleepon a shared host. - Linux
fuser -kforms whose namespace, variable, or other operands cannot be resolved by the ownership check remain refused as broad process cleanup.
Matching ignores case and accepts a path-qualified executable name. PID-targeted
commands such as taskkill //PID 1234 //F and Stop-Process -Id 1234 stay
allowed, as do win-kill-tree itself and text that only mentions these commands
(quoted prose, comments, heredoc bodies). Use win-kill-tree <pid> for a known
process, or start your own processes with a unique string in their command line
and stop them with win-kill-tree --tag <your-unique-tag>. There is no override
variable. The opencode and freebuff backends expose no hook surface, so
this guard does not cover them; a command typed directly in your own shell is
never filtered.
Dispatch daemon
Section titled “Dispatch daemon”For always-on, hands-off delivery, a dispatch daemon can cold-launch or wake a
delivery session whenever a repo has claimable ready issues — no manual duo /
team required.
If you start a duo yourself with mat duo, the dispatcher recognizes it when
the repository and backend pair match its configuration, including when the
duo’s managed worktree is outside the checkout. Manually started sessions are
never reaped by the dispatcher.
Watch one repo in the foreground:
dispatch-repo ~/projects/my-repo owner/my-repo --mode duo --interval 5Run this command from your operator shell, outside an active delivery session.
Watch many repos at once by listing them in
${XDG_CONFIG_HOME:-~/.config}/mat/dispatch.json and running dispatch:
{ "max_active_repos": 3, "repos": [ { "repo": "owner/repo-a", "repo_root": "/home/user/projects/repo-a", "mode": "duo" }, { "repo": "org/repo-b", "repo_root": "/home/user/projects/repo-b", "mode": "team", "backends": ["opus", "sonnet"] } ]}max_active_repos caps how many repos run at once (idle sessions are cheap and
do not count); repos over the cap are deferred and retried next tick. dispatch
validates the config before starting and exits non-zero on malformed JSON, a
missing repo_root, an unknown mode, or duplicate repo+mode pairs.
For a resident service instead of a foreground loop, register it with
mat dispatch enable (opt-in, never auto-enabled by the installer). Its verbs
are watch, enable, disable, start, stop, status, and logs.
Sessions the dispatcher owns that stay fully idle past a timeout (12 hours by
default) are archived and torn down automatically; a fresh session cold-launches
on the next tick that finds a ready issue. Sessions you started by hand are
never reaped.
Troubleshooting
Section titled “Troubleshooting”Start with the local diagnostic log, ~/.local/state/my-ai-team/mat.log
($MAT_STATE_HOME/my-ai-team/mat.log when MAT_STATE_HOME is set). Set
MAT_LOG_FILE to write it somewhere else. A launcher failure, a session temp
directory that cannot be created, or a mode launch that exits non-zero leaves
one line there, and the line outlives the pane:
[2026-09-24T09:15:02+0000] ERROR mode: mat explore exited with status 1 (cwd /home/me/repo)Read the newest lines with tail -n 50 ~/.local/state/my-ai-team/mat.log. The
log holds diagnostics only, never credentials. It is capped at 50 MiB, and when
it reaches the cap it keeps the newest half and records a truncation line.
Next, run mat doctor; it names tools, gh auth, PATH/links,
backend token, Telegram credentials, and relay-unit failures in one read-only
pass.
mat exits at once reporting that its launcher runtime is missing.
- Check
readlink -f ~/.local/bin/mat— it must resolve inside~/.local/share/my-ai-team. - Re-run
./install.shfrom an unpacked release tarball to rebuild the install.
mat starts on Windows Git Bash but a mode session fails immediately.
- Check that
tmuxitself is installed and onPATH. - Re-run
./install.shso~/.local/bin/matpoints back at the launcher.
mat reports that it cannot create a session temp directory.
- Symptom: the launch stops with a
mat: cannot create session temp dir ...diagnostic naming the directory and the filesystem reason. - Cause: the per-user session-temp root is missing and cannot be created, or
the launching user cannot write there. By default mat uses a private base
under
${TMPDIR:-/tmp}/mat-sessions-<uid>for the current user. - Fix: make that root writable, or set
MAT_SESSION_TMP_DIRto an absolute directory the launching user can create and write before launching again.
gh --body-file /tmp/x.md fails with file-not-found even though /tmp/x.md
was just written.
-
Symptom: a Write tool creates
/tmp/x.md, butghcannot read it. On Windows Git Bash the MSYS/tmpmount maps to%TEMP%, while the Claude Write tool (native Node) resolves/tmptoC:\tmp— two different on-disk dirs for the same path.mat doctorreports this as atmp mountFAIL on Windows. -
Fix: repoint the MSYS
/tmpmount to a real NTFS dir so all three resolvers agree. Add one line to your per-userfstab(<msys-prefix>/etc/fstabor%USERPROFILE%\.fstab):C:/tmp /tmp ntfs binary,noacl,posix=0 0 0Create
C:/tmpfirst if it doesn’t exist. After this,cygpath -w /tmpreturnsC:\tmpand the Write/Read tools, bash, andghall land on the same file.
Windows Git Bash: agents get the Bash tool, not PowerShell
Section titled “Windows Git Bash: agents get the Bash tool, not PowerShell”- Symptom: a launched agent runs its shell commands through PowerShell instead of
bash. Prompts and skills that assume POSIX quoting,
$(...), and pipes behave differently, and the guards mat installs never fire. - Cause: Claude Code enables its PowerShell tool by default on Windows Git Bash,
alongside the Bash tool. Its startup probe can also miss a Git installed
outside the standard locations — most commonly a scoop install under
~/scoop/apps/git/<ver>/— so an agent may serve PowerShell even though Git Bash is available. - What mat does: when a real
bash.exepath is confirmed, it exports bothCLAUDE_CODE_GIT_BASH_PATHandCLAUDE_CODE_USE_POWERSHELL_TOOL=0into every launched Claude agent. This selects the Bash tool deterministically, so the managedmatcher: "Bash"hooks fire. If no Git Bash path is available, mat leaves the PowerShell setting untouched because disabling it would leave Claude without a shell tool.mat doctorreports the decision and path. - Overrides: set
CLAUDE_CODE_USE_POWERSHELL_TOOL=1to opt into PowerShell deliberately; mat then leaves the shell-tool choice alone. To keep Bash with a custom install, exportCLAUDE_CODE_GIT_BASH_PATHyourself to abash.exethat exists in Windows form (with a trailing.exe).
Windows: Copilot-backed panes run shell commands through PowerShell
Section titled “Windows: Copilot-backed panes run shell commands through PowerShell”- Symptom: in a Copilot-backed session on Windows, the agent’s shell commands run through PowerShell — there is no bash option in the Copilot CLI.
- What mat does: it routes Copilot’s
powershelltool calls through the same command guards asbash, so the delivery-command boundaries and destructive-command guards still apply on Windows. The guards scan command text with bash-syntax rules, so a delivery command written in PowerShell-only syntax (for example$env:VAR = ...rather thanexport VAR=...) may not be recognized — write shell commands in bash style even though PowerShell executes them.
Windows: one Git for Git Bash and the MSYS2 shell
Section titled “Windows: one Git for Git Bash and the MSYS2 shell”- Symptom, in either shell:
fatal: Unsupported SSL backend 'schannel'on every HTTPS fetch, push, or clone — or, from a stale credential, GitHub’sremote: Repository not foundfor a private repository the account can demonstrably read. - Cause: Git for Windows and an MSYS2-native
gitpackage can both be installed, and Git Bash and the MSYS2 shell then resolve differentgit.exewhile sharing one global~/.gitconfig. Git for Windows supportshttp.sslbackend=schannel; the MSYS2-native build links OpenSSL only and rejects that value.mat doctorreports the unsupported combination as agit sslFAIL. - The supported setup is both shells resolving Git for Windows, with the
shared config selecting
schannelplus exactly one credential helper.
Run in the MSYS2 shell only (Git Bash already resolves Git for Windows):
pacman -R git # drop the MSYS2-native buildecho 'export PATH="/c/Program Files/Git/cmd:$PATH"' >> ~/.bashrcexec bash -l # reload PATHVerify in each shell — both must print the same executable and a version
stamped .windows.:
type -a gitgit --version # expect: git version <x.y.z>.windows.<n>Run once from either shell (~/.gitconfig is shared, so this configures
both):
git config --global http.sslbackend schannelgit config --global --unset-all credential.helper || true # exit 5 = nothing configuredgit config --global credential.helper managerCertificate verification stays on throughout. http.sslVerify=false is never the
fix for this, and nothing in mat suggests it.
Windows MSYS2: Telegram relay fails with curl exit 60
Section titled “Windows MSYS2: Telegram relay fails with curl exit 60”- Symptom: no Telegram messages arrive, and the relay log
(
~/.local/state/my-ai-team/tg-relay.log) repeatsgetUpdates failed rc=60 (untrusted TLS certificate ...), retrying with a growing delay of up to 60 seconds. - Cause: a corporate proxy intercepts TLS and re-signs
api.telegram.orgwith its own root CA. Windows trusts that root, so Git for Windows’curl(Schannel) works, but when the relay runs under the MSYS2 shell, MSYS2’scurluses its own OpenSSL trust store, which does not contain it. - Fix: add the proxy’s root CA to MSYS2’s trust anchors. Anchors survive
ca-certificatespackage upgrades, but edits made directly to the bundle file do not. In PowerShell, export the root (replace<Proxy Root CA>with the issuer name shown byopenssl s_client -connect api.telegram.org:443):
$c = Get-ChildItem Cert:\LocalMachine\Root | Where-Object Subject -match '<Proxy Root CA>'$c | ForEach-Object { "-----BEGIN CERTIFICATE-----`n" + [Convert]::ToBase64String($_.RawData, 'InsertLineBreaks') + "`n-----END CERTIFICATE-----" } | Set-Content C:\msys64\etc\pki\ca-trust\source\anchors\proxy-root.crtThen, in the MSYS2 shell (adjust the path above if MSYS2 is not installed in
C:\msys64):
update-ca-trustcurl -sS -o /dev/null -w '%{http_code}\n' https://api.telegram.org/ # expect 302The running relay picks up the new trust store on its next poll; you do not need to restart it. Certificate verification stays on, so never disable it as a workaround.
Windows Git Bash: cmp installer prerequisite
Section titled “Windows Git Bash: cmp installer prerequisite”install.sh needs cmp from the active shell. A Git for Windows cmp.exe does
not satisfy an MSYS2 shell if its directory is not on PATH; install the MSYS2
diffutils package there:
pacman -S diffutilstmux socket arbitration (Linux)
Section titled “tmux socket arbitration (Linux)”If tmux’s socket file goes missing while its server keeps running — a crash that
skipped cleanup, or an rm -rf /tmp — the next tmux new-session can silently
fork a second, unreachable server at the same address, stranding the panes on
the original. On Linux, mat checks the resolved default socket before every
launch and, when it finds this data-loss state, tries to repair the original
server before refusing the launch. A refusal names the stale pid(s) and the
recovery: salvage anything needed from those panes, kill them, then relaunch. A
manually rebuilt session must keep its original name — relay routing keys off the
session name, not the pid. mat doctor surfaces the same classification.
Optional tmux persistence
Section titled “Optional tmux persistence”If a named mat server should survive a drop to zero sessions, opt in manually:
tmux -L mat set-option -g exit-empty offThis keeps the server alive without a resident session; run
tmux -L mat kill-server to reset it.
Upgrading
Section titled “Upgrading”mat upgrade [--list|--to <version>|--check|--force|--when-idle|--when-idle-unbounded|--override-locked|--no-service-restart]mat upgrade --check # report installed vs. offered version; change nothingmat upgrade # fetch and install the latest release for your licensemat upgrade --force # reinstall even when already currentmat upgrade --list # list the versions still available to your licensemat upgrade --to <ver> # install that exact version instead of the latestmat upgrade needs an activated machine (mat activate <LICENSE_KEY>, once per
host). It validates the license, fetches a signed release tarball, and installs
it in place over ~/.local/share/my-ai-team/; your configuration and agent homes
are untouched. Plain mat and every mode invocation never touch the network.
Running agents keep the runtime they launched with, so upgrade between cycles.
After a successful upgrade,
stale managed Baton host services are refreshed and restarted when the host is
quiet. The turn check is strict: only an empty mat agents --json array or
rows whose status is exactly standby are idle; an unavailable or malformed
status is busy. Windows verifies that every running duo’s role sessions and
pollers come back after the node restart, waiting up to two minutes
(MAT_INSTALL_POLLER_VERIFY_WAIT_SECONDS) for them; other Baton sessions are
listed in the output but do not fail the upgrade. Windows never rewrites a
running service’s launcher in place: the
refreshed launcher is staged beside it while the service runs and replaces it
only after the node reports itself stopped, immediately before the restart. A
service left stale because the host is busy is not abandoned on Windows: the
installer arms a deferred converge job for it. The job re-checks the host
periodically for up
to two hours (MAT_INSTALL_DEFERRED_RESTART_WAIT_SECONDS), restarts the
service at the first quiet moment, verifies its duo sessions and pollers, and
sends you one notification per failed attempt. If the wait bound passes with
the host still busy, the job restarts the Windows node service anyway, keeping
session IDs intact. Outside Windows a busy service is left running and the
output gives its exact manual restart command. (The same restart is also
available by hand on Windows:
MAT_BATON_CONTROL_DIR='<control-dir>' mat baton service restart stops
the service, publishes the staged launcher and starts it again.) If the service
does not report itself stopped, it is left running on its current launcher and
its sessions are untouched. This gate is host-global: an
open explore, live, or caucus session is also busy even when the stale
service itself has no role turn. Use --when-idle to wait for quiet before
that decision. --no-service-restart skips only the Baton service restarts —
the Telegram relay is still recovered by the same deferred job. On systemd and
OpenRC hosts also restart the Telegram relay
service so the daemon runs the code you just deployed.
To roll back, pick a version from mat upgrade --list and install it with
mat upgrade --to <version>; it uses the same verified in-place path, and how
far back the list reaches is bounded by the release store’s retention. Plain
mat upgrade always follows the latest release and never rolls back on its own.
If the installed version is the offered tag, or that tag plus a commit
suffix (v3.63.0-1-gabc1234), it already covers the release; --force replaces it
with the tarball.
Beyond what the list holds, unpack an earlier release tarball and run its
./install.sh again.
Before migrating a license to another machine, release this host’s slot with
mat license deactivate.
On Windows, an in-place upgrade can hit files a running session still holds open.
mat upgrade --when-idle waits for running sessions to go quiet first (up to
five minutes; --when-idle-unbounded removes the ceiling). Before deciding,
the installer probes the agent status with a bounded timeout (one minute by
default, MAT_INSTALL_IDLE_PROBE_TIMEOUT_SECONDS); a probe that does not
answer in time counts as busy, so a loaded host is never interrupted. When
sessions are
busy, the installer leaves the resident relay running and upgrades files in
place. If the running relay could not be replaced with the installed files
before the install finishes, the same deferred converge job retries the
replacement (three attempts) and notifies you if it still could not. When
sessions are idle, the installer briefly stops the relay and
attempts an atomic directory swap. If the relay starts again during that
attempt, the installer waits briefly and retries; it falls back to the in-place
upgrade only if the swap still cannot complete. The relay is briefly unavailable
during this idle upgrade, which normally completes with the atomic swap. If a
critical file is still held, the install leaves a
resumable journal and reports what it could not replace, together with the
holder process names and PIDs when Windows can identify them (reported for the
held path set as a whole); when it cannot, the report says attribution is
unavailable. The report never stops any process. Retry when the host is idle, or
use --override-locked when the failure message suggests it and you deliberately
accept a mixed runtime. In an interactive terminal, the installer asks the
equivalent yes/no question; --override-locked accepts that decision without
prompting. The upgrade continues with files it can replace, while held files
remain from the previous release. The new version is recorded even though some
files are still old.
After accepting a mixed runtime, a plain mat upgrade reports that the host is
already on the latest version and does nothing. Once every mat session on the
host has exited, run mat upgrade --force to restore a single-version install
root.
See Versioning policy for what a version number means and the upgrade/rollback contract.