slack-a2a-rooms skill
Agent-to-agent Slack rooms — a group DM (MPIM) holding a human plus two or more NanoClaw sibling bots, where each bot hears the room over its own Socket Mode connection. Registers the admission policy on the Slack channel's bot-inbound guard so bot-authored inbound is admitted only for rooms allowlisted in SLACK_A2A_ROOMS (re-attributed as slack:bot:<bot_id>, hop-limited via SLACK_A2A_MAX_HOPS), plus scripts/open-a2a-room.ts to open a room and register it.
Is the slack-a2a-rooms skill safe?
Clean: nothing in its files matched our rules. We read 4 files in the folder on 2026-09-28.
No findings.
Install the slack-a2a-rooms skill
A skill is a folder. Copy it into your agent's skills folder and the agent loads it when the task matches its description.
git clone --depth 1 https://github.com/nanocoai/nanoclaw.git /tmp/nanoclaw mkdir -p ~/.claude/skills cp -r /tmp/nanoclaw/.claude/skills/slack-a2a-rooms ~/.claude/skills/slack-a2a-rooms
In the Claude apps, zip the folder and upload it from the Skills settings. The folder on GitHub
The instructions your agent would load
SKILL.md as published, without the frontmatter. Read it on GitHub
Slack agent-to-agent rooms (SLACKA2AROOMS)
Lets two or more NanoClaw Slack bots talk to each other — and to a human — in a shared group DM (MPIM). Empirically established against live Slack: conversations.open with users=[, …] works from a bot token holding mpim:write and returns an ismpim channel, and bots receive each other's messages over their own Socket Mode connections as plain message events with channeltype: "mpim", bot_id set, and subtype null.
Canonical home. This directory on main is the skill's canonical source — the setup wizard and any direct apply read it from the checkout. The copy on the channels branch is a compatibility mirror for older checkouts whose setup fetches companions from there; edits land here, never there.
Where sibling-bot messages die today. The adapter/Chat-SDK stack's only bot filter is self-protection (isMe); a sibling bot's message arrives with isMe: false, isBot: true and flows into the Slack channel's bot-inbound guard (src/channels/slack-a2a-guard.ts, installed with /add-slack), which drops all bot-authored inbound at the bridge boundary by default — before the router, before any sender-approval flow. The guard exposes a single admission seam, setBotInboundPolicy; this skill registers the policy that opens it up selectively:
to the user id slack:bot:, under a consecutive-hop limit.
- Rooms listed in SLACKA2AROOMS: bot-authored inbound passes, attributed
as the guard's default already does.
- Everywhere else: bot-authored inbound stays dropped at the bridge, exactly
Loop safety. After N consecutive bot-authored inbound messages in an A2A room without a human message (N = SLACKA2AMAX_HOPS, default 6), further bot messages are dropped with a log line until a human speaks. The counter is per bot identity and per room; any human message resets it.
Requires:
bot-inbound guard (src/channels/slack-a2a-guard.ts with the setBotInboundPolicy seam). The default drop arrives with that payload; this skill only adds the allowlisted-room admission on top.
- The Slack channel installed (/add-slack), current enough to ship the
hosts sharing the workspace). Named identities are registered natively by the adapter from SLACKINSTANCES — see the slack-multi-instance skill for the env-key format. scripts/open-a2a-room.ts reads tokens by that convention (SLACKBOTTOKEN; default → SLACKBOTTOKEN).
- At least two Slack bot identities on this host (or sibling bots on other
mpim:write (open the room), mpim:history + mpim:read (see it), and the message.mpim bot event (hear it). Apps provisioned through the managed flow (apps.manifest.create + apps.managedInstall) already include all of these.
- Every participating app's manifest must carry the MPIM scopes and event:
Apply
1. Verify the installed Slack channel ships the bot-inbound guard
The policy module copied next imports setBotInboundPolicy from the installed src/channels/slack-a2a-guard.ts. On a Slack payload that predates the guard, that import takes down the channel barrel — and with it every adapter — so verify the seam first. If the check fails, stop: re-run /add-slack from a channels branch that ships the guard, then re-apply this skill.
grep -sq 'export function setBotInboundPolicy' src/channels/slack-a2a-guard.ts || { echo 'slack-a2a-rooms: src/channels/slack-a2a-guard.ts is missing or does not export setBotInboundPolicy. Installing anyway would break the channel barrel and take down every channel adapter. Update the installed Slack channel first (re-run /add-slack from a channels branch that ships the bot-inbound guard), then re-apply this skill.' >&2; exit 1; }2. Copy the policy module, its guard test, and the room-opener script
This skill ships three files alongside this document; copy them into the tree at the same relative paths (overwrite; the skill's copies are canonical):
src/channels/slack-a2a.ts
src/channels/slack-a2a.test.ts
scripts/open-a2a-room.tsSLACKA2AMAX_HOPS parsing (re-read with a ~30s cache so a freshly opened room needs no restart), the per-room, per-identity consecutive-hop counter with human reset, and the slack:bot: re-attribution. The module registers itself onto the guard's admission seam on import.
- slack-a2a.ts — the admission policy: SLACKA2AROOMS /
real bot-inbound guard end-to-end (see step 4 for what it pins).
- slack-a2a.test.ts — the guard: drives the real channel barrel and the
- open-a2a-room.ts — operator CLI to open a room (see Configuration).
3. Register the policy module
Append the self-registration import to the channel barrel (skipped if the line is already present). Appending at the end keeps it after the Slack channel's own imports:
import './slack-a2a.js';4. Build and validate
Build first — it guards the typed setBotInboundPolicy call against guard drift. The test imports the real channel barrel (a deleted or broken barrel line goes red) and asserts the policy end-to-end: allowlisted-room admission with slack:bot: re-attribution, non-listed-room drop, the hop limit with human reset, per-room/per-identity budgets, and that a downstream throw consumes no hop budget:
pnpm run buildpnpm exec vitest run src/channels/slack-a2a.test.tsConfiguration
.env keys (both re-read with a ~30s cache — no restart needed after edits):
opener script prints, e.g. G0AAAAAAA — note MPIM ids may start with G or C depending on workspace vintage). Only these rooms admit bot-authored inbound.
- SLACKA2AROOMS — comma-separated raw Slack channel ids (the MPIM ids the
an A2A room without a human message before further bot messages are dropped. Default 6.
- SLACKA2AMAX_HOPS — consecutive bot-authored inbound messages allowed in
Opening a room
pnpm exec tsx scripts/open-a2a-room.ts --instances dana,eli --user U0AAAAAAAThe first listed instance opens the conversation (via conversations.open with the human + the other bots' user ids, resolved through auth.test per token) and posts an intro message. The script prints the channel id and appends it to SLACKA2AROOMS in .env. Without --user you get a bots-only room, which needs at least three instances (Slack collapses a two-party open into a 1:1 IM).
Letting bot senders through the access gate
The room allowlist gets bot messages to the router; the permissions module still gates them like any sender. Bot senders arrive as user id slack:bot:, which starts unknown. After the first human mention in the room auto-creates its messaging group, either set the room public:
pnpm exec tsx scripts/q.ts data/v2.db "UPDATE messaging_groups SET unknown_sender_policy='public' WHERE platform_id='slack:<channel id>'"or keep request_approval and approve each slack:bot: sender once (or add them as members of the agent group). A private A2A room with known humans is a reasonable place for public.
Engagement: A2A conversation is mention-driven
An MPIM is a group context in NanoClaw's channel-defaults model (Slack DMs are only D… channels), so the Slack group defaults apply: engagemode: mention-sticky, per-thread stickiness. That means a bot replies when it is @-mentioned (then stays engaged in that thread) — it does not answer every room message. Bot-to-bot conversation is therefore mention-driven by design: bot A's reply reaches bot B's agent when it @-mentions bot B, and the chain continues only as long as each reply mentions the next speaker. Slack does not emit appmention for bot-authored messages, but mention detection still works: the Chat SDK's text-level detector matches the bot's own <@U…> token, which the adapter deliberately leaves unresolved in the text. This is the intended loop governor alongside the hop limit — prompt the agents (group CLAUDE.md / personality) to @-mention the sibling they want an answer from, and to stop mentioning anyone when the exchange has converged.
Remove
src/channels/slack-a2a.test.ts, scripts/open-a2a-room.ts.
- Delete the three copied files: src/channels/slack-a2a.ts,
unknownsenderpolicy='public', and archive/leave the MPIMs from Slack (the rooms themselves are ordinary Slack conversations; NanoClaw holds no other state for them beyond the usual messaging-group/session rows).
- Delete the import './slack-a2a.js'; line from src/channels/index.ts.
- Remove SLACKA2AROOMS and SLACKA2AMAX_HOPS from .env.
- Optionally re-tighten any messaging groups you set to
- Rebuild (pnpm run build).
With the skill removed, the channel guard's default applies everywhere again: bot-authored inbound is dropped in every room.
Notes
drops bot-authored messages in rooms not listed in SLACKA2AROOMS at the bridge — before the router and before any sender-approval flow — with or without this skill applied. If an install relies on a bot sender (another workspace app posting into a channel the agent watches), add that room to SLACKA2AROOMS. Human messages are never affected.
- Bot senders outside A2A rooms: the Slack channel's bot-inbound guard
More skills from nanocoai/nanoclaw
- Aadd-anydocAdd local office-document-to-Markdown conversion to NanoClaw agent containers with the pinned Firecrawl AnyDoc CLI. Use when agents need to read attached Word, PowerPoint, Excel, OpenDocument, RTF, EPUB, CSV, or text-based PDF files without uploading them to a hosted parser.
- Aadd-atomic-chat-toolAdd Atomic Chat MCP server so the container agent can call local models served by the Atomic Chat desktop app via its OpenAI-compatible API.
- Fadd-clidashAdd clidash — a zero-dependency, read-only web dashboard that derives its tabs and tables at runtime from any CLI that lists resources as JSON. Ships pre-wired for NanoClaw's ncl CLI (agent groups, sessions, channels, users, roles), plus message-activity charts, a log tail, and a read-only file viewer for group skills/CLAUDE.md/profiles.
- Aadd-codexUse Codex (OpenAI's codex app-server) as a full agent provider — planning, tool orchestration, MCP tools, server-side history, session resume — alongside or instead of Claude. ChatGPT subscription or OpenAI API key, vault-only via the selected gateway. Per-group via `ncl groups config update --provider codex`. Distinct from using OpenAI as an MCP tool (where Claude remains the planner).
- Aadd-dashboardAdd a monitoring dashboard to NanoClaw. Installs @nanoco/nanoclaw-dashboard and a pusher that sends periodic JSON snapshots.
- Aadd-deltachatAdd DeltaChat channel integration via @deltachat/stdio-rpc-server. Native adapter — no Chat SDK bridge. Email-based messaging with end-to-end encryption.
- Cadd-dialAdd Dial channel integration — a real phone number for SMS and AI voice calls via the Dial platform (getdial.ai). Native adapter — no Chat SDK bridge.
- Aadd-dial-numberAdd another phone number to an existing Dial channel — a second (or third) public line for the agent, so one NanoClaw install answers SMS and AI voice calls on multiple numbers. Use when Dial is already installed and the operator wants an additional number (e.g. a personal line plus a support line). Requires the Dial channel to already be installed (see /add-dial).
- Aadd-dial-toolGive chosen NanoClaw agents a real phone number as a container tool — the `dial` CLI baked into the agent image plus OneCLI credential injection for api.getdial.ai, scoped per agent, so the agents you pick can send SMS, place AI voice calls, and receive verification codes from inside the sandbox. Independent of the Dial channel; idempotent; re-run to change which agents may use it. Use when the user wants agents to text, call, or run `dial …` from a chat, without wiring Dial as a messaging channel.
- Aadd-discordAdd Discord bot channel integration via Chat SDK.
- Aadd-emacsAdd Emacs as a channel. Opens an interactive chat buffer and org-mode integration so you can talk to NanoClaw from within Emacs (Doom, Spacemacs, or vanilla). Local HTTP bridge — no bot token or external service needed.
- Aadd-gchatAdd Google Chat channel integration via Chat SDK.