ncl admin CLI. For a task-oriented walkthrough, see The ncl admin CLI.
Invocation
bin/ncl resolves the project root and execs the client via tsx — symlink it onto your PATH (ln -s "$(pwd)/bin/ncl" /usr/local/bin/ncl) or run pnpm ncl ... from the checkout. The client sends one request frame over the Unix socket at data/ncl.sock and exits.
Conventions that apply to every command:
- Positional IDs keep their dashes. The dispatcher matches the longest registered command that prefixes the words, and everything after it is the target ID — dashes intact.
ncl groups get abc123andncl tasks cancel task-374f-…-442both resolve;--id <uuid>still works everywhere. - Kebab and snake case are interchangeable.
--agent-group-idand--agent_group_idare the same flag; hyphens normalize to underscores. listaccepts column filters and--limit. Any non-generated column is a filter (ncl sessions list --container-status running), except a column literally namedid— the list handler always skips theidarg, soncl users list --id ...is silently ignored (affects users). Filters combine with AND. Default--limitis 200 (minimum 1). No ordering or offset flags.get,update,deleterequire--id— the value of the resource’s ID column.createvalidates enums, applies defaults, auto-generates the UUID primary key and*_attimestamps.updaterequires at least one updatable column flag, validates enums, and returns the full updated row.--jsonprints the raw response frame; default output is an aligned table for lists, pretty JSON otherwise,error (<code>): <message>on failure.- Exit codes:
0success,1command error,2transport error (host unreachable, missing command). - Error codes in response frames:
unknown-command,invalid-args,forbidden,approval-pending,handler-error,transport-error(plus reservedpermission-denied,not-found). - Help is introspected:
ncl helplists resources and verbs;ncl <resource> helpshows its fields, enums, and defaults.
Access control
Each operation has an access level:open or approval. Host callers (your terminal) bypass approval entirely — every command runs inline. When a containerized agent calls an approval operation, the command is not executed; an approval card goes to an admin, the call returns approval-pending, and on approval the host executes it and notifies the agent. Across the resources the rule is near-uniform: reads (list, get, config get) are open and mutations are approval — except tasks, where every verb is open: an agent scheduling its own future work is scoped to its own group and doesn’t gate on an admin.
Agent callers are also subject to the group’s cli_scope (set via ncl groups config update --cli-scope):
approvals
In-flight approval cards waiting for an admin response. Read-only; rows are deleted after approve/reject/expiry.
Columns (all usable as list filters):
approval_id, session_id, request_id, action (e.g. install_packages, add_mcp_server, onecli_credential), payload (JSON), created_at, agent_group_id, channel_type, platform_id, platform_message_id, expires_at, status (pending | approved | rejected | expired | awaiting_reason), approver_user_id, title, options_json.
destinations
Per-agent routing entries and ACL — each row lets an agent send to a target (channel or another agent) under a local name. Local names are unique per agent. Noget, create, update, or delete; mutations go through add/remove, which also project the change into every active session’s inbound.db so running containers pick it up without restart.
--target-id is a messaging_groups.id when --target-type channel, an agent_groups.id when agent.
dropped-messages
Log of messages dropped by the router or access gate, aggregated by(channel_type, platform_id) with a running count. List-only.
Columns:
channel_type, platform_id, user_id, sender_name, reason (no_agent_wired | no_agent_engaged | unknown_sender_strict | unknown_sender_request_approval | unknown_sender_decline_notify), messaging_group_id, agent_group_id, message_count, first_seen, last_seen.
groups
Agent groups — logical agent identities with their own workspace folder, history, and container image.add-mcp-server and remove-mcp-server refuse to touch a server owned by a stamped template — update the template and restamp instead. A plugin-owned server removed by hand reappears on the next restamp.
members
Membership rows granting unprivileged users access to an agent group. Admins/owners are implicitly members and need no row. Checked by the router when a wiring’ssender_scope is known.
messaging-groups
One chat or channel on one platform. Identity is the unique(channel_type, platform_id, instance) triple.
detached_at is read-only state maintained by channel membership hooks. It records when NanoClaw’s bot left a conversation, when the channel integration reports that departure. Not every adapter/SDK dispatches leave events. The row, wiring, destinations, and history remain intact; outbound delivery is refused until the bot rejoins and the hook clears the marker.
policies
Per-message approval gate on an agent-to-agent connection. A row forces every message from one agent to another to be approved by a human before delivery — without un-wiring the destination. No row = free flow. Directed and per-pair: gate both directions with two policies. Operator-only — agents can’t manage their own gates, and self-messages (--from equal to --to) are never gated.
set upserts — re-running with a different --approver replaces it. Only the named --approver (or an owner) can approve a held message. Removing a policy returns the edge to free flow. Columns: from_agent_group_id, to_agent_group_id, approver, created_at.
roles
Privilege grants.owner is always global; admin can be global (agent_group_id null) or scoped to one group. Admin at a group implies membership.
sessions
The runtime unit — one(agent_group, messaging_group, thread) combination mapped to a container. Created automatically by the router; read-only in the CLI.
Columns:
id, agent_group_id, messaging_group_id (null for agent-shared sessions), thread_id (per-thread mode only), agent_provider (null = inherit from group), status (active | closed), container_status (running | idle | stopped — idle is reserved and currently unused), last_active, created_at.
history merges inbound and outbound rows chronologically so an agent can catch up on another session in its own group. Human output is timestamp|direction|kind|sender|text, with text capped at 200 characters. --json returns the raw rows with full text. The default limit is 50; the command selects the newest rows and prints them oldest-to-newest.
tasks
Scheduled tasks — the CLI home for what used to be the agent’sschedule_task MCP tools. Each task is a series: the live pending/paused row is the next run, and completed rows are its run history. Tasks are created from the host or by an agent inside its own task session, and each runs in an isolated per-task session that the host closes once the series is spent (history remains until explicit deletion). Ids are t-<hex>, or <slug>-<hex> when you pass --name.
run fires a task now without changing its schedule (a safe test that neither consumes a one-shot nor advances a recurring series — unlike update --process-after now). append-log adds a note to a task’s run log (tasks/<id>.md); every run already auto-logs its final text, so it’s for additive mid-run notes only.
delete refuses a running task session. For an isolated task it removes the session mailbox, history, attachments, heartbeat, session directory and row, and run log. For legacy tasks in a shared session it removes only the series rows and run log.
user-dms
DM route cache mapping(user, channel_type) to the messaging group used for cold DMs (approvals, pairing). Populated lazily for the default adapter instance; named instances resolve their own routes without using this cache. List-only.
Columns:
user_id, channel_type, messaging_group_id, resolved_at.
users
Messaging-platform identities — one row per sender per channel (no cross-channel linking yet). No delete.wirings
Connects a messaging group to an agent group — which agent handles which chat. Many-to-many in both directions. Undergroup CLI scope, agents get self-service access to the wiring of their current conversation only: wirings get and wirings update (limited to engage_mode/engage_pattern; the id is force-filled, updates stay approval-gated).