ncl on your PATH (or pnpm ncl from the checkout), and Docker up.
Create the agent group
An agent group is just a row in the central DB plus a folder under Now check the filesystem:
groups/ — but the folder doesn’t exist yet, as you’ll see.ncl prints the inserted row, including the generated UUID. Save it — every later command takes --id:ls groups/ — there is no scout/ folder. Creation only writes the DB row. The folder (and the container config, session state, and skills directory) is initialized lazily by initGroupFilesystem() the first time a container spawns for this group. An agent that has never been messaged costs nothing.Find the CLI messaging group
A messaging group is one chat on one platform, identified by a unique (Note the
channel_type, platform_id) pair. The CLI channel hardcodes its platform ID to local, and setup leaves the “Local CLI” messaging group in place, so it should already exist:id of the row with platform_id = local. If the list is empty (CLI-less install), create it exactly as the setup script does — unknown_sender_policy must be public because the terminal’s synthetic cli:local user holds no role:Wire them together
A wiring tells the router which agent handles messages from which chat, and when it engages:Two things matter here:
--engage-mode patternis required on CLI. The default mode,mention, relies on platform-level @-mentions, which the CLI channel never produces — amentionwiring on CLI simply never fires.- The pattern is a JavaScript regex tested against the message text.
^[Ss]cout\bengages Scout only when a message starts with its name. Don’t use inline flags like(?i)— JS regexes reject them, and an invalid pattern fails open (the agent responds to everything).
Talk to it
data/cli.sock, the router matches your pattern, creates a session, and spawns a container. First contact is a cold start — expect 30–60 seconds before the reply prints. Follow-ups to a warm container are much faster, and the session persists server-side, so context carries across invocations.If nothing comes back after the cold start, check what the router did with the message:no_agent_engaged drop row only appears when no wiring on the chat matched at all — in this tutorial’s setup a catch-all (or complementary) pattern always engages one agent, so if the table is empty, check whether the other agent answered instead: your message probably didn’t start with “scout”.Inspect what was created
That first message materialized everything that was lazy in step 1. The group folder now exists:Look for It stays warm for a while after the conversation, then exits; the host respawns it on the next message. To force a fresh one (with an image rebuild, e.g. after adding packages):
CLAUDE.md is the composed instructions file the host regenerates on every spawn — don’t edit it. CLAUDE.local.md is the agent’s editable persistent memory, auto-loaded on every spawn. Groups created via ncl start with it empty — edit it to give Scout a personality and standing instructions (see customize an agent).The session — the runtime unit mapping this (agent, chat) pair to a container — is visible too:status = active and container_status = running. And the container itself, named nanoclaw-v2-<folder>-<timestamp>:What you built
One agent identity, one chat, one routing rule between them — and a session/container pair that only exists because you sent a message. Every channel in NanoClaw works through these same four pieces; only the adapter changes. See the entity model for the full picture.Next steps
- Customize an agent — memory, container config, models, packages
- Multi-agent swarm — wire several agents that talk to each other
- Channels overview — wire Scout to WhatsApp, Telegram, Discord, and more