NanoClaw Container Debugging
This guide covers debugging the containerized agent execution system.
Architecture Overview
Host (macOS) Container (Linux VM)
─────────────────────────────────────────────────────────────
src/container-runner.ts container/agent-runner/
│ │
│ spawns container │ runs Claude Agent SDK
│ with volume mounts │ with MCP servers
│ │
├── data/env/env ──────────────> /workspace/env-dir/env
├── groups/{folder} ───────────> /workspace/group
├── data/ipc/{folder} ────────> /workspace/ipc
├── data/sessions/{folder}/.claude/ ──> /home/node/.claude/ (isolated per-group)
└── (main only) project root ──> /workspace/project
Important: The container runs as user node with HOME=/home/node. Session files must be mounted to /home/node/.claude/ (not /root/.claude/) for session resumption to work.
Log Locations
| Log | Location | Content |
|---|---|---|
| Main app logs | logs/nanoclaw.log | Host-side WhatsApp, routing, container spawning |
| Main app errors | logs/nanoclaw.error.log | Host-side errors |
| Container run logs | groups/{folder}/logs/container-*.log | Per-run: input, mounts, stderr, stdout |
| Claude sessions | ~/.claude/projects/ | Claude Code session history |
Enabling Debug Logging
Set LOG_LEVEL=debug for verbose output:
# For development
LOG_LEVEL=debug pnpm run dev
# For launchd service (macOS), add to plist EnvironmentVariables:
<key>LOG_LEVEL</key>
<string>debug</string>
# For systemd service (Linux), add to unit [Service] section:
# Environment=LOG_LEVEL=debug
Debug level shows:
- Full mount configurations
- Container command arguments
- Real-time container stderr
Common Issues
1. "No adapter for channel type" / Messages silently lost (null platformMsgId)
Symptom: The bot stops replying. logs/nanoclaw.error.log shows repeated:
WARN No adapter for channel type channelType="telegram"
WARN No adapter for channel type channelType="signal"
The main log shows "Message delivered" entries with platformMsgId=undefined — meaning the delivery poll ran, found no adapter, and permanently marked the message as delivered without sending it.
Root cause: two NanoClaw service instances running simultaneously.
When a second service instance (often nanoclaw-v2-<id>.service running alongside nanoclaw.service) is active with a stale binary, it has no channel adapters registered. Its delivery poll races against the working instance and wins — permanently marking outbound messages as delivered without ever sending them.
Diagnosis:
# Check for duplicate running instances
ps aux | grep 'nanoclaw/dist/index.js' | grep -v grep
# Check which services are active
systemctl --user list-units 'nanoclaw*' --all
# Confirm channel adapters registered by the current process
grep "Channel adapter started" logs/nanoclaw.log | tail -10
Fix:
- Identify which service has the correct binary and EnvironmentFile (the one showing
signal,telegram,cliall started in the log). - Stop and disable the stale duplicate service:
systemctl --user stop nanoclaw.service # or whichever is the old one systemctl --user disable nanoclaw.service - If the remaining service unit is missing
EnvironmentFile, add it:# Edit the service unit — add this line under [Service]: # EnvironmentFile=/home/[user]/nanoclaw/.env systemctl --user daemon-reload systemctl --user restart nanoclaw-v2-<id>.service - Verify only one instance runs:
ps aux | grep nanoclaw/dist/index.js | grep -v grep
Note: Messages that were marked delivered with a null platform_message_id cannot be automatically retried — they are permanently lost. The user must resend their message.
2. "Claude Code process exited with code 1"
Check the container log file in groups/{folder}/logs/container-*.log
Common causes:
Missing Authentication
Invalid API key · Please run /login
Fix: Ensure .env file exists with either OAuth token or API key:
cat .env # Should show one of:
# CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-... (subscription)
# ANTHROPIC_API_KEY=sk-ant-api03-... (pay-per-use)
Root User Restriction
--dangerously-skip-permissions cannot be used with root/sudo privileges
Fix: Container must run as non-root user. Check Dockerfile has USER node.
2. Environment Variables Not Passing
Runtime note: Environment variables passed via -e may be lost when using -i (interactive/piped stdin).
Workaround: The system extracts only authentication variables (CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY) from .env and mounts them for sourcing inside the container. Other env vars are not exposed.
To verify env vars are reaching the container:
echo '{}' | docker run -i \
-v $(pwd)/data/env:/workspace/env-dir:ro \
--entrypoint /bin/bash nanoclaw-agent:latest \
-c 'export $(cat /workspace/env-dir/env | xargs); echo "OAuth: ${#CLAUDE_CODE_OAUTH_TOKEN} chars, API: ${#ANTHROPIC_API_KEY} chars"'
3. Mount Issues
Container mount notes:
- Docker supports both
-vand--mountsyntax - Use
:rosuffix for readonly mounts:# Readonly -v /path:/container/path:ro # Read-write -v /path:/container/path
To check what's mounted inside a container:
docker run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c 'ls -la /workspace/'
Expected structure:
/workspace/
├── env-dir/env # Environment file (CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY)
├── group/ # Current group folder (cwd)
├── project/ # Project root (main channel only)
├── global/ # Global CLAUDE.md (non-main only)
├── ipc/ # Inter-process communication
│ ├── messages/ # Outgoing WhatsApp messages
│ ├── tasks/ # Scheduled task commands
│ ├── current_tasks.json # Read-only: scheduled tasks visible to this group
│ └── available_groups.json # Read-only: WhatsApp groups for activation (main only)
└── extra/ # Additional custom mounts
4. Permission Issues
The container runs as user node (uid 1000). Check ownership:
docker run --rm --entrypoint /bin/bash nanoclaw-agent:latest -c '
whoami
ls -la /workspace/
ls -la /app/
'
All of /workspace/ and /app/ should be owned by node.
5. Session Not Resuming / "Claude Code process exited with code 1"
If sessions aren't being resumed (new session ID every time), or Claude Code exits with code 1 when resuming:
Root cause: The SDK looks for sessions at $HOME/.claude/projects/. Inside the container, HOME=/home/node, so it looks at /home/node/.claude/projects/.
Check the mount path:
# In container-runner.ts, verify mount is to /home/node/.claude/, NOT /root/.claude/
grep -A3 "Claude sessions" src/container-runner.ts
Verify sessions are accessible:
docker run --rm --entrypoint /bin/bash \
-v ~/.claude:/home/node/.claude \
nanoclaw-agent:latest -c '
echo "HOME=$HOME"
ls -la $HOME/.claude/projects/ 2>&1 | head -5
'
Fix: Ensure container-runner.ts mounts to /home/node/.claude/:
mounts.push({
hostPath: claudeDir,
containerPath: '/home/node/.claude', // NOT /root/.claude
readonly: false
});
6. MCP Server Failures
If an MCP server fails to start, the agent may exit. Check the container logs for MCP initialization errors.
Manual Container Testing
Test the full agent flow:
# Set up env file
mkdir -p data/env groups/test
cp .env data/env/env
# Run test query
echo '{"prompt":"What is 2+2?","groupFolder":"test","chatJid":"test@g.us","isMain":false}' | \