Skip to main content
NanoClaw uses a filesystem-based IPC system to enable communication between the host process and containerized agents. This design avoids network sockets while maintaining security boundaries.

Architecture overview

The IPC system is built on three core components:
  1. Per-group namespaces - Each group gets its own IPC directory at data/ipc/{group}/
  2. File-based message passing - JSON files are written to subdirectories and polled by the host
  3. Authorization checks - The directory name determines the source group identity
data/ipc
main
messages
tasks
input
family-chat
messages
tasks
input
errors

How it works

Starting the IPC watcher

The IPC watcher starts at application launch and runs continuously, polling for new files at IPC_POLL_INTERVAL (default: 1000ms on the host). Inside containers, the agent runner polls its own IPC input directory at 500ms intervals. From src/ipc.ts:
export function startIpcWatcher(deps: IpcDeps): void {
  if (ipcWatcherRunning) {
    logger.debug('IPC watcher already running, skipping duplicate start');
    return;
  }
  ipcWatcherRunning = true;

  const ipcBaseDir = path.join(DATA_DIR, 'ipc');
  fs.mkdirSync(ipcBaseDir, { recursive: true });

  const processIpcFiles = async () => {
    // Scan all group IPC directories
    let groupFolders: string[];
    try {
      groupFolders = fs.readdirSync(ipcBaseDir).filter((f) => {
        const stat = fs.statSync(path.join(ipcBaseDir, f));
        return stat.isDirectory() && f !== 'errors';
      });
    } catch (err) {
      logger.error({ err }, 'Error reading IPC base directory');
      setTimeout(processIpcFiles, IPC_POLL_INTERVAL);
      return;
    }
    // ...
  };

  processIpcFiles();
}

Message flow

1

Agent writes IPC file

The agent running in a container writes a JSON file to /workspace/ipc/messages/ or /workspace/ipc/tasks/.Example message file:
{
  "type": "message",
  "chatJid": "1234567890@s.whatsapp.net",
  "text": "Hello from the agent!"
}
The chatJid format varies by channel: 1234567890@s.whatsapp.net for WhatsApp, tg:12345 for Telegram, discord:12345 for Discord, etc.
2

Host polls IPC directory

The IPC watcher on the host scans data/ipc/{group}/messages/ and finds the new JSON file.
3

Authorization check

The host determines the source group from the directory path ({group}) and verifies the operation is authorized.From src/ipc.ts:
// Authorization: verify this group can send to this chatJid
const targetGroup = registeredGroups[data.chatJid];
if (
  isMain ||
  (targetGroup && targetGroup.folder === sourceGroup)
) {
  await deps.sendMessage(data.chatJid, data.text);
} else {
  logger.warn(
    { chatJid: data.chatJid, sourceGroup },
    'Unauthorized IPC message attempt blocked',
  );
}
4

Execute operation

If authorized, the host executes the requested operation (send message, schedule task, etc.).
5

Delete IPC file

The IPC file is deleted after processing. If processing fails, the file is moved to data/ipc/errors/ for debugging.

IPC operations

Send message

Allows agents to send messages to chats. File location: data/ipc/{group}/messages/message-{timestamp}.json Format:
{
  "type": "message",
  "chatJid": "1234567890@s.whatsapp.net",
  "text": "Message content"
}
The host extracts chatJid and text from the JSON file. Any additional fields are ignored by the core IPC handler. Authorization:
  • Main group can send to any chat
  • Non-main groups can only send to their own chat

Schedule task

Creates a scheduled task that runs at specified intervals. File location: data/ipc/{group}/tasks/task-{timestamp}.json Format:
{
  "type": "schedule_task",
  "prompt": "Task prompt for the agent",
  "schedule_type": "cron",
  "schedule_value": "0 9 * * 1",
  "context_mode": "group",
  "targetJid": "1234567890@s.whatsapp.net"
}
Parameters:
  • schedule_type: cron, interval, or once
  • schedule_value: Cron expression, milliseconds, or ISO timestamp
  • context_mode: group (shared session) or isolated (fresh session)
  • targetJid: The chat JID to associate with this task
Authorization:
  • Main group can schedule tasks for any group
  • Non-main groups can only schedule tasks for themselves
From the processTaskIpc function in src/ipc.ts:
// Authorization: non-main groups can only schedule for themselves
if (!isMain && targetFolder !== sourceGroup) {
  logger.warn(
    { sourceGroup, targetFolder },
    'Unauthorized schedule_task attempt blocked',
  );
  break;
}

Pause task

Pauses an active task. Format:
{
  "type": "pause_task",
  "taskId": "task-1234567890-abc123"
}

Resume task

