Cross-agent messaging for CLI AI agents. No daemon, no network, no complexity.
For AI agents: see
/llms.txtfor a quick, machine-friendly orientation.
You stop being the copy-paste courier between your agents. Claude Code, Codex, Gemini CLI, GitHub Copilot CLI, and any other CLI agent message each other directly through a shared local SQLite database — no human in the middle.
What it isn't:
- Not MCP. No MCP server, no extra runtime — just
bash+sqlite3. - Not subagents. agmsg connects peer sessions across different tools.
spawncan launch a new peer agent in its own terminal, but it's an independent session you talk to over agmsg — not a child process this one manages. - Not a broker. The SQLite file holds the local queue and its short receiver leases; there is no daemon, socket, or remote service.
Two monitor-mode Claude Code instances, left alone in the same team, play tic-tac-toe against each other with no human in the loop — each picks up the other's move in real time:
In real use it looks like this — Claude Code asking Codex for a code review and getting it back, all over agmsg:
Requires: bash and sqlite3. macOS ships both. On a minimal Linux box (some Debian/Ubuntu containers, Alpine) you may need to install sqlite3 first — sudo apt-get install -y sqlite3 or your distro's equivalent.
# 1. Install — npx is the fastest path, no clone needed
npx agmsg
# 2. Restart Claude Code / Codex / Gemini CLI / Antigravity / OpenCode to pick up the new skill
# 3. Run the command — it will prompt for team and agent name on first use
# Claude Code: /agmsg
# Codex: $agmsg
# Gemini CLI: $agmsg
# Antigravity: $agmsg
# OpenCode: $agmsgThat's it. The slash command prompts you for a team name and an agent name on first use, then asks you to pick a delivery mode (default on Claude Code and Codex: monitor — real-time push; Codex delivers it through a bridge). After that, you talk to your agent naturally — see First run below.
Prefer to inspect the code first, track the latest main, or pick a custom command name? See Install below for the setup.sh one-liner, git clone, and the Claude Code plugin marketplace paths.
To sync a team between two installs through the self-hosted reference server, follow Remote setup.
agmsg is a thin transport. Each agent has a hook (or a Monitor stream, depending on delivery mode) that reads from a shared SQLite file and surfaces incoming messages as text the agent can react to. send.sh queues a row; a receiver takes a short exclusive lease, hands its text to the host, then persists a receipt. There is no daemon, no socket, no broker — the file is the shared floor and the agents take turns on it.
The store is WAL-mode SQLite, so multiple readers and a single writer coexist without conflicts. A receipt means only that the receiver handed the message to its host (stdout or an inline Codex turn); it does not mean the LLM completed the requested task. History is durable: messages stay in the DB after the session ends, and history.sh can replay an old room into a fresh agent.
agmsg ends up at ~/.agents/skills/agmsg/ no matter which install path you take. Pick whichever fits your setup.
Which path gets the latest? The git clone and setup.sh (curl) paths install straight from main, so they're always current. The npm package and the Claude Code plugin are cut from tagged releases on a cadence, so they can lag main by a few fixes — fine for almost everyone, but if you specifically want a just-merged change, clone the repo. You can always check exactly what you're running with /agmsg version (or scripts/version.sh): a tagged release reads like v1.0.3, while a checkout ahead of the last release reads like v1.0.3-6-g1a2b3c4 (6 commits past v1.0.3).
npx agmsg # one-shot, no global install
# or
npm i -g agmsg && agmsg installThe npm package is a thin bootstrapper that downloads and runs the canonical setup.sh. Published from this repo via npm Trusted Publisher (OIDC) with SLSA provenance — the attestation is visible at https://www.npmjs.com/package/agmsg.
Inside Claude Code:
/plugin marketplace add fujibee/agmsg
/plugin install agmsg@fujibee-agmsg
/reload-plugins
/agmsg
The plugin install path drops the skill into ~/.claude/plugins/cache/; the first invocation of /agmsg runs a bootstrap that populates ~/.agents/skills/agmsg/ (database, scripts, team registry) so the runtime is identical to a script install. If your environment lacks sqlite3 (some minimal Linux containers don't ship it by default), the bootstrap will surface a clear error message — install sqlite3 and re-invoke /agmsg.
Clone the repo first, then run the installer — this is also the path that always tracks the latest main:
git clone https://github.com/fujibee/agmsg.git
cd agmsg
./install.sh # Interactive (asks command name, default: agmsg)
./install.sh --cmd m # Non-interactive with custom command name
./install.sh --agent-type gemini # Install a Gemini-oriented SKILL.md
./install.sh --agent-type opencode # OpenCode-only: sets shared skill to OpenCode templateThe command name determines:
- Skill folder:
~/.agents/skills/<cmd>/ - Claude Code / Copilot CLI:
/<cmd> - Codex / Gemini CLI / Antigravity:
$<cmd>
--cmd and --agent-type are only available via the direct-script path; the npm and plugin paths always install as agmsg and auto-detect the host agent type.
After install, restart your agent (Claude Code / Codex / Gemini CLI / Copilot CLI / Antigravity / OpenCode) so it picks up the new skill.
When gh is installed, install.sh also installs ~/.agents/bin/gh. Put
~/.agents/bin before the real GitHub CLI on the agent PATH if it is not
already there. The launcher fixes both the guard script and the real gh
executable to absolute paths at install time; it does not search PATH again.
The guard allows read-only commands and destination-checks GitHub writes. A
write is allowed only when the resolved URL is exactly on github.com and the
owner is kappaseijin or kappaseijinjp. It resolves destinations in this
order: explicit -R/--repo, GH_REPO, gh repo set-default --view, then the
current checkout. GH_HOST and malformed, missing, ambiguous, or failed
resolver results are rejected. gh api is read-only only for GET (or when no
method/body is requested); -f/-F, --field/--raw-field, and --input
make an omitted-method request non-read-only. Unknown commands, aliases that
execute commands, and extensions that execute or install commands fail closed.
The installer refuses to overwrite a non-agmsg ~/.agents/bin/gh. If gh is
not installed, it leaves the guard uninstalled and prints a message; rerun
./install.sh --update after installing gh. uninstall.sh removes only the
agmsg-generated guard.
When Git is installed, install.sh also installs ~/.agents/bin/git. Put
~/.agents/bin before the real Git executable on every agent PATH, then verify
the effective command with:
PATH="$HOME/.agents/bin:$PATH" command -v gitIt must print ~/.agents/bin/git (with the home directory expanded). The
launcher pins the guard and the real Git executable to absolute paths at
install time. git push checks every effective push URL, including all
remote.*.pushurl entries and url.*.insteadOf / url.*.pushInsteadOf
rewrites. Writes are allowed only when the resolved host is exactly one of
github.com, github.com-kappaseijinsub, github.com-kappaseijin4claude, or
github.com-kappaseijin4codex, and the owner is kappaseijin or
kappaseijinjp. Malformed URLs, local paths, unknown hosts, third-party
owners, and unresolved destinations fail closed before transport starts.
Direct URLs are resolved through a temporary synthetic remote so the same
rewrite rules are checked. Git aliases and git send-pack are rejected;
read-only commands and git clone continue to use the real Git executable.
The four SSH host names above are a fixed code-literal policy boundary. An
arbitrary github.com-* name, an SSH config alias, or an environment/config
override does not extend it. Adding an approved alias requires changing the
guard's code-literal allowlist and adding local fake-SSH positive and negative
tests in a reviewed PR.
The installer refuses to overwrite a non-agmsg ~/.agents/bin/git. If Git is
not found, it leaves the guard uninstalled and prints a message; rerun
./install.sh --update after installing Git. uninstall.sh removes only the
agmsg-generated Git guard. This is a user-space PATH guard: invoking Git by an
absolute path, changing PATH, replacing the shim, or using another user is
outside its guarantee.
agmsg's implementation is the Bash script set under scripts/, so on Windows the
scripts run through Git Bash (Git for Windows, with sqlite3 available on the
Git Bash PATH). There is no PowerShell reimplementation.
-
In Windows environments, Claude Code naturally works with Bash/Git Bash for these script calls, but native Windows Codex commands and hooks often start from PowerShell. Keep the actual agmsg execution path pinned to Git Bash so all agents share the same
$HOMEand SQLite database. -
Codex delivery hooks are wrapped automatically. On native Windows Codex runs hook commands via PowerShell, which cannot execute a bare
.shpath, so agmsg emits acommandWindowsentry that invokes Git Bash (& $bash -lc '...'). No setup needed — seewindows_wrap()inscripts/delivery.sh. -
Interactive / agent-typed commands call the scripts through Git Bash, e.g.
bash -lc 'scripts/whoami.sh "$(pwd)" codex'. -
Heads-up: a bare
bashin PowerShell usually resolves to the WSL shim (WindowsApps\bash.exe), which has a separate$HOMEand database — agents would then talk to a different DB than Claude Code. Pin Git Bash in your PowerShell profile so everything shares one database:Set-Alias bash 'C:\Program Files\Git\bin\bash.exe'
Open your project in your agent (Claude Code, Codex, Gemini CLI, etc.) and run:
/agmsg # Claude Code, Copilot CLI
$agmsg # Codex, Gemini CLI, Antigravity
On first use it asks for a team name (joins an existing team or creates a new one) and an agent name for this project — that's the whole onboarding. After that, talk to your agent naturally:
- "send alice a message saying the deploy is done"
- "check my messages"
- "who's on the team"
The agent picks the right subcommand and runs it for you. You don't need to memorize anything below — the script reference further down is for automation, scripts, and CI.
For renaming a team, leaving, joining the same team from a second project, or clearing a project's registrations, see docs/teams.md.
Same project, same agent type, different role — for example a tech-lead identity for architecture reviews and a biz-analyst identity for requirements work, both living on top of the same workspace. Toolset and assets are shared; only the role differs.
/agmsg actas tech-lead # switch to tech-lead (creates it if not yet registered)
/agmsg actas biz-analyst # switch to biz-analyst
/agmsg drop biz-analyst # remove the role from this project
actas <name> is exclusive across sessions: it switches sending and receiving to the exact <name> for every locally registered team of the current runtime, claims the matching locks, and refuses if another session already holds any of them. drop removes the registration only from the current project and releases this session's locks for that name across all teams. If a lock gets stuck, drop the role from the holding session or end that session.
See docs/actas.md for the full mechanics — exclusivity model, recovery, liveness / PID recycling, Codex caveat.
Where actas switches this session to a different role, spawn brings up a separate agent process that takes a role on boot — handy for fanning out collaborators.
/agmsg spawn codex reviewer # new codex agent, joins and becomes "reviewer"
/agmsg spawn claude-code alice --window # new claude-code agent in a fresh tmux window
/agmsg spawn codex reviewer --boot-prompt "review the diff on this branch" # joins AND starts the task
spawn <type> <name> pre-joins <name>, then launches the target CLI with the actas slash command (/<your-command> actas <name>, matching your install command name) as its initial prompt. If the current session is inside tmux, it opens in a new pane (or --window for a new window, --split h|v for the direction); otherwise it opens a new OS terminal window.
Pass --boot-prompt <text> to hand the new agent an initial task: the boot prompt becomes the actas slash command followed (newline-separated) by your text, so the agent claims its identity and acts on the task in the same first turn. This is the only way to give a one-shot goal to a codex peer, which has no Monitor and so never notices a message you send after it goes idle.
By default spawn blocks until the new agent is actually listening — its watcher attaches and touches a readiness sentinel — then prints status=ready, so you can send work the moment spawn returns without losing it to the agent's cold start. Use --no-wait for fire-and-forget, or --ready-timeout <secs> to bound the wait (default 90; on timeout it prints status=timeout and exits 3 so a caller can re-spawn). Codex skips the wait (it has no Monitor).
Options: --boot-prompt <text> (initial task; see above), --project <path> (default: current project), --team <team> (auto-resolved when the project has a single team), and --terminal <tmpl> / $AGMSG_TERMINAL / config spawn.terminal to override the terminal command on the non-tmux path (a {cmd} placeholder is replaced with the path to the generated boot script). On macOS the default opens whichever terminal you're currently in (iTerm or Terminal, via $TERM_PROGRAM) using open -a — a plain app launch, so it does not trigger the Automation/AppleScript permission prompts that scripting the terminal directly would.
To always pass a given agent type extra CLI flags on spawn (e.g. a default permission mode or sandbox policy), set them in a YAML spawn options file — one section per type, a flat --flag: value map underneath. Path: $AGMSG_SPAWN_OPTIONS_FILE, else ~/.agmsg/config/spawn_options.yaml; a missing file or section is a no-op. An optional <type>@<role> section appends its tokens after the base <type> tokens. spawn resolves the role from --role <role> first, then from the second-to-last _-separated component of a three-or-more-component agent name; if neither applies, it uses only the base section.
claude-code:
--permission-mode: acceptEdits
--dangerously-skip-permissions: true # a `true` value emits the flag with no argument
codex:
--sandbox: workspace-write
--dangerously-skip-permissions: false # a `false` value suppresses the flag entirely
codex@architect:
-p: architect
-c: model_reasoning_effort=xhigh
codex@programmer:
-p: programmer
-c: model_reasoning_effort=mediumFor example, spawn codex project_architect_codex applies the codex@architect overlay. An overlay key replaces the same key from the base section; an overlay value of false suppresses that key entirely. An explicit role must contain only letters, digits, _, or -; an unsafe value is rejected before spawn creates or joins anything.
To make role configuration fail closed for a type, add the separate metadata
section below. Missing metadata, a missing type key, or false preserves the
optional behavior. With true, spawn requires a role and the matching
<type>@<role> section before it resolves a team or pre-joins the agent; an
empty section is valid for types without a profile contract. Metadata is never
emitted as CLI argv.
agmsg.require-role-overlay:
claude-code: true
codex: trueWhen codex: true, the role overlay must select exactly one profile with
-p: alias, --profile: alias, -p=alias: true, or --profile=alias: true.
The base codex: section must not contain a profile flag, including a
false value used to suppress it. The alias must start with a letter or digit
and contain only letters, digits, ., _, or -. spawn verifies the
readable file $CODEX_HOME/<alias>.config.toml; when CODEX_HOME is unset it
uses ~/.codex/. The verified absolute directory is exported as CODEX_HOME
in the generated boot script. A missing, unreadable, duplicate, or malformed
profile fails before registration or terminal launch. Policy-disabled Codex
configurations keep the existing pass-through behavior.
Eight of the nine agent types are spawnable — claude-code, codex, grok-build, cursor, gemini, antigravity, copilot, opencode. hermes is not: its CLI has no mode that starts an interactive session pre-seeded with an initial prompt (#279). macOS is the primary target; Linux and Windows are best-effort (please open an issue/PR if your terminal isn't handled). Headless environments — no tmux and no usable terminal — error out, since the agent CLIs need an interactive terminal.
despawn is the inverse of spawn — it cleanly tears down a member you brought up.
/agmsg despawn reviewer # graceful: the member drops its role and closes its own pane
/agmsg despawn alice --force # force: tear it down from here when its watcher can't respond
By default despawn <name> is graceful: it sends a ctrl:despawn control message to <name>, whose watcher drops its own role (releasing the actas lock and registration) and closes its own tmux pane — ending the agent. It blocks until the role is released, up to --timeout <secs> (default 30), then prints status=ok. If the member's watcher never responds it prints status=timeout and exits 3 — retry with --force.
--force skips the message and tears the member down from the placement recorded at spawn time: it kills the member's tmux pane/window and drops its registration. Use it when the member's watcher can't respond — a dead watcher, or a codex member (no Monitor, so graceful has nothing to act on). A member started by hand (no spawn placement record) can't be --forced; despawn says so and leaves it for you to close.
Despawn only acts on the named member — the session running despawn is never torn down, and a broad-subscription watcher ignores a ctrl:despawn aimed at another role.
A role remembers the session that last embodied it: sessions are named
<team>-<agent>, and spawn resumes a role's previous session by default —
so re-spawning after a despawn, crash, or restart comes back in the prior
conversation, not blank (--fresh forces new). With
tmux-resurrect, one ~/.tmux.conf
line re-seats every role pane into its session after a tmux-server restart.
See docs/session-resurrect.md for the tmux-resurrect setup, how it resolves each pane, what does and doesn't come back automatically, and the manual fallback.
How incoming messages reach your agent. Pick one at first join via the prompt, or change it later with /agmsg mode <name>.
| mode | mechanism | latency | who it's for |
|---|---|---|---|
monitor (default on Claude Code; on OpenCode with the opencode-sentinel plugin) |
SessionStart hook → Monitor tool → blocking SQLite stream | ~5s | Claude Code users wanting real-time push |
turn (default on Codex / Copilot CLI / OpenCode without the plugin) |
Stop hook fires check-inbox.sh between assistant turns |
until your next interaction | Codex / Copilot CLI / OpenCode users not running monitor; Claude Code users on a quieter loop |
both |
monitor primary, turn as per-session safety net | ~5s; falls back to turn-end on watcher failure | belt-and-suspenders |
off |
no automatic delivery | manual /agmsg only |
minimalists |
/agmsg mode monitor — switch this project to real-time push (Claude Code)
/agmsg mode turn — switch to between-turns checking
/agmsg mode both — monitor with turn as a safety net
/agmsg mode off — manual /agmsg only
/agmsg mode — show current mode
Settings are per-project. Each <project>/.claude/settings.local.json gets exactly the hooks the chosen mode needs — repeated set calls are idempotent.
Monitor priming: in monitor mode, the receiving agent doesn't react to its first inbound message until it has taken at least one turn this session. If you've just started a fresh session and a teammate has already sent something, nudge the agent with any short message ("hi") to prime it — subsequent messages stream in real time.
hook on is now a thin alias for mode turn (with a one-line deprecation hint). To switch to real-time push:
/agmsg mode monitor
The command updates db/config.yaml, rewrites the project's hook entries, and prints an AGMSG-DIRECTIVE that activates monitor in the current session — no agent restart needed.
/agmsg — check inbox (all teams)
/agmsg history — message history
/agmsg team — list team members
/agmsg send <agent> <message> — send message
/agmsg mode <monitor|turn|both|off> — switch delivery mode
/agmsg mode — show current mode
/agmsg actas <name> — switch to another role (same name across locally registered teams; create here if needed)
/agmsg drop <name> — remove a role from this project
/agmsg spawn <type> <name> — launch a new agent (claude-code/codex) that takes <name>
/agmsg despawn <name> [--force] — tear down a member you spawned (graceful, or --force)
/agmsg hook on | off — legacy aliases (mode turn | off)
/agmsg version — show the installed version (git-describe provenance)
/agmsg reset — clear current project registration
$agmsg — or /skills → agmsg
Codex supports mode monitor through an app-server bridge, plus mode turn and mode off.
⚠️ Monitor mode changes how Codex starts — enable it knowing that. Codex has no Monitor tool, somode monitorprints a shell function that makescodexroute through agmsg's monitor shim in your interactive shell. In monitor-mode projects the shim routes interactive launches through a bridge that turns incoming agmsg messages into turns on the current Codex thread;codex execand non-monitor projects pass straight through to the real Codex. It depends on Codex app-server behavior and has a known limitation (orphans on TUI close — #149).
If you prefer a global PATH shim, run ~/.agents/skills/<cmd>/scripts/drivers/types/codex/codex-shim-install.sh install and put ~/.agents/bin before the real Codex binary on PATH. You can also launch with ~/.agents/skills/<cmd>/scripts/drivers/types/codex/codex-monitor.sh. Codex sandboxing must allow writes to the skill's db/, teams/, and run/ dirs — install.sh configures those writable_roots when ~/.codex/config.toml exists. Setup notes and internals: docs/codex-monitor-beta.md.
/agmsg — invokes the agmsg skill
The Copilot installer drops a SKILL.md at ~/.copilot/skills/agmsg/ so /agmsg is auto-discovered. Per-project hooks live at <project>/.github/hooks/agmsg.json. Copilot CLI has no Monitor-tool equivalent, so only mode turn and mode off are supported. Asking for monitor or both is rejected with an error.
$agmsg
Install with ./install.sh (when ~/.config/opencode/ exists, the OpenCode-typed skill is placed automatically alongside the default Codex-typed shared skill). Use --agent-type opencode only for OpenCode-only environments where Codex is not installed. OpenCode supports mode monitor (via the external opencode-sentinel plugin; without it the rule instructs a fallback to turn mode, which the agent follows rather than agmsg enforcing it), mode turn, and mode off. spawn opencode is available via opencode --prompt (TUI mode, which stays resident after the boot prompt's turn). both is not supported.
This makes OpenCode useful as a local coding agent, including configurations backed by local providers such as Ollama.
See docs/opencode.md for full setup instructions.
~/.agents/skills/<cmd>/scripts/send.sh <team> <from> <to> "<message>" [--force]
~/.agents/skills/<cmd>/scripts/inbox.sh <team> <agent_id>
~/.agents/skills/<cmd>/scripts/message-status.sh <team> <agent_id> [--format human|json]
~/.agents/skills/<cmd>/scripts/history.sh <team> [agent_id] [limit]
~/.agents/skills/<cmd>/scripts/join.sh <team> <agent_id> <type> <project_path> [--role <role>] [--kind <seat|human|service>] [--force]
~/.agents/skills/<cmd>/scripts/team.sh <team> [--format human|json]
~/.agents/skills/<cmd>/scripts/roster-normalize.sh <team> --check|--apply
~/.agents/skills/<cmd>/scripts/team-work.sh <validate|self-check|g4-audit|observe|queue|audit|reconcile|watchdog|dispatch|dispatch-ack|claim|ack|renew|release|set-state|link-pr|writeback> <team> <contract-pack.json> ...
~/.agents/skills/<cmd>/scripts/whoami.sh <project_path> [type] [--format human|json]
~/.agents/skills/<cmd>/scripts/delivery.sh set <mode> <type> <project_path>
~/.agents/skills/<cmd>/scripts/delivery.sh status [<type> <project_path>] [--format human|json]
~/.agents/skills/<cmd>/scripts/reset.sh [--no-resolve] <project_path> <type> [agent_id] [session_id]send.sh takes four positional arguments — <team> <from> <to> "<message>" — plus an optional --force. The flag may appear before, between, or after the positional arguments. Unknown options and extra arguments fail with a diagnostic; use -- before a positional value that intentionally starts with -. Quote the message so the shell sees it as one argument; an unquoted message with spaces will be misparsed. Both from and to must already be registered in <team>; an unregistered name errors out (listing the currently registered names) instead of silently storing an undeliverable message. Pass --force to bypass this check for an intentional pre-registration send.
New teams created by join.sh have a versioned roster contract. Give a
member's role and kind explicitly when registering it:
~/.agents/skills/<cmd>/scripts/join.sh demo architect codex "$(pwd)" \
--role architect --kind seat
~/.agents/skills/<cmd>/scripts/team.sh demo --format json
~/.agents/skills/<cmd>/scripts/whoami.sh "$(pwd)" codex --format json--kind is one of seat, human, or service. If omitted, join.sh
records the explicit defaults kind: seat and role: unassigned; it never
derives either field from the agent name or runtime. A later join.sh with
--role or --kind updates that member's metadata.
The JSON commands return schemaVersion: 1, stable member records
(name, kind, role, and registrations), and for whoami.sh the lookup
runtime, resolved session.project, and exact matching registrations.
whoami.sh returns an empty registrations array when no member matches.
They report roster structure only: use message-status.sh or
delivery.sh status for delivery/liveness information.
Existing team configs remain usable through the human default output. JSON
mode does not guess missing fields: a legacy or incomplete matching config
returns exit code 2 with schema error: on stderr and no JSON on stdout.
If a team's JSON roster predates the versioned contract and is missing only the
root schemaVersion, use the normalizer. It adds the integer 1 after the
complete candidate passes the same roster validator used by team.sh; it does
not infer or change member roles, kinds, or registrations.
Test against a disposable copy first. The copy must contain the installed script tree and the target team's config at the matching paths:
skill="$HOME/.agents/skills/agmsg"
team=demo
scratch="$(mktemp -d "${TMPDIR:-/tmp}/agmsg-roster.XXXXXX")"
mkdir -p "$scratch/scripts" "$scratch/teams/$team"
cp -R "$skill/scripts/." "$scratch/scripts/"
cp "$skill/teams/$team/config.json" "$scratch/teams/$team/config.json"
bash "$scratch/scripts/roster-normalize.sh" "$team" --check
bash "$scratch/scripts/roster-normalize.sh" "$team" --apply
bash "$scratch/scripts/team.sh" "$team" --format jsonWhen the copy reports status: "ready" without changing its config and then
reports status: "applied", run the same apply command on the installed team:
bash "$skill/scripts/roster-normalize.sh" "$team" --apply
bash "$skill/scripts/team.sh" "$team" --format json--check never writes the config or acquires the registry lock. --apply
re-reads under the per-team lock and publishes the validated candidate with an
atomic replacement. A current roster returns
status: "already_current"; invalid JSON or incomplete member metadata exits
with code 2, prints a schema error: on stderr, emits no JSON on stdout, and
leaves the config unchanged. If the error names a missing role, kind, or
registration field, update that member through join.sh with explicit
--role and --kind (and --force only when the existing registration is
intentionally being updated); do not hand-edit config.json. Remove the
disposable copy after verification if it contains sensitive roster data.
Use the JSON status command before automatically assigning work to a seat:
~/.agents/skills/<cmd>/scripts/delivery.sh status codex "$(pwd)" --format json
~/.agents/skills/<cmd>/scripts/delivery.sh status claude-code "$(pwd)" --format jsonThe result has schemaVersion: 1, the requested type and project, an
aggregate runtime, liveness, sessionId, deliverable, receipt counts,
evidence, and one record per registered role in seats. A consumer may
dispatch to a role only when its deliverable is the JSON boolean true.
truemeans agmsg observed a live, role-bound receiver.falsemeans delivery is off, missing, or stale with a concrete reason."unknown"means the runtime cannot prove a current receiver; treat it as non-dispatchable until it becomestrue.
For Claude Code, true requires a live exclusive watch.sh watcher for that
role, not merely a SessionStart hook. For Codex, it requires a live bridge with
matching metadata and the role's recorded Codex session. Other runtimes report
"unknown" when agmsg has no type-specific liveness probe; configuration alone
never makes them dispatchable.
The nested receipt records queued, claimed, handed-off, and legacy-unknown
messages. handedOff acknowledges delivery to the receiver only. It does not
mean the role finished the task, closed an Issue, or merged a PR.
validate and self-check validate a versioned work-state contract pack. They
require node on PATH, read the selected team's team.sh --format json
roster, and never modify the pack, team config, GitHub, messages, leases, or
agents.
~/.agents/skills/<cmd>/scripts/team-work.sh validate demo .team-work.json
~/.agents/skills/<cmd>/scripts/team-work.sh self-check demo .team-work.jsonThe pack is JSON with schemaVersion: 1, the exact team, and a non-empty
workItems array. Each item requires a unique workItem.id, an issue source,
an ownerSeat, one or more workKinds, a revision, a
classificationBasis, and boolean writebackRequired. ownerSeat must be an
existing roster member with kind: "seat"; human and service identities are
not valid owners. Supported work kinds are implementation, writeback,
inventory, closeout, and reconciliation.
{
"schemaVersion": 1,
"team": "demo",
"workItems": [{
"schemaVersion": 1,
"workItem": {
"id": "issue:40",
"source": {"kind": "issue", "repository": "kappaseijin/agmsg", "number": 40}
},
"ownerSeat": "architect",
"workKinds": ["implementation"],
"relations": [{
"kind": "pull_request",
"repository": "kappaseijin/agmsg",
"number": 46,
"relation": "contributes"
}],
"revision": 1,
"classificationBasis": {
"contentDigest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"refs": [{"kind": "issue", "repository": "kappaseijin/agmsg", "number": 40}]
},
"writebackRequired": false
}]
}relations may be empty before a PR exists. A contributes PR relation needs
its repository and positive number. A closes relation additionally needs a
closingIssue object with the same repository and number as workItem.source;
otherwise validation fails. classificationBasis needs a lowercase
sha256: digest and at least one issue, pull request, commit, or evidence
reference.
validate prints compact JSON such as
{"schemaVersion":1,"valid":true,"team":"demo","workItemCount":1}.
self-check adds a canonical JSON representation plus contractDigest and
per-item envelopeDigest values. Object key order and whitespace do not change
these SHA-256 digests; array order remains significant. Invalid packs return
exit code 2 with schema error: on stderr. Usage errors, a missing pack, a
missing Node runtime, or an unavailable roster return nonzero without treating
the contract as empty or valid.
After a pack validates, team-work.sh can record an explicit local work-item
lease and its append-only revision chain. Mutating commands require both
node and sqlite3 on PATH. They initialize the same local store used by
agmsg messages: $AGMSG_STORAGE_PATH/messages.db when
AGMSG_STORAGE_PATH is set, otherwise the skill's db/messages.db.
# The declared owner obtains a 300-second lease (the default TTL is 300).
~/.agents/skills/<cmd>/scripts/team-work.sh claim demo .team-work.json issue:41 architect 300
# The active lease holder records delivery acknowledgement, renews, then releases.
~/.agents/skills/<cmd>/scripts/team-work.sh ack demo .team-work.json issue:41 architect received
~/.agents/skills/<cmd>/scripts/team-work.sh renew demo .team-work.json issue:41 architect 600
~/.agents/skills/<cmd>/scripts/team-work.sh release demo .team-work.json issue:41 architect
# An exact roster manager may update local work state and references.
~/.agents/skills/<cmd>/scripts/team-work.sh set-state demo .team-work.json issue:41 manager in_progress
~/.agents/skills/<cmd>/scripts/team-work.sh link-pr demo .team-work.json issue:41 manager kappaseijin/agmsg 47 contributes
~/.agents/skills/<cmd>/scripts/team-work.sh writeback demo .team-work.json issue:41 manager "merged PR verified locally"| Command | Authorized caller | Result |
|---|---|---|
claim |
Declared owner seat or exact kind: seat, role: manager member |
Takes an absent or expired lease. A manager claim makes that manager the lease holder. |
ack, renew, release |
The exact, unexpired lease holder | Acknowledges with evidence, extends the TTL, or clears the lease. |
set-state, link-pr, writeback |
The active holder, or an exact manager seat | Appends a local state, PR-link, or writeback revision. |
Authorization comes only from team.sh --format json: names that merely look
like managers, and human or service members, are rejected. An active owner
cannot have its ACK, renewal, or release performed by a manager or another
seat. A manager may record state, PR-link, and writeback changes without taking
the owner's live lease. ttl-seconds is a non-negative integer; zero is useful
only for deterministic expiry tests. An expired lease cannot be renewed or
released; an authorized owner or manager must claim it again.
set-state accepts acknowledged, in_progress, blocked, or completed;
claim itself creates claimed. link-pr accepts a positive PR number and
one unique contributes or closes relation. A closes relation must use the
same repository as the work item's issue source. writeback and link-pr
record local evidence only: they do not query GitHub, write a GitHub comment,
close an Issue, or merge a PR.
Each successful mutation returns compact JSON with team, workItemId,
revision, state, leaseOwner, leaseExpiresAt, envelopeDigest, and
lastAction. The current snapshot is stored in team_work_current; SQLite
triggers append the resulting immutable JSON snapshot to
team_work_revisions in the same transaction. A changed contract or envelope
digest, a duplicate PR relation, an unauthorized caller, or a competing live
lease fails closed with exit code 2 and leaves both tables unchanged. These
commands do not modify the contract pack, team configuration, messages,
message_claims, or message_receipts.
Use these commands to compare the pack and its local lease rows with live
GitHub Issue / PR state. They require node and use authenticated gh plus
sqlite3 when those live sources are available on PATH; an unavailable live
source becomes an unknown result. They idempotently ensure the local
store's schema exists -- a team's first-ever observe/queue/audit call
sees a genuinely empty store and classifies packed work normally rather than
reporting unknown for lack of a store -- but never write a lease, dispatch,
or message row.
# See every packed item's observed GitHub/local state.
~/.agents/skills/<cmd>/scripts/team-work.sh observe demo .team-work.json
# Emit only work that is currently ready to claim.
~/.agents/skills/<cmd>/scripts/team-work.sh queue demo .team-work.json
# Include closing-relation checks and every safety violation.
~/.agents/skills/<cmd>/scripts/team-work.sh audit demo .team-work.jsonThe command fetches each packed source Issue and related PR with GitHub
GraphQL. Both Issue.closedByPullRequestsReferences and
PullRequest.closingIssuesReferences are retrieved through every page. A
closes relation must occur on both sides; a contributes relation must not
close the source Issue. Pack relations and local link-pr records are both
checked. A missing page, API error, unknown relation, one-sided closing
relation, or a local row whose contract/envelope digest no longer matches the
pack is never treated as "no work".
Every successful observation is one canonical JSON object. It includes
contractDigest, sourceDigest, and auditDigest, plus this
classificationBasis:
| Status | Meaning | queue.ready |
|---|---|---|
ready |
One or more packed source Issues are open and have no live matching lease. | Those work items, in pack order. |
fully_allocated |
Open packed Issues all have matching, unexpired local leases or dispatch ledger entries. | Empty. |
quiescent |
Every packed source Issue is closed and its checked relations are complete. | Empty. |
unknown |
Evidence is unavailable, incomplete, contradictory, or locally stale. | Empty; do not dispatch from this result. |
classificationBasis also records readyCount, openItemCount,
allocatedItemCount, closedItemCount, a stable reasons array, and the
same sourceDigest. observe returns all item summaries; queue adds the
ready list; audit adds relationChecks and violations. The output uses
recursively sorted object keys, so its digests and exact JSON are stable for
the same pack, live responses, and local state.
Each item summary exposes the local row's workflow state as the additive
localState.workflowState field; it is null when no local row exists. An open
item whose workflow state is blocked is withheld from queue.ready and from
reconcile/dispatch ready selection. If that leaves no ready item, the
classification is unknown with the stable blocked_work_item reason in
classificationBasis.reasons; this condition is not copied into
audit.violations. If another unleased item remains, the classification stays
ready and only that other item appears in the ready list. An active blocked
lease remains fully_allocated, while a row returned to acknowledged or
in_progress can become ready again after its lease expires.
Malformed packs, invalid command syntax, a missing Node runtime, or an
unavailable roster remain nonzero errors. In contrast, a live GitHub/local
source failure returns a valid unknown result so a reconciler can record the
reason while safely withholding work. These commands never create or update a
GitHub Issue/PR, update a lease, send a message, spawn an agent, or decide a
remediation action. Their work universe is the supplied pack only; they do not
discover unlisted repository Issues.
g4-audit is the read-only Phase 1A implementation of the G4 state/coverage
contract. It is a separate input from the work-item contract pack above and
audits only the GitHub scopes explicitly declared in that input:
~/.agents/skills/<cmd>/scripts/team-work.sh g4-audit demo .g4-state-pack.jsonThe pack is JSON with schemaVersion: 1, the exact team, at least one
scopes declaration, and an entries array (which may be empty only when the
declared scopes return no matching open Issues). A scope has exactly these
fields:
{
"id": "demo-open-issues",
"repository": "kappaseijin/example",
"issueState": "OPEN",
"labelsAll": [],
"basis": {
"contentDigest": "sha256:<64 lowercase hex>",
"refs": [{"kind": "git", "repository": "kappaseijin/example", "commit": "<immutable commit>"}]
}
}repository is an explicit owner/name; issueState is currently only
OPEN; and labelsAll is a unique list of labels that every returned Issue
must contain. basis.refs is non-empty and must identify immutable evidence
(git, github_issue, github_pull_request, commit, or evidence). The
scope basis.contentDigest is the SHA-256 digest of the recursively
canonicalized scope object with basis removed.
Each entry maps one covered Issue exactly once:
{
"schemaVersion": 1,
"source": {"repository": "kappaseijin/example", "number": 42},
"state": "ready",
"ownerSeat": "demo_programmer_codex",
"workKinds": ["implementation"],
"basis": {
"contentDigest": "sha256:<64 lowercase hex>",
"refs": [{"kind": "github_issue", "repository": "kappaseijin/example", "number": 42}]
},
"revision": 1,
"entryDigest": "sha256:<64 lowercase hex>"
}ownerSeat must be an exact roster member with kind: "seat". Supported
workKinds are implementation, writeback, inventory, closeout, and
reconciliation. Entry state is only ready, blocked, or unknown;
quiescent is an aggregate audit result, never an entry state. A ready
entry has no blocker. A blocked entry requires a stable lower-snake-case
reasonCode and one v1 releasePredicate:
"blocker": {
"reasonCode": "upstream_issue",
"releasePredicate": {
"kind": "issue_closed",
"repository": "kappaseijin/example",
"number": 41
}
}The other predicate kinds are pull_request_merged, review_approved (with
headOid), not_before (JST RFC3339 time with +09:00),
issue_comment_digest (with commentId and a SHA-256 contentDigest), and
all_of (a non-empty list of predicates). An unknown entry cannot carry a
blocker and is never a claim or dispatch basis. entryDigest, when present,
is the canonical SHA-256 of the entry with only entryDigest removed. The
entry basis digest is calculated with both basis and entryDigest removed.
Object keys are sorted recursively for all digests; array order remains
significant.
The audit executes an explicit read-only GraphQL query for each scope, follows
every page, filters labelsAll, de-duplicates scope overlap, and compares the
resulting sorted (repository, number) set with entries. The result includes
packDigest, coverageDigest, sourceDigest, auditDigest, scopeAudits,
coverage, entries, ready, and classificationBasis:
| Status | Meaning | ready |
|---|---|---|
complete |
Live scope reads completed and entry coverage matches exactly. | Valid ready entries only. |
quiescent |
All declared scopes returned no matching open Issues and entries is empty. |
Empty. |
unknown |
A source/pagination error, duplicate, coverage mismatch, unknown entry, or unresolved blocker predicate prevents a safe result. | Empty; do not claim or dispatch. |
classificationBasis also reports scopeCount, coverageCount,
entryCount, readyCount, blockedCount, unknownCount, coverageDigest,
and a stable reasons array. A false blocker predicate remains a blocked
entry but makes the aggregate audit unknown; a true predicate is recorded as
a fresh observation and does not itself change the entry.
Malformed packs return exit code 2 with schema error:. GitHub transport,
GraphQL, pagination, or coverage failures return a valid unknown JSON result
with exit code 0, so callers can fail closed without confusing source
failure with an invalid pack.
Phase 1A does not initialize or mutate SQLite, create a G4 ledger, bootstrap or
transition state, pull work, dispatch messages, change GitHub labels, or call
any GitHub write method. g4-bootstrap, g4-transition, and g4-pull are not
published commands. They remain gated until the #97 recovery work and the #98
roster/delivery acceptance are both live; while #98 is open, this command is
an audit-only safety observation.
reconcile and watchdog run independently of an interactive agent command.
They consume the same contract pack, roster, live GitHub audit, local lease
state, and delivery-capability JSON as the commands above. They never call
send.sh, spawn.sh, or herdr, and never mutate GitHub. A closed or non-live
role is reported for remediation; it is not started automatically.
# Emit findings and remediation. Without the optional path this is read-only.
~/.agents/skills/<cmd>/scripts/team-work.sh reconcile demo .team-work.json
# Atomically replace only this requested heartbeat file after one reconcile run.
~/.agents/skills/<cmd>/scripts/team-work.sh reconcile demo .team-work.json \
/tmp/demo-reconciler-heartbeat.json
# Read a heartbeat from a separate process. The default stale limit is 900 seconds.
~/.agents/skills/<cmd>/scripts/team-work.sh watchdog demo .team-work.json \
/tmp/demo-reconciler-heartbeat.json 900reconcile returns canonical JSON with result (healthy or attention),
findings, remediation, the G2 sourceDigest/auditDigest, and a
reconcileDigest. It detects expired_lease, upstream_closed,
orphan_ready, writeback_required, and stale_state independently.
orphan_ready means the audit already classified the item as ready (see the
table above -- an open source with no live matching lease) but its packed
ownerSeat does not currently resolve to exactly one live, deliverable
roster registration. This is the PM/owner liveness signal: it fires exactly
as designed against an isolated or test roster with no matching registered
seat, not only against a real owner who has stopped running, so seeing it in
a sandboxed dry run is expected and is not evidence of a lease or dispatch
defect.
It changes no SQLite state unless an explicit heartbeat path is supplied; the
parent directory must already exist. The heartbeat records cycleId,
startedAt, finishedAt, result, and sourceDigest in canonical JSON.
watchdog only reads that heartbeat. It returns canonical JSON with status
healthy, stale, or unknown, plus alarm. A fresh quiescent reconcile is
healthy and does not produce an alarm. A missing, malformed, future-dated, or
old heartbeat is never treated as quiescent.
Dispatch is deliberately a two-stage local state transition. First define an
explicit JSON allowlist of existing seat names, then create a dispatching
entry. The default ACK deadline is 120 seconds.
export TEAM_WORK_DISPATCH_ALLOWLIST='["demo_programmer_codex"]'
# Only an exact roster manager may create a dispatching entry.
~/.agents/skills/<cmd>/scripts/team-work.sh dispatch demo .team-work.json \
issue:42 demo_manager_codex 120
# The declared owner must ACK the exact, unexpired epoch before starting work.
~/.agents/skills/<cmd>/scripts/team-work.sh dispatch-ack demo .team-work.json \
issue:42 demo_programmer_codex '<lease-epoch-from-dispatch>' received
# An exact manager can explicitly close an expired dispatch epoch.
~/.agents/skills/<cmd>/scripts/team-work.sh dispatch-abandon demo .team-work.json \
issue:42 demo_manager_codex '<expired-lease-epoch>' timeout-recoverydispatch requires all of the following: the packed Issue is currently
ready; the target is its exact kind: "seat" owner; that owner is in
TEAM_WORK_DISPATCH_ALLOWLIST; and exactly one of its registered delivery
runtimes reports both deliverable: true and liveness: "alive". false,
"unknown", ambiguous registrations, stale runtime evidence, a missing
allowlist, or an active lease produce state: "not_dispatchable" with
remediation and no ledger write. A caller that is not an exact manager seat is
rejected with a schema error before any ledger operation.
On success, dispatch writes an append-only local dispatch ledger containing
the queue digest, lease epoch/deadline, and canonical delivery evidence. Its
JSON reports state: "dispatching" and sendInvoked: false: queuing or
sending a message is not task ownership. It does not create a G2 work-item
lease yet.
dispatch-ack requires the declared owner, the same lease epoch, an unexpired
dispatch, an open and complete source audit, and fresh live delivery evidence.
Only then does one SQLite transaction change the dispatch ledger to claimed
and create the corresponding G2 team_work_current lease. A wrong, late, or
unavailable ACK returns acknowledged: false and leaves both ledgers unchanged.
dispatch-abandon is a manager-only recovery command: the manager, pack
digests, declared owner, exact epoch, and an expired (lease_expires_at <= now)
row must match in one guarded transaction, and its evidence must be non-empty.
It returns abandoned: true, stores the evidence in recovery_evidence, and
keeps the current row and append-only revision history; an unexpired G2 claim
causes a no-mutation refusal. abandoned is terminal evidence, not an active
allocation. A later dispatch may replace only an expired abandoned row with
a new epoch (last_action: "dispatch-replace"); expired dispatching or
claimed rows must be abandoned first. An ACK for the old epoch returns
acknowledged: false with dispatch_epoch_invalid and creates no claim.
The first team-work initialization after upgrade automatically migrates the legacy dispatch tables transactionally. It validates state, revision chain, and JSON before copying; a copy failure rolls back the legacy schema. Fresh and already-migrated stores are idempotent.
Neither command creates a GitHub mutation, sends a message, operates a herdr pane, or spawns an agent. Production dispatch remains disabled until the #98 roster/delivery gate and the Phase 3 prerequisite are satisfied.
send.sh prints Queued message #<opaque-id> … delivery not yet acknowledged. The identifier is storage-driver-defined and is not necessarily numeric. That is intentionally not a delivery-success claim. Inspect one receiver's state with either the human default or JSON for automation:
~/.agents/skills/<cmd>/scripts/message-status.sh myteam alice
~/.agents/skills/<cmd>/scripts/message-status.sh myteam alice --format jsonsend.sh's own output names the exact follow-up command for the message it just queued, so you don't have to eyeball aggregate counts or scan history.sh for it:
~/.agents/skills/<cmd>/scripts/message-status.sh myteam alice --id <the-id-send.sh-printed>| State | Meaning |
|---|---|
queued |
No receiver currently holds the message. |
claimed |
A receiver has a short exclusive lease while preparing host handoff. |
handedOff |
A durable receipt records host handoff. This is not LLM task completion. |
unknown |
A legacy row has read_at but no receipt, so handoff cannot be proven. |
notFound (--id only) |
No message with that id exists for this team/agent — check the id, team, and recipient you passed. |
history.sh uses the same distinction: ● queued, ○ receiver handoff acknowledged, and ? legacy/unknown receipt. If a receiver exits before acknowledging, its lease expires and the message remains eligible for another receive attempt.
reset.sh normally resolves <project_path> to the registered project associated with the current session or an ancestor/worktree. When the argument is already the exact stored path (for example, while cleaning up an orphaned registration), pass --no-resolve; the flag may appear before, between, or after the positional arguments. If no registration is removed, the command prints both the searched path and the argument path, and suggests --no-resolve when they differ.
External tools should read agmsg through api.sh, not through db/ or
teams/ files:
~/.agents/skills/<cmd>/scripts/api.sh get teams <team> members
~/.agents/skills/<cmd>/scripts/api.sh get teams <team> registrations
~/.agents/skills/<cmd>/scripts/api.sh get teams <team> messagesThe registrations resource emits one JSONL object per registration, so each
agent/type/project pair stays associated:
{"team":"agsuite","agent":"alice","type":"claude-code","project":"/work/agmsg"}The existing members resource remains unchanged for compatibility; it is an
agent-level aggregate and may return only one project.
Is this MCP? Do I need an MCP server?
No. agmsg is standalone — bash + sqlite3, no server, no daemon, no network. The two stacks are orthogonal: you can run agmsg alongside any MCP setup you already have.
Concurrent writes to the same channel — do they conflict?
The store is SQLite in WAL mode. Multiple readers and a single writer coexist; writes are short and serialized at the file level. In practice, two agents sending into the same team don't collide.
Does SQLite guarantee turn order? Is there a lock or token?
SQLite guarantees the ordering of the log itself — every row has a monotonic id and timestamp. Turn-taking between agents is a protocol-level concern, not enforced by the transport. The floor is intentionally dumb; the protocol lives in your prompts.
Two Claude Code instances grab the same task — claim/lock?
Receiver delivery uses a short message lease, so competing adapters do not both hand the same message to a host at once. This is deliberately narrower than task ownership: a handoff receipt does not assign the requested work or prove it completed. Use actas to keep two sessions from holding the same role, and put task ownership / completion rules in your team protocol.
Runaway loops — where does the stop condition live?
At the protocol/prompt level, not the transport. Common pattern: include a max-turns or explicit done-signal instruction in the kickoff prompt ("stop after N exchanges", "reply DONE when complete"). agmsg won't cut a conversation off for you.
What's carried on a handoff — context, diffs, or just text?
Plain text. Messages are short — a sentence, a request, a path. Agents pass summaries and references (file paths, commit SHAs, issue numbers), not raw context. Transport is the message; semantic packing is up to the prompt.
What if the output exceeds the receiver's context window?
Use the summary + file-reference pattern: write the artifact to disk, send a one-line pointer. The DB stores messages, not files.
Does it hold up with more than 2 agents?
Yes. Teams are N-agent. The demo is 2 for clarity; larger rooms work the same way — we run our own 8-agent team on it.
Does context persist across sessions?
Yes. Messages live in SQLite and survive sessions. history.sh <team> replays the room.
Can I re-seed a fresh agent from an old room?
The message store is effectively a replay log. There's no one-shot "rehydrate from room X" command yet, but history.sh gives you the transcript and you can prompt a new agent with it. Treat persistence as the unlock that makes that possible.
A receiving session isn't picking up messages — is there another way to reach it?
No, and that's by design: agmsg has exactly one delivery path, the SQLite queue plus whichever delivery mode (monitor/turn/both) is configured for the receiving session — there's no daemon or side channel that can push text into a specific terminal/pane for you (see "Is this MCP?" above; the same "no server, no daemon" boundary applies here). A terminal multiplexer's own text-injection commands, where available, are outside agmsg's scope and are not guaranteed to reliably submit a full turn to every CLI.
The message itself is never lost — it stays queued in the store regardless of whether the receiving session is currently consuming it — so the fix is getting that session to consume, not finding an alternate transport:
- Check the session actually has a delivery mode configured:
/agmsg mode(ormessage-status.sh/delivery.sh statusfrom the shell) —offmeans nothing will arrive until/agmsgis run manually. This is the most common cause: a session that joined via a path other than/agmsg actas <name>(a script callingactas-claim.shdirectly, for instance) can end up with no delivery hook installed at all. - In
monitormode, remember Monitor priming (above): a session that hasn't taken a turn yet this session won't react to what's already queued — nudge it with any short message. - If the session's process itself is gone, there's nothing left to nudge — restart/respawn it. The undelivered messages are still in the queue and
history.sh/inbox.shwill hand them to the new session.
How do you pronounce it?
"AG message" (ay-jee message), or spelled out as A-G-M-S-G. Either is fine.
cd agmsg
git pull
./install.sh --updateDB and team configs are preserved. Only scripts and assets are updated.
The script tree is staged and moved into place by rename, so an already-running watcher keeps reading the old file it has open instead of reading a partially overwritten file. Restart running agent sessions after the update to use the new scripts. If the staging move cannot be performed safely, the update fails loudly; it does not fall back to an in-place overwrite. The no-interruption guarantee is verified on macOS/Linux; Windows/MSYS2 behavior remains an explicitly unverified edge case.
An uninstall.sh copy ships inside every install, so this works whether you
installed via git clone, npx agmsg, or the curl one-liner:
~/.agents/skills/agmsg/uninstall.sh # Interactive (confirms each step)
~/.agents/skills/agmsg/uninstall.sh --yes # Remove everything
~/.agents/skills/agmsg/uninstall.sh --keep-data # Remove skill but keep DB and teams(If you have a git clone checkout handy, ./uninstall.sh from the repo root works the same way.)
Auto-detects installed skill directories and cleans up: skill files, slash commands, hooks, AGENTS.md sections, and team configs.
| Variable | Default | Purpose |
|---|---|---|
AGMSG_STORAGE_PATH |
<skill>/db |
Directory holding the SQLite message store (messages.db). Override to relocate the store — handy for tests, sandboxes, or running isolated instances. |
AGMSG_PLUGIN_DIRS |
(unset) | :-separated extra directories to search for external drivers, in addition to <skill>/plugins. Each holds <axis>/<name>/ subdirs. Drivers found here are still ignored until opted into with agmsg plugin trust. See docs/plugins.md. |
The message store path resolves as AGMSG_STORAGE_PATH (env) > built-in default. (A config-file layer is planned to slot in between the two as part of the storage-driver work; the intended order is env > config > default.) The override is scoped to the SQLite store only — team configs under teams/ are unaffected.
# Run against an isolated store
AGMSG_STORAGE_PATH=/tmp/agmsg-sandbox ./scripts/send.sh myteam alice bob "hi"Claude Code's sandbox restricts filesystem writes to the project directory. In monitor mode, watch.sh runs inside the sandbox and needs to write pidfiles and SQLite WAL files under ~/.agents/skills/agmsg/. If you have sandboxing enabled, add an allowlist entry to your settings:
~/.claude/settings.json (user-level — applies to all projects):
{
"sandbox": {
"filesystem": {
"allowWrite": [
"~/.agents/skills/agmsg/"
]
}
}
}The allowlist does not enable sandboxing by itself. Use /sandbox in Claude Code to choose a sandbox mode, or add "enabled": true alongside "filesystem" under "sandbox" to configure it in settings. The allowlist has no effect until sandboxing is enabled.
This can also go in project-level .claude/settings.local.json if you prefer per-project scope. The allowlist merges across all settings scopes and takes effect immediately — no restart needed.
If you installed agmsg under a custom command name (e.g. m), adjust the path accordingly (~/.agents/skills/m/).
Codex may run shell commands in a workspace-write sandbox. agmsg stores its
SQLite database and team metadata under ~/.agents/skills/<cmd>/ by default,
which is outside most project workspaces. If the sandbox cannot write there,
commands that append or update state can fail with errors such as
sqlite3.OperationalError: unable to open database file.
This affects operations such as:
- sending messages (
send.shwrites todb/messages.db) - claiming or acknowledging inbox handoffs (
inbox.sh,check-inbox.sh, andwatch.shupdate the message store) - joining, resetting, switching roles, or changing delivery mode (
teams/and config/state files may be updated)
If you use Codex with filesystem sandboxing enabled, allow writes to the agmsg skill storage directory in your Codex config.
Example ~/.codex/config.toml:
sandbox_mode = "workspace-write"
[sandbox_workspace_write]
writable_roots = [
"~/.agents/skills/agmsg/db",
"~/.agents/skills/agmsg/teams",
]If you installed agmsg under a custom command name, adjust the path accordingly:
[sandbox_workspace_write]
writable_roots = [
"~/.agents/skills/m/db",
"~/.agents/skills/m/teams",
]You can also allow the whole skill directory if your Codex setup supports that:
[sandbox_workspace_write]
writable_roots = [
"~/.agents/skills/agmsg",
]Codex only supports mode turn and mode off; it does not have Claude Code's
Monitor tool. The sandbox allowlist is still required for writes performed by
manual $agmsg commands and turn-end inbox checks.
Some Codex runtimes or automations may inject a managed permission profile for a single run. In that case, the run-specific writable roots must also include the agmsg storage paths; the user-level config alone may not be enough.
bats tests/ # requires bats-core: brew install bats-core~/.agents/skills/<cmd>/ # Folder name = command name
├── SKILL.md # Skill definition (read by CC & Codex)
├── agents/
│ └── openai.yaml # Codex metadata
├── scripts/ # Bash scripts (the type-agnostic engine)
│ ├── lib/ # Sourced helper libraries
│ └── drivers/types/<name>/ # Built-in agent-type drivers (manifest + runtime)
├── plugins/<axis>/<name>/ # External drivers you opt into (agmsg plugin trust)
├── db/messages.db # SQLite WAL-mode message store
└── teams/ # Team configs (self-contained)
└── <team>/
└── config.json
- Storage: Single SQLite file with WAL mode
- Concurrency: Multiple readers + 1 writer, no conflicts
- Dependencies:
bash,sqlite3(no Python required) - Auto detection: Stop hook checks inbox after each response (60s cooldown, configurable via
hook.check_interval) - No daemon: Direct filesystem access
- No network: Everything local
agmsg's pluggable units are drivers grouped by axis (types for agent
runtimes; storage and delivery to follow). Built-ins ship under
scripts/drivers/; you can drop your own under <skill>/plugins/<axis>/<name>/
(or point AGMSG_PLUGIN_DIRS at a directory) to extend agmsg without forking.
Because a driver is shell code that runs with your privileges, external drivers
are never loaded until you opt in — an unexpected drop-in is ignored (with a
warning) until you run agmsg plugin trust <axis>/<name>. List what's discovered
and its trust state with agmsg plugin list.
Full discovery order, the trust model, and authoring guidance: docs/plugins.md (design rationale in ADR 0002).
Writing something outside agmsg's own scripts that reads or drives agmsg —
a GUI app, a bot, a derivative project (agmsg-shogi, agmsg-go,
agmsg-mcp, …)? Read data via scripts/api.sh (JSON out, no need to touch
messages.db or teams/*/config.json directly — those are internal and free
to change), and write through the existing scripts (send.sh, join.sh,
…) rather than the database. Full guidance:
docs/building-on-agmsg.md.
- Product Hunt: #5 Product of the Day, 2026-06-09 launch — 219 upvotes, 39 comments
- Community projects (also on the showcase):
agkanban— multi-agent kanban board that pairs with agmsg;agmsg-office— replays message logs as characters speaking on a stage;agmsg-viewer— message history in a chat interface in the browser;agmsg-bubblelog— replays a team's log locally as a messenger-style thread;agmsg-tui— Rust/ratatui terminal client, friendly to SSH, mosh, and tmux - External contributors: @MiuraKatsu (Gemini support + whoami auto-detect), @roundrop (Copilot CLI support), @TOMONOSUKEJP (native Windows / Git Bash), @kenshin-yamada (watcher scoping fix), @utenadev (OpenCode contribution), @lucianlamp (native Windows PowerShell helpers), @tatsuya6502 (sandboxed Bash tool support), @Masashi-Ono0611 (project-path validation, watchdog and watcher fixes), @chemica-tan (Windows codex bridge: project compare and port parsing), @otsune (Git Bash quoting from PowerShell), @tsukimiya (OpenCode: resident spawn and monitor delivery)
agmsg.cc is an Astro project under site/.
- Source of truth:
site/(Astro + Tailwind). The built output is not committed — CI builds it. - Local preview:
cd site npm install npm run dev # http://localhost:4321, live reload # or, to serve the production build: npm run build && npm run preview
- Publish: pushing to
mainwith changes undersite/runs.github/workflows/pages.yml, which builds the site and deploys it to GitHub Pages. The custom domain is set bysite/public/CNAME. - The agent-types gallery is generated at build time from
scripts/drivers/types/*/type.conf, so adding an agent type surfaces it on the site automatically.
docs/ is developer documentation (markdown, ADRs, spec) read on GitHub — it is not the published site.
See Design & Architecture for developer documentation — identity model, data storage, hook system, and script responsibilities.
If agmsg saves you copy-paste round-trips, a GitHub star helps other people find it.
MIT


