| name | agmsg |
| description | Cross-agent messaging via SQLite. Send messages between Claude Code, Codex, Gemini CLI, GitHub Copilot CLI, and other agents. No daemon, no network, no dependencies beyond bash and sqlite3. |
Agent Messaging
IMPORTANT: Always use the provided scripts. NEVER directly read or edit config files, DB, or team data. There is NO register.sh — use join.sh to join a team.
Shell requirement: All agmsg scripts are Bash scripts. Always execute them via bash, never via PowerShell or cmd directly. If your default shell is not Bash (e.g. PowerShell on Windows), wrap every command with bash -lc '...'. Example: bash -lc '~/.agents/skills/agmsg/scripts/send.sh myteam alice bob "hello"'. Do NOT construct DB paths manually — the scripts handle path resolution internally. If you need to redirect storage, use AGMSG_STORAGE_PATH (the supported override).
How to use
Step 0: First-run bootstrap
agmsg keeps its SQLite database, team registry, and runtime state under ~/.agents/skills/agmsg/. The ./install.sh install path creates that tree; the Claude Code plugin install path does not (the plugin marketplace flow only drops the skill content into ~/.claude/plugins/cache/). Before any other command, bootstrap if needed:
if [ ! -d ~/.agents/skills/agmsg ]; then
installer=$(ls ~/.claude/plugins/cache/fujibee-agmsg/agmsg/*/install.sh 2>/dev/null | head -1)
if [ -n "$installer" ]; then
bash "$installer" --cmd agmsg
else
echo "agmsg not installed. Either:" >&2
echo " - run ./install.sh in the agmsg repo, or" >&2
echo " - install via /plugin marketplace add fujibee/agmsg && /plugin install agmsg@fujibee-agmsg" >&2
exit 1
fi
fi
After this runs once, ~/.agents/skills/agmsg/ is populated and you can skip Step 0 on future invocations.
Step 1: Check identity
~/.agents/skills/agmsg/scripts/whoami.sh "$(pwd)" <type>
Step 2a: If not in a team — join one
Before first-time setup, inspect the user's request. If they ask to join,
import, or bring in a team that already exists on a server, do not run
join.sh. Go directly to the remote pull command under Step 2b. Before
pulling, run team-list.sh --json --scope all; if a same-named local team has
binding_state none or disconnected, stop and ask the user how to proceed.
After pull succeeds, return here so the user can register a new local agent in
the team that pull just created.
Ask the user for a team name. If it's an existing team, run team.sh <team> first to see the current roster and note the names already in use. Look for a naming convention already in play (e.g. a shared base name with role and number suffixes (<base>-<role><n>), or names derived from the team name) and, when one exists, propose 2-3 unused names that extend it; otherwise propose 2-3 short, distinctive identity names (not a bare tool-type label like codex/cc). Either way, names must not collide with the roster. For a brand-new team, skip the roster check and just ask. Then run:
~/.agents/skills/agmsg/scripts/join.sh <team> <agent_name> <type> "$(pwd)" [--force]
Do NOT manually edit config files. Always use join.sh. If the name was recently renamed away with rename.sh, join.sh refuses to revive it (printing the new name it maps to) instead of silently re-registering it — this guards against a CLI slash-command history resubmitting actas <old_name> after a rename. Pass --force only for a deliberate, unrelated reuse of that exact name.
Step 2b: If already in a team — execute command
Default (no arguments): IMMEDIATELY check inbox. Do NOT ask what to do.
~/.agents/skills/agmsg/scripts/inbox.sh <team> <agent_id>
~/.agents/skills/agmsg/scripts/send.sh <team> <from_agent> <to_agent> "<message>" [--force]
~/.agents/skills/agmsg/scripts/history.sh <team> [agent_id] [limit]
~/.agents/skills/agmsg/scripts/export.sh --team <team> [--agent <agent>] [--limit N] [--out <file>]
~/.agents/skills/agmsg/scripts/team.sh <team>
~/.agents/skills/agmsg/scripts/team-list.sh [--json] [--scope all|project] [<project_path>]
~/.agents/skills/agmsg/scripts/leave.sh <team> <agent_id>
~/.agents/skills/agmsg/scripts/rename-team.sh <old_team> <new_team>
~/.agents/skills/agmsg/scripts/version.sh
~/.agents/skills/agmsg/scripts/reset.sh <> [agent_id] [session_id]
~/.agents/skills/agmsg/scripts/delivery.sh <mode> <>
~/.agents/skills/agmsg/scripts/delivery.sh status <>
~/.agents/skills/agmsg/scripts/actas-claim.sh <> <name>
~/.agents/skills/agmsg/scripts/reset.sh <> <name>
~/.agents/skills/agmsg/scripts/spawn.sh <claude-code|codex> <name> [options]
~/.agents/skills/agmsg/scripts/despawn.sh <team> <from> <name> [--force] [-- N]
Rename
If argument starts with "rename" but not "rename-team":
- Accept only an explicit user request. Parse either
<team> <old_name> <new_name>,
or <old_name> <new_name> only when this agent belongs to exactly one team.
- Never invent either name. Before execution, repeat the resolved team, old name,
and new name and ask the user to confirm. Wait for confirmation.
- Run:
bash ~/.agents/skills/agmsg/scripts/rename.sh <team> <old_name> <new_name>
- Show the result. For a connected team, the
member_renamed journal event
propagates the rename to other machines.
If argument starts with "rename-team":
- Accept only an explicit user request. Parse
<old_team> <new_team>.
- Never invent either team name. Before execution, repeat the old and new team
names and ask the user to confirm. Wait for confirmation.
- Run:
bash ~/.agents/skills/agmsg/scripts/rename-team.sh <old_team> <new_team>
- Show the result.
Remote sync
Remote setup is no-auth. Do not ask for a token or create one.
If argument starts with "remote connect":
- Parse the required
--endpoint <url> and <team>, plus optional --e2ee.
- Run:
bash ~/.agents/skills/agmsg/scripts/remote.sh connect --endpoint <url> [--e2ee] <team>
- Show the output to the user. Plain sync is the default; pass
--e2ee only
when the user explicitly requests end-to-end encryption. The choice is
fixed by the first connect.
- End by showing this copy-paste command for the other machine, with the
actual endpoint and team substituted:
bash ~/.agents/skills/agmsg/scripts/remote.sh pull --endpoint <actual-url> <actual-team>
If argument starts with "remote pull":
- When the user asks to join or bring in a team that already exists on a
server, NEVER use
join.sh, create a team, or create a same-named local
team. Always use remote pull.
- Before pulling, check for a same-named local team. If one already exists
without an active remote connection, stop and ask the user how to proceed;
do not overwrite, merge, connect, or rename it on your own.
- Parse the required
--endpoint <url> and <team>, plus optional
--team-id <uuid>.
- Run:
bash ~/.agents/skills/agmsg/scripts/remote.sh pull --endpoint <url> [--team-id <uuid>] <team>
- Show the output to the user.
Machine B needs its own install, not just its own environment variables.
Only remote.sh, remote-sync.sh, key.sh and the two internal helpers read
AGMSG_SYNC_CONNECTION_DIR; send.sh, history.sh, team.sh and inbox.sh
resolve the team config from the install directory. So a pull driven by
environment variables alone succeeds, and the send that is supposed to confirm
it then reports the team as missing — the failure lands one step after the
cause. See "Use a separate install for testing" in docs/remote-setup.md.
If argument starts with "remote unlock":
- Parse
<team>, --bundle <file>, and --confirm-digest <sha256>.
- Run:
bash ~/.agents/skills/agmsg/scripts/remote.sh unlock <team> --bundle <file> --confirm-digest <sha256>
- The snapshot digest must be compared over a separate live channel. Never
infer or auto-confirm it. The bundle is permanent secret key material; tell
the user to transfer and handle it only through their own trusted channel,
never by pasting it into agent chat.
- Show the complete result, including the imported-envelope count and engine
PID.
- The advanced form with repeatable
--snapshot plus --identity or
--identity-stdin remains available when explicitly requested.
If argument starts with "remote status":
- Parse an optional
<team> and --json.
- Run:
bash ~/.agents/skills/agmsg/scripts/remote.sh status [<team>] [--json]
- Show the output to the user.
If argument starts with "remote sync start":
- Parse the required
<team>.
- Run:
bash ~/.agents/skills/agmsg/scripts/remote.sh sync start <team>
- Show the output to the user.
If argument starts with "remote disconnect":
- Parse the required
<team>.
- Run:
bash ~/.agents/skills/agmsg/scripts/remote.sh disconnect <team>
- Show the output to the user.
If argument starts with "remote forget":
- Parse the required
<team>. This permanently deletes that team's local
roster, history, keys, trust, and sync state, but never changes the server.
- Do not add
--yes yourself. Run:
bash ~/.agents/skills/agmsg/scripts/remote.sh forget <team>
- The command requires the user to confirm in their terminal. If this agent
has no interactive terminal, show the deletion summary and tell the user to
rerun the displayed command directly; never bypass confirmation for them.
End-to-end encryption
If argument starts with "key generate" followed by an optional team name:
- Run:
bash ~/.agents/skills/agmsg/scripts/key.sh generate [<team>]
- Show the full output to the user, including the mandatory key-backup notice.
If argument starts with "key show":
- Parse an optional team name and
--reveal-secret.
- Run:
bash ~/.agents/skills/agmsg/scripts/key.sh show [<team>] [--reveal-secret]
--reveal-secret requires a real interactive terminal and is refused in
agent mode. Tell the user to run it directly in their own terminal.
- Show the output to the user.
If argument starts with "key handoff" followed by a team name:
- Parse optional
--out <file> and run:
bash ~/.agents/skills/agmsg/scripts/key.sh handoff <team> [--out <file>]
- The output bundle contains every epoch identity and is itself permanent
secret key material. Never read it into agent chat or display its contents.
- Show the bundle path, latest snapshot digest, and full secrecy warning.
If argument starts with "key import" followed by a team name:
- Do not ask the user to paste the private identity into this chat, and do not
run the command yourself. Tell the user to run this in their own terminal:
read -rsp 'Identity: ' IDENTITY; echo
printf '%s' "$IDENTITY" | ~/.agents/skills/agmsg/scripts/key.sh import <team> --identity-stdin
unset IDENTITY
- Ask them to paste back only the command output, never the identity itself.
- Do not offer an environment-variable path. An identity file is a permanent
secret; always use the human-in-own-terminal flow above.
If argument starts with "key rotate" followed by a team name:
- Rotation mints a replacement epoch for a team that already has a key and
announces it on the roster journal. It requires an existing current key, an
identity journal (connect or migrate the team first), and
age; it refuses
with a message naming whichever is missing.
- Confirm with the user before running it. It changes the team's key state,
and every other machine has to receive the new identity out of band.
- Run:
bash ~/.agents/skills/agmsg/scripts/key.sh rotate <team>
- Show the output: epoch, key_id, and recipient fingerprint. The private key
is never written to the journal. Revealing it needs
key show <team> --key-id <id> --reveal-secret, which is refused in agent
mode — tell the user to run that in their own terminal.
- Messages before the acknowledged rotation boundary remain readable with the
old key.
Device pairing (key request / key approve) is not implemented — they are
not key.sh subcommands, so a call prints usage and exits 1. If the user asks
for one, tell them so instead of attempting to run it.
Permission prompts (Claude Code)
Every agmsg step above runs through the host's Bash tool, so on Claude Code each call is gated by the permission system until you allowlist the script directory. Add these to ~/.claude/settings.json (or project-level .claude/settings.local.json):
{
"permissions": {
"allow": [
"Bash(~/.agents/skills/agmsg/scripts/*)",
"Bash(/Users/<you>/.agents/skills/agmsg/scripts/*)",
"Bash(bash ~/.agents/skills/agmsg/scripts/*)",
"Bash(bash /Users/<you>/.agents/skills/agmsg/scripts/*)"
]
}
}
Four entries rather than one because a rule matches the command string as written: the scripts are invoked both as ~/... and as an absolute path, and with or without an explicit bash prefix. Replace /Users/<you> with your home directory, and the agmsg path segment with your command name if you installed under a different one.
Every subcommand needs its own match. Claude Code's permission docs state that a rule must match each subcommand independently, and that the recognized separators are &&, ||, ;, |, |&, &, and newlines. Chaining two agmsg scripts is fine — both match the entries above. What reintroduces the prompt is mixing in a command those entries do not cover: delivery.sh status … ; printenv AGMSG_SPAWNED prompts because of the printenv, not because of the ;. Splitting it into its own call does not remove that prompt — it only keeps it from gating the agmsg call. Allowlist the command as well if it needs to be prompt-free.
Sandbox compatibility (Claude Code)
When Claude Code's sandbox is enabled, watch.sh (monitor mode) runs inside the sandbox and needs to write pidfiles and SQLite WAL files under ~/.agents/skills/agmsg/. Add an allowlist entry to ~/.claude/settings.json (or project-level .claude/settings.local.json):
{
"sandbox": {
"filesystem": {
"allowWrite": [
"~/.agents/skills/agmsg/"
]
}
}
}
The allowlist merges across scopes and takes effect immediately — no restart needed. If agmsg was installed under a custom command name (e.g. m), adjust the path accordingly.
Note on BASH_SOURCE: The sandboxed Bash tool runs commands via pipe/eval, so BASH_SOURCE[0] is empty inside sourced functions like storage.sh. This is handled internally — watch.sh resolves SKILL_DIR from $0 (which works correctly when invoked as a command), and storage.sh falls back to that value. No user configuration needed.
Architecture
- Storage: SQLite with WAL mode in
~/.agents/skills/agmsg/db/messages.db
- Teams:
~/.agents/skills/agmsg/teams/<name>/config.json
- Concurrency: WAL allows multiple readers + 1 writer without conflicts
- No daemon: Direct DB access via
sqlite3 CLI
- Dependencies: bash, sqlite3 (no python3 required) for core messaging;
remote/key additionally need curl, python3, and age/age-keygen, but only if those commands are used