Skip to main content
NanoClaw’s security model is built on OS-level isolation rather than application-level permission checks. Agents run in isolated Linux containers with explicit filesystem mounts, creating a security boundary that’s enforced by the kernel.

Trust model

NanoClaw operates with different trust levels for different entities:
EntityTrust LevelRationale
Main groupTrustedPrivate self-chat, admin control
Non-main groupsUntrustedOther users may be malicious
Container agentsSandboxedIsolated execution environment
Incoming messagesUser inputPotential prompt injection

Security boundaries

Container isolation (primary boundary)

Agents execute in containers (lightweight Linux VMs), providing:
  • Process isolation - Container processes cannot affect the host
  • Filesystem isolation - Only explicitly mounted directories are visible
  • Non-root execution - Runs as unprivileged node user (uid 1000)
  • Ephemeral containers - Fresh environment per invocation (--rm)
This is the primary security boundary. Rather than relying on application-level permission checks, the attack surface is limited by what’s mounted.
The container runtime is abstracted through src/container-runtime.ts, making it easy to swap runtimes. Docker is the default, but you can switch to Apple Container on macOS for a lighter-weight native runtime.

Mount security

External allowlist Mount permissions are stored at ~/.config/nanoclaw/mount-allowlist.json, which is:
  • Outside the project root
  • Never mounted into containers
  • Cannot be modified by agents
Default blocked patterns:
[
  ".ssh", ".gnupg", ".gpg", ".aws", ".azure", ".gcloud", ".kube", ".docker",
  "credentials", ".env", ".netrc", ".npmrc", ".pypirc", "id_rsa", "id_ed25519",
  "private_key", ".secret"
]
Protections:
  • Symlink resolution before validation (prevents traversal attacks)
  • Container path validation (rejects .. and absolute paths)
  • nonMainReadOnly option forces read-only for non-main groups
See src/mount-security.ts for the validation implementation.
The mount allowlist is cached in memory for the lifetime of the host process. Changes to mount-allowlist.json require a restart to take effect. This is different from the sender allowlist, which is reloaded on every message cycle.
Read-only project root The main group’s project root is mounted read-only. Writable paths the agent needs (group folder, IPC, .claude/) are mounted separately. This prevents the agent from modifying host application code (src/, dist/, package.json, etc.) which would bypass the sandbox entirely on next restart. From src/container-runner.ts:
if (isMain) {
  // Main gets the project root read-only. Writable paths the agent needs
  // (group folder, IPC, .claude/) are mounted separately below.
  // Read-only prevents the agent from modifying host application code
  // (src/, dist/, package.json, etc.) which would bypass the sandbox
  // entirely on next restart.
  mounts.push({
    hostPath: projectRoot,
    containerPath: '/workspace/project',
    readonly: true,
  });
}

Session isolation

Each group has isolated Claude sessions at data/sessions/{group}/.claude/:
  • Groups cannot see other groups’ conversation history
  • Session data includes full message history and file contents read
  • Prevents cross-group information disclosure
Sessions are mounted per-group in src/container-runner.ts:
const groupSessionsDir = path.join(
  DATA_DIR,
  'sessions',
  group.folder,
  '.claude',
);
fs.mkdirSync(groupSessionsDir, { recursive: true });

IPC authorization

Messages and task operations are verified against group identity. The IPC system uses per-group namespaces in data/ipc/{group}/ to prevent privilege escalation.
OperationMain GroupNon-Main Group
Send message to own chat
Send message to other chats
Schedule task for self
Schedule task for others
Update own tasks
Update other groups’ tasks
View all tasksOwn only
Manage other groups
Authorization is enforced in 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);
  logger.info(
    { chatJid: data.chatJid, sourceGroup },
    'IPC message sent',
  );
} else {
  logger.warn(
    { chatJid: data.chatJid, sourceGroup },
    'Unauthorized IPC message attempt blocked',
  );
}

Sender allowlist

The sender allowlist (~/.config/nanoclaw/sender-allowlist.json) filters who can interact with the agent on a per-chat basis. Like the mount allowlist, it lives outside the project root and cannot be modified by agents. File format (SenderAllowlistConfig):
interface ChatAllowlistEntry {
  allow: '*' | string[];  // '*' = everyone, or array of sender JIDs
  mode: 'trigger' | 'drop';
}