Resumes a paused task. Format:
{
  "type": "resume_task",
  "taskId": "task-1234567890-abc123"
}

Update task

Updates an existing task’s prompt, schedule type, or schedule value. The next run time is automatically recalculated when the schedule changes. Format:
{
  "type": "update_task",
  "taskId": "task-1234567890-abc123",
  "prompt": "Updated task prompt",
  "schedule_type": "cron",
  "schedule_value": "0 10 * * *"
}
All fields except taskId are optional — only include the fields you want to change. Authorization:
  • Main group can update any task
  • Non-main groups can only update their own tasks

Cancel task

Deletes a task permanently. Format:
{
  "type": "cancel_task",
  "taskId": "task-1234567890-abc123"
}

Refresh groups

Forces a refresh of group metadata. Main group only. Format:
{
  "type": "refresh_groups"
}
From src/ipc.ts:
case 'refresh_groups':
  // Only main group can request a refresh
  if (isMain) {
    logger.info(
      { sourceGroup },
      'Group metadata refresh requested via IPC',
    );
    await deps.syncGroups(true);
  } else {
    logger.warn(
      { sourceGroup },
      'Unauthorized refresh_groups attempt blocked',
    );
  }
  break;

Register group

Registers a new group for agent access. Main group only. Format:
{
  "type": "register_group",
  "jid": "1234567890-9876543210@g.us",
  "name": "Family Chat",
  "folder": "family-chat",
  "trigger": "@Andy",
  "requiresTrigger": true,
  "containerConfig": {
    "additionalMounts": [],
    "timeout": 1800000
  }
}

Task snapshot refresh

When a task mutation succeeds (schedule_task, pause_task, resume_task, cancel_task, or update_task), the host immediately refreshes the current_tasks.json snapshot for all registered groups. This ensures that a subsequent list_tasks call within the same agent session sees the updated task list without waiting for a container restart. The refresh is implemented via an onTasksChanged callback in IpcDeps, keeping ipc.ts decoupled from the container and snapshot layers.

Security model

Identity verification

The source group identity is determined by the directory path, not the file contents. This prevents impersonation attacks. From the IPC watcher in src/ipc.ts:
for (const sourceGroup of groupFolders) {
  const isMain = folderIsMain.get(sourceGroup) === true;
  const messagesDir = path.join(ipcBaseDir, sourceGroup, 'messages');
  const tasksDir = path.join(ipcBaseDir, sourceGroup, 'tasks');
  // Process files with verified identity
}

Mount isolation

Each container can only write to its own IPC namespace:
  • Main group: /workspace/ipcdata/ipc/main/
  • Other groups: /workspace/ipcdata/ipc/{group}/
This is enforced at mount time in src/container-runner.ts:
// Per-group IPC namespace: each group gets its own IPC directory
// This prevents cross-group privilege escalation via IPC
const groupIpcDir = resolveGroupIpcPath(group.folder);
fs.mkdirSync(path.join(groupIpcDir, 'messages'), { recursive: true });
fs.mkdirSync(path.join(groupIpcDir, 'tasks'), { recursive: true });
fs.mkdirSync(path.join(groupIpcDir, 'input'), { recursive: true });
mounts.push({
  hostPath: groupIpcDir,
  containerPath: '/workspace/ipc',
  readonly: false,
});

Error handling

Failed IPC operations are moved to data/ipc/errors/ with a prefix identifying the source group: From src/ipc.ts:
const errorDir = path.join(ipcBaseDir, 'errors');
fs.mkdirSync(errorDir, { recursive: true });
fs.renameSync(
  filePath,
  path.join(errorDir, `${sourceGroup}-${file}`),
);

Debugging IPC issues

ls -la data/ipc/errors/
cat data/ipc/errors/main-message-*.json
Failed operations are moved here with the source group prefix.
grep 'IPC' logs/nanoclaw.log | tail -20
All IPC operations are logged with the source group and operation type.
ls -la data/ipc/main/
ls -la data/ipc/family-chat/
Ensure the host process can read and write these directories.
# Write a test message
echo '{"type":"message","chatJid":"me","text":"test"}' > \
  data/ipc/main/messages/test.json

# Watch logs for processing
tail -f logs/nanoclaw.log | grep IPC

Performance considerations

The host-side IPC poll interval is configurable via IPC_POLL_INTERVAL in src/config.ts (default: 1000ms). The in-container poll interval is 500ms (hardcoded in the agent runner). Lowering the host interval reduces latency but increases CPU usage.
IPC operations are processed sequentially. If processing a single operation takes longer than the poll interval, a backlog can form. Monitor data/ipc/{group}/ directories for accumulating files.
Last modified on March 24, 2026