Auto-continue was punishing the model for thinking — text-only turns were treated as failures and re-prompted up to 5 times with aggressive "execute now" messages. This made Codex overly compliant and suppressed critical thinking. Now each turn result is delivered directly. The model decides whether to think, push back, or execute.
Dual-agent AI assistant running Claude Code + Codex as parallel services over Discord.
Based on qwibitai/nanoclaw
Overview
Two AI agents running as parallel systemd services, communicating over Discord:
- Claude Code — powered by Claude Agent SDK, trigger
@claude - Codex — powered by Codex app-server (JSON-RPC), trigger
@codex
Each agent has its own store, data, and groups directories. Discord channels can be registered with either agent, or both (both agent type for shared channels).
Key Features
- Direct host processes — no container overhead, agents run natively
- Bidirectional image support — receive images as multimodal input, send as Discord attachments
- Skill sync — single source of truth (
~/.claude/skills/), auto-synced to all sessions - OAuth auto-refresh — token lifecycle managed automatically for headless environments
- Priority queue — per-group serialization, global concurrency limit, idle preemption
- Auto-continue — Codex text-only turns automatically retried to enforce task execution
Architecture
Discord ──► SQLite ──► GroupQueue ──┬──► Claude Agent SDK (host process)
└──► Codex App-Server (JSON-RPC stdio)
├── thread/start, thread/resume
├── turn/start (streaming, multimodal)
├── turn/steer (mid-turn injection)
├── Auto-approval (bypass sandbox)
└── Auto-continue (text-only turn retry)
Directory Layout
nanoclaw/
├── src/
│ ├── index.ts # Orchestrator: state, message loop, agent invocation
│ ├── agent-runner.ts # Spawns agent processes, manages env/sessions/skills
│ ├── group-queue.ts # Per-group concurrency, priority queue, idle preemption
│ ├── group-folder.ts # Group directory resolution and management
│ ├── router.ts # Outbound message formatting and routing
│ ├── sender-allowlist.ts # Security: sender-based access control
│ ├── session-commands.ts # Session commands (/compact)
│ ├── token-refresh.ts # OAuth auto-refresh + session directory sync
│ ├── task-scheduler.ts # Scheduled tasks (cron/interval/once)
│ ├── ipc.ts # IPC watcher and task processing
│ ├── db.ts # SQLite operations
│ ├── config.ts # Paths, intervals, trigger patterns
│ └── channels/
│ ├── registry.ts # Channel self-registration system
│ └── discord.ts # Discord: mentions, images, typing, file attachments
├── container/
│ ├── agent-runner/ # Claude Code runner (Agent SDK, multimodal input)
│ ├── codex-runner/ # Codex runner (app-server JSON-RPC, auto-continue)
│ └── skills/ # Shared agent skills (browser, etc.)
├── store/ # Claude Code service DB
├── store-codex/ # Codex service DB
├── data/
│ ├── sessions/ # Per-group Claude sessions (.claude/)
│ └── attachments/ # Downloaded Discord image attachments
├── data-codex/sessions/ # Per-group Codex sessions (.codex/)
├── groups/ # Per-group memory and workspace (Claude Code)
├── groups-codex/ # Per-group memory and workspace (Codex)
└── logs/ # Service logs
Codex App-Server Integration
The Codex runner (container/codex-runner/) communicates with codex app-server via JSON-RPC over stdio:
- Session persistence: Thread IDs stored in DB, sessions saved as JSONL on disk
- Streaming:
item/agentMessage/deltanotifications for real-time text - Mid-turn steering: IPC messages injected via
turn/steerduring execution - Auto-approval:
approvalPolicy: "never"+sandbox: "danger-full-access" - Auto-continue: Detects text-only turns (no tool execution) and automatically retries up to 5 times to nudge the agent into actually executing tasks
- Multimodal input: Image attachments converted to
localImageinput blocks inturn/start - Per-group config: Model, effort, MCP servers configured per channel
Image Handling
Bidirectional image support through Discord:
- Receiving (user → agent): Discord image attachments are downloaded to
data/attachments/, then passed as base64ImageBlockParamcontent blocks (Claude Code) orlocalImageinput blocks (Codex) - Sending (agent → user): Markdown image links
[name.png](/path)in agent responses are automatically parsed and sent as Discord file attachments. Non-image file links are converted to readable filenames (BuildPanel.tsx:320)
Skill Sync
Skills are managed from a single source of truth (~/.claude/skills/ on the server) and automatically synced to all agent session directories at process start:
- Claude Code sessions:
~/.claude/skills/+ projectcontainer/skills/ - Codex sessions: Same sources, synced to per-group
.codex/directories - Skills auto-register as slash commands (
/name) in Claude Code and$namein Codex
OAuth Token Auto-Refresh
src/token-refresh.ts handles Claude Code OAuth token lifecycle:
- Checks every 5 minutes, refreshes 30 minutes before expiry
- Tries
platform.claude.comthen falls back toapi.anthropic.com - Syncs refreshed credentials to all per-group session directories
- Solves the known headless environment token expiry issue
GroupQueue
src/group-queue.ts manages agent execution with:
- Per-group serialization: Only one agent process per group at a time
- Global concurrency limit: Configurable max concurrent agents across all groups
- Task priority: Scheduled tasks drain before message processing
- Idle preemption: Idle agents are terminated when higher-priority tasks arrive
- Exponential backoff: Retries with backoff on processing failure
Setup
Prerequisites
- Linux (Ubuntu 22.04+) or macOS
- Node.js 20+
- Claude Code CLI
- Codex CLI (
npm install -g @openai/codex) - Two Discord bot tokens (one per agent) — create at Discord Developer Portal
1. Clone and Install
git clone https://github.com/phj1081/nanoclaw.git
cd nanoclaw
npm install
npm run build:runners # installs + builds both agent-runner and codex-runner
npm run build # builds main project
2. Authenticate CLIs
# Claude Code — opens browser for OAuth login
claude login
# Codex — set API key
export OPENAI_API_KEY=sk-...
3. Environment Variables
Create .env in the project root:
# .env — shared config (read by both services)
DISCORD_BOT_TOKEN= # Claude Code Discord bot token
ASSISTANT_NAME=claude # Bot trigger name (@claude)
ANTHROPIC_API_KEY= # Or use OAuth (claude login)
OPENAI_API_KEY= # For Codex
For dual-service setup, create .env.codex for Codex-specific overrides:
# .env.codex — Codex service secrets (loaded via systemd EnvironmentFile)
DISCORD_BOT_TOKEN= # Codex Discord bot token (different from above)
Security: Never put tokens in systemd service files or commit them to git. Use
.envfiles with restricted permissions (chmod 600).
4. Systemd Services (Linux)
Create ~/.config/systemd/user/nanoclaw.service:
[Unit]
Description=NanoClaw Claude Code
After=network.target
[Service]
Type=simple
ExecStart=/path/to/node /path/to/nanoclaw/dist/index.js
WorkingDirectory=/path/to/nanoclaw
Restart=always
RestartSec=5
Environment=HOME=/home/youruser
Environment=PATH=/path/to/node/bin:/usr/local/bin:/usr/bin:/bin
[Install]
WantedBy=default.target
Create ~/.config/systemd/user/nanoclaw-codex.service:
[Unit]
Description=NanoClaw Codex
After=network.target
[Service]
EnvironmentFile=/path/to/nanoclaw/.env.codex
Type=simple
ExecStart=/path/to/node /path/to/nanoclaw/dist/index.js
WorkingDirectory=/path/to/nanoclaw
Restart=always
RestartSec=5
Environment=HOME=/home/youruser
Environment=PATH=/path/to/node/bin:/usr/local/bin:/usr/bin:/bin
Environment=ASSISTANT_NAME=codex
Environment=NANOCLAW_STORE_DIR=/path/to/nanoclaw/store-codex
Environment=NANOCLAW_DATA_DIR=/path/to/nanoclaw/data-codex
Environment=NANOCLAW_GROUPS_DIR=/path/to/nanoclaw/groups-codex
[Install]
WantedBy=default.target
Then enable and start:
systemctl --user daemon-reload
systemctl --user enable nanoclaw nanoclaw-codex
systemctl --user start nanoclaw nanoclaw-codex
# Logs
journalctl --user -u nanoclaw -f
journalctl --user -u nanoclaw-codex -f
5. Register Discord Channels
Channels are stored in each service's SQLite database (registered_groups table). Use the IPC auth endpoint or insert directly:
# Example: register a channel for Claude Code
sqlite3 store/nanoclaw.db "INSERT INTO registered_groups (jid, name, folder, agent_type, trigger_pattern) VALUES ('dc:CHANNEL_ID', 'my-channel', 'my-channel', 'claude-code', '@claude');"
# Example: register a channel for Codex
sqlite3 store-codex/nanoclaw.db "INSERT INTO registered_groups (jid, name, folder, agent_type, trigger_pattern) VALUES ('dc:CHANNEL_ID', 'my-channel', 'my-channel', 'codex', '@codex');"
Fields:
| Field | Description |
|---|---|
jid |
dc:<discord_channel_id> |
name |
Display name |
folder |
Group folder name (workspace directory) |
agent_type |
claude-code, codex, or both |
trigger_pattern |
Regex for activation (e.g., @claude) |
work_dir |
Optional working directory override |
container_config |
Optional JSON (e.g., {"codexEffort":"high"}) |
macOS (launchd)
launchctl load ~/Library/LaunchAgents/com.nanoclaw.plist
launchctl load ~/Library/LaunchAgents/com.nanoclaw-codex.plist
launchctl kickstart -k gui/$(id -u)/com.nanoclaw
Development
npm install # Install dependencies
npm run build # Build main project
npm run build:runners # Install + build both runners
npm run dev # Dev mode with hot reload
npm test # Run tests
License
MIT — Based on qwibitai/nanoclaw