interface SenderAllowlistConfig {
  default: ChatAllowlistEntry;
  chats: Record<string, ChatAllowlistEntry>;  // Per-chat overrides, keyed by JID
  logDenied: boolean;  // Default: true
}
How it works:
  1. On every message cycle, the file is read from disk (hot-reloaded, no restart needed)
  2. For each incoming message, the system checks chats[chatJid] first, then falls back to default
  3. If the sender is not in the allow list:
    • trigger mode: the message is stored in the database but cannot activate the agent
    • drop mode: the message is silently discarded before reaching the database
  4. If logDenied is true, denied attempts are logged at debug level
Fallback behavior: if the file is missing, unreadable, or contains invalid JSON, the system defaults to { allow: "*", mode: "trigger" } — all senders permitted. Invalid per-chat entries are skipped with a warning; valid entries are still applied. From src/sender-allowlist.ts:
export function isSenderAllowed(
  chatJid: string,
  sender: string,
  cfg: SenderAllowlistConfig,
): boolean {
  const entry = cfg.chats[chatJid] ?? cfg.default;
  if (entry.allow === '*') return true;
  return entry.allow.includes(sender);
}
The sender allowlist complements container isolation. Containers protect against what an agent can do; the allowlist controls who can invoke an agent. Both are enforced on the host, outside the agent’s reach.

Credential handling

NanoClaw delegates all credential management to the OneCLI gateway. The host process never reads API keys — secrets are registered with OneCLI and injected into container traffic by the gateway.How it works:
  • The @onecli-sh/sdk package’s applyContainerConfig() configures each container’s network to route through the gateway
  • The gateway intercepts HTTPS traffic to api.anthropic.com and injects the registered secret
  • Each non-main group receives an agentIdentifier (derived from its folder name) for per-group credential scoping
  • ONECLI_URL (default http://localhost:10254) configures the gateway address
Container environment (from src/container-runner.ts):
// OneCLI SDK configures container networking — no explicit env vars needed
const onecliApplied = await onecli.applyContainerConfig(args, {
  addHostMapping: false, // NanoClaw already handles host gateway
  agent: agentIdentifier,
});
If the OneCLI gateway is unreachable at container start, the container launches with no credentials. The agent will fail on API calls, and a warning is logged. Re-run after ensuring OneCLI is running (curl http://127.0.0.1:10254/api/health).
NOT mounted:
  • Channel sessions (e.g., store/auth/ for WhatsApp) - host only
  • Mount allowlist - external, never mounted
  • Real API keys or OAuth tokens - injected by secret injection layer, never in containers
  • Any credentials matching blocked patterns

Privilege comparison

CapabilityMain GroupNon-Main Group
Project root access/workspace/project (ro)None
Group folder/workspace/group (rw)/workspace/group (rw)
Global memoryVia project mount/workspace/global (ro, if exists)
Additional mountsConfigurableRead-only unless allowed
Network accessUnrestrictedUnrestricted
MCP toolsAllowlistedAllowlisted

Security architecture diagram

┌──────────────────────────────────────────────────────────────────┐
│                        UNTRUSTED ZONE                             │
│  Incoming Messages (potentially malicious)                         │
└────────────────────────────────┬─────────────────────────────────┘

                                 ▼ Trigger check, input escaping
┌──────────────────────────────────────────────────────────────────┐
│                     HOST PROCESS (TRUSTED)                        │
│  • Message routing                                                │
│  • IPC authorization                                              │
│  • Mount validation (external allowlist)                          │
│  • Container lifecycle                                            │
│  • Secret injection (OneCLI or credential proxy)                  │
└────────────────────────────────┬─────────────────────────────────┘

                                 ▼ Explicit mounts only
┌──────────────────────────────────────────────────────────────────┐
│                CONTAINER (ISOLATED/SANDBOXED)                     │
│  • Agent execution                                                │
│  • Bash commands (sandboxed)                                      │
│  • File operations (limited to mounts)                            │
│  • Network access (unrestricted)                                  │
│  • Cannot modify security config                                  │
└──────────────────────────────────────────────────────────────────┘

Best practices

Check ~/.config/nanoclaw/mount-allowlist.json to ensure only necessary directories are mounted. Remove entries you no longer need.
When mounting directories containing sensitive data, use the readonly option in containerConfig.additionalMounts to prevent modifications.
Never place API keys, passwords, or other secrets in directories that are mounted to non-main groups.
Check groups/{name}/logs/container-*.log files to review what agents are doing. Enable verbose logging with LOG_LEVEL=debug for detailed output. Note that error logs only include input metadata (prompt length and session ID) rather than full prompt content.
Last modified on March 24, 2026