Claude Runners
Three runner containers, one per blog, each with its own agent configs and MCP connections.
Runner Inventory
| Container | Blog | Agent Config | Workspace |
|---|---|---|---|
| runner-cam4 | Cam4 | ./runners/cam4/claude-config/ |
./runners/cam4/workspace/ |
| runner-cam4models | Cam4Models | ./runners/cam4models/claude-config/ |
./runners/cam4models/workspace/ |
| runner-cam4pays | Cam4Pays | ./runners/cam4pays/claude-config/ |
./runners/cam4pays/workspace/ |
Docker Image
Built from ./runners/Dockerfile:
- Base: python:3.11-slim
- Adds: Node.js 20, Claude Code CLI (@anthropic-ai/claude-code), Python (requests, anthropic)
- Runs tail -f /dev/null to stay alive for docker exec
Volume Mounts
Each runner has 3 bind mounts:
volumes:
- ./runners/<slug>/claude-config:/root/.claude # Agent definitions + MCP config
- ./runners/<slug>/.claude.json:/root/.claude.json # Auth token (persists across recreate)
- ./runners/<slug>/workspace:/workspace # Working directory
Critical: The .claude.json bind mount ensures authentication survives container recreation. Without it, runners need re-authentication after every docker compose up -d.
Agent Definitions
Each runner has 6 agents in claude-config/agents/:
| Agent | File | Purpose |
|---|---|---|
| Researcher | wicked-researcher.md |
Finds trending topics via Reddit |
| Blogger | wicked-blogger.md |
Writes the blog post |
| Photographer | wicked-photographer.md |
Generates images via Runware |
| Optimizer | wicked-optimizer.md |
SEO optimization |
| Humanizer | wicked-humanizer.md |
Makes content sound natural |
| Publisher | wicked-publisher.md |
Publishes to Ghost via MCP |
MCP Connections
graph LR
subgraph Runner["Claude Runner"]
agents["6 Agents<br>Researcher, Blogger,<br>Photographer, Optimizer,<br>Humanizer, Publisher"]
end
Runner -->|"blog token"| ghost["ghost-mcp :3002"]
Runner -->|"SSE"| reddit["reddit-mcp :8080"]
Runner -->|"HTTP"| playwright["playwright-mcp :8931"]
Runner -->|"HTTP"| runware["runware-mcp :8081"]
Runner -->|"HTTP"| twitter["twitter-mcp :8081"]
Runner -->|"HTTP"| stockimages["stockimages-mcp :8000"]
Runner -->|"HTTP"| memes["memes-mcp :3000"]
Each runner connects to 7 MCP servers via claude-config/.mcp.json:
{
"mcpServers": {
"ghost": {
"type": "http",
"url": "http://ghost-mcp:3002/<blog-specific-token>"
},
"reddit": {
"type": "sse",
"url": "http://reddit-mcp:8080/sse"
},
"playwright": {
"type": "http",
"url": "http://playwright-mcp:8931/mcp"
},
"runware": {
"type": "http",
"url": "http://runware-mcp:8081/mcp"
},
"twitter": {
"type": "http",
"url": "http://twitter-mcp:8081/mcp"
},
"stockimages": {
"type": "http",
"url": "http://stockimages-mcp:8000/mcp"
},
"memes": {
"type": "http",
"url": "http://memes-mcp:3000/mcp"
}
}
}
Note: The Ghost token is unique per runner — it routes to the correct Ghost instance.
Brand Guidelines
Each runner has brand-specific configuration:
runners/<slug>/claude-config/config/brand-guidelines.json # Blog voice, audience, topics
runners/<slug>/claude-config/config/runware-models.json # Image generation model prefs
Authentication Procedure
After first build or if .claude.json is lost:
# Interactive login
docker exec -it runner-cam4 claude /login
# Follow the OAuth flow in your browser
# The token is saved to /root/.claude.json (bind-mounted for persistence)
# Verify
docker exec runner-cam4 claude --version
Common Operations
cd /opt/camlab
# Check if a runner is alive
docker exec runner-cam4 echo "alive"
# Check Claude Code version
docker exec runner-cam4 claude --version
# View runner logs
docker compose logs --tail 50 runner-cam4
# Restart a runner
docker compose restart runner-cam4
# Run a command in runner
docker exec runner-cam4 claude --agent wicked-researcher "Find trending topics"