rdev/docs/reference.md
jordan 179b6521ca docs: Add v0.1.0 history and update reference with k3s notes
- Created history/v0.1.0.md with full deployment notes
- Added k3s implementation section to reference.md
- Fixed auth command: `claude` not `claude /login`
- Documented issues encountered and solutions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-24 20:09:03 -07:00

42 KiB

Multi-monorepo Claude Code infrastructure: Complete reference guide

This guide covers deploying claudebox across multiple monorepos with Discord control, using Claude Pro or Team subscription authentication. This architecture provides complete project isolation, parallel AI development, and team-based access control without API keys.

Architecture overview

Pattern: Multi-claudebox with single-bot routing

This architecture runs separate Docker containers for each monorepo, managed by a single Discord bot that routes commands based on channel context. Each container maintains independent dependencies, git state, network policies, and Claude authentication sessions.

Physical Layout:
├── Remote VM (Ubuntu 22.04+)
│   ├── Docker Container: claudebox-project-a
│   ├── Docker Container: claudebox-project-b
│   └── Docker Container: claudebox-project-c
│   └── Discord Bot Process (routes to containers)
│
Discord Server:
├── #project-a-dev → claudebox-project-a
├── #project-b-dev → claudebox-project-b
└── #project-c-dev → claudebox-project-c

Why this pattern:

  • Dependency isolation (Project A: Node 18, Project B: Node 20, Project C: Python 3.11)
  • Parallel execution (Claude works on all projects simultaneously)
  • Security boundaries (network policies and file access per project)
  • Resource allocation (CPU/memory limits per project)
  • Independent git state (no cross-contamination)

Subscription requirements and authentication

Required subscriptions:

You need ONE of the following:

  • Claude Pro ($20/month per developer) - individual use
  • Claude Team ($30/month per developer, 5 minimum) - team collaboration with centralized billing
  • Claude Enterprise - contact Anthropic sales for multi-team deployments

Authentication model:

Claude Code authenticates via OAuth to your claude.ai account. Each developer on your team needs their own subscription, and each will authenticate their claudebox instances with their personal credentials. This is fundamentally different from API-based usage—you're using the web subscription's usage limits, not paying per token.

Per-developer or shared authentication:

You have two deployment options:

  1. Per-developer claudebox (Recommended for teams):

    • Each developer has their own VM with their own claudebox instances
    • Each authenticates with their personal Claude subscription
    • Usage tracked per developer
    • No credential sharing
  2. Shared claudebox with team account (Single shared development server):

    • One VM running all claudebox instances
    • Authenticate with a shared Claude Team account
    • All developers use same Discord bot
    • Usage pooled across team

This guide assumes shared claudebox for simplicity, but the architecture works for both models.


Initial VM setup and prerequisites

Provision your remote VM:

Minimum specifications:

  • CPU: 8 cores (for 3 parallel containers)
  • RAM: 16GB (allocate 4GB per container minimum)
  • Storage: 100GB SSD (monorepos + Docker images + build artifacts)
  • OS: Ubuntu 22.04 LTS or Debian 12

Cloud provider recommendations:

  • AWS: t3.2xlarge or c6i.2xlarge
  • GCP: n2-standard-8
  • Azure: Standard_D8s_v3
  • DigitalOcean: CPU-Optimized 8GB/4vCPU droplet

Install system dependencies:

# Update system
sudo apt update && sudo apt upgrade -y

# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
newgrp docker

# Install Node.js (for Claude Code CLI)
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Install Deno (for Discord bot)
curl -fsSL https://deno.land/install.sh | sh
echo 'export DENO_INSTALL="$HOME/.deno"' >> ~/.bashrc
echo 'export PATH="$DENO_INSTALL/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# Install Git and development tools
sudo apt install -y git build-essential curl wget vim

# Install Docker Compose
sudo apt install -y docker-compose-plugin

Clone your monorepos:

mkdir -p ~/projects
cd ~/projects

# Clone your monorepos
git clone https://github.com/yourorg/monorepo-a.git
git clone https://github.com/yourorg/monorepo-b.git
git clone https://github.com/yourorg/monorepo-c.git

Claude Code CLI installation and authentication

Install Claude Code globally:

npm install -g @anthropic-ai/claude-code

Authenticate with your subscription:

claude /login

This opens a browser window where you'll sign in with your Claude Pro/Team account. The authentication token is saved to ~/.claude/.credentials.json and will be mounted into each container.

Verify authentication:

claude --version
ls -la ~/.claude/.credentials.json

The credentials file should exist and contain your authentication state. This single authentication covers all three claudebox instances since they'll mount the same credentials directory.

Important: Credential persistence

Your authentication lasts approximately 30 days. When it expires:

  1. Run claude /login again on the host VM
  2. Restart all claudebox containers to pick up new credentials
  3. No need to re-authenticate inside containers

Installing and configuring claudebox

Download and install claudebox:

cd ~
wget https://github.com/RchGrav/claudebox/releases/latest/download/claudebox.run
chmod +x claudebox.run
./claudebox.run

# Add to PATH
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# Verify installation
claudebox info

Initialize claudebox for each project:

# Project A
cd ~/projects/monorepo-a
claudebox init project-a

# Project B
cd ~/projects/monorepo-b
claudebox init project-b

# Project C
cd ~/projects/monorepo-c
claudebox init project-c

This creates isolated configuration directories:

~/.claudebox/
├── project-a/
│   ├── .claude/          # Will mount host credentials
│   ├── firewall/         # Network allowlist
│   └── workspace/        # Symlink to ~/projects/monorepo-a
├── project-b/
│   └── ...
└── project-c/
    └── ...

Install profiles for each project:

Profiles define the development environment for each container.

# Project A: JavaScript/TypeScript monorepo with Turborepo
claudebox profile javascript typescript
claudebox profile project-a --install

# Project B: Python monorepo with Poetry
claudebox profile python ml
claudebox profile project-b --install

# Project C: Rust monorepo
claudebox profile rust
claudebox profile project-c --install

Configure project-specific profiles:

Create custom profile files for monorepo tooling:

# ~/.claudebox/profiles/project-a.ini
[base]
name = JavaScript Monorepo (Turborepo)
extends = javascript,typescript

[packages]
# System packages
apt = build-essential python3 git

# Node.js tools
npm = turbo nx pnpm typescript eslint prettier

[environment]
NODE_VERSION = 20
PNPM_HOME = /root/.local/share/pnpm
PATH = $PNPM_HOME:$PATH

[startup]
# Install dependencies on container start
commands = 
  cd /workspace && pnpm install
  turbo daemon start

[firewall]
# Allow access to package registries
allow = registry.npmjs.org,github.com,*.github.com

[resource_limits]
cpus = 2.0
memory = 4g
# ~/.claudebox/profiles/project-b.ini
[base]
name = Python Monorepo (Poetry)
extends = python,ml

[packages]
apt = python3.11 python3-pip python3-venv
pip = poetry pytest black mypy pandas numpy torch

[environment]
PYTHON_VERSION = 3.11
POETRY_VIRTUALENVS_IN_PROJECT = true

[startup]
commands =
  cd /workspace && poetry install
  poetry run python --version

[firewall]
allow = pypi.org,files.pythonhosted.org,github.com

[resource_limits]
cpus = 4.0
memory = 8g
# ~/.claudebox/profiles/project-c.ini
[base]
name = Rust Monorepo
extends = rust

[packages]
apt = build-essential pkg-config libssl-dev
cargo = cargo-workspaces cargo-watch cargo-edit

[environment]
RUST_VERSION = stable
CARGO_HOME = /usr/local/cargo

[startup]
commands =
  cd /workspace && cargo fetch
  cargo build --workspace --release

[firewall]
allow = crates.io,static.crates.io,github.com

[resource_limits]
cpus = 3.0
memory = 6g

Docker compose orchestration

Create a centralized orchestration file for all containers.

Create infrastructure directory:

mkdir -p ~/claude-infra
cd ~/claude-infra

Create docker-compose.yml:

# ~/claude-infra/docker-compose.yml
version: '3.8'

services:
  claudebox-project-a:
    image: ghcr.io/rchgrav/claudebox:latest
    container_name: claudebox-project-a
    hostname: project-a-dev
    
    volumes:
      # Mount monorepo
      - ~/projects/monorepo-a:/workspace:rw
      
      # Mount shared Claude credentials (READ-ONLY)
      - ~/.claude:/root/.claude:ro
      
      # Mount project-specific config
      - ~/.claudebox/project-a/firewall:/etc/claudebox/firewall:rw
      
      # Shared caches for faster builds
      - npm-cache:/root/.npm
      - pnpm-store:/root/.local/share/pnpm/store
    
    environment:
      - PROJECT_NAME=project-a
      - WORKSPACE=/workspace
      - CLAUDEBOX_PROFILE=project-a
    
    working_dir: /workspace
    
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 4G
        reservations:
          cpus: '1.0'
          memory: 2G
    
    networks:
      - claude-network-a
    
    restart: unless-stopped
    
    # Keep container running
    command: tail -f /dev/null

  claudebox-project-b:
    image: ghcr.io/rchgrav/claudebox:latest
    container_name: claudebox-project-b
    hostname: project-b-dev
    
    volumes:
      - ~/projects/monorepo-b:/workspace:rw
      - ~/.claude:/root/.claude:ro
      - ~/.claudebox/project-b/firewall:/etc/claudebox/firewall:rw
      - pip-cache:/root/.cache/pip
      - poetry-cache:/root/.cache/pypoetry
    
    environment:
      - PROJECT_NAME=project-b
      - WORKSPACE=/workspace
      - CLAUDEBOX_PROFILE=project-b
      - PYTHON_VERSION=3.11
    
    working_dir: /workspace
    
    deploy:
      resources:
        limits:
          cpus: '4.0'
          memory: 8G
        reservations:
          cpus: '2.0'
          memory: 4G
    
    networks:
      - claude-network-b
    
    restart: unless-stopped
    command: tail -f /dev/null

  claudebox-project-c:
    image: ghcr.io/rchgrav/claudebox:latest
    container_name: claudebox-project-c
    hostname: project-c-dev
    
    volumes:
      - ~/projects/monorepo-c:/workspace:rw
      - ~/.claude:/root/.claude:ro
      - ~/.claudebox/project-c/firewall:/etc/claudebox/firewall:rw
      - cargo-registry:/usr/local/cargo/registry
      - cargo-git:/usr/local/cargo/git
    
    environment:
      - PROJECT_NAME=project-c
      - WORKSPACE=/workspace
      - CLAUDEBOX_PROFILE=project-c
      - RUST_VERSION=stable
    
    working_dir: /workspace
    
    deploy:
      resources:
        limits:
          cpus: '3.0'
          memory: 6G
        reservations:
          cpus: '1.5'
          memory: 3G
    
    networks:
      - claude-network-c
    
    restart: unless-stopped
    command: tail -f /dev/null

networks:
  claude-network-a:
    driver: bridge
  claude-network-b:
    driver: bridge
  claude-network-c:
    driver: bridge

volumes:
  npm-cache:
  pnpm-store:
  pip-cache:
  poetry-cache:
  cargo-registry:
  cargo-git:

Container lifecycle management:

# Start all containers
cd ~/claude-infra
docker compose up -d

# View status
docker compose ps

# View logs
docker compose logs -f claudebox-project-a

# Stop all containers
docker compose stop

# Restart specific container
docker compose restart claudebox-project-b

# Remove all containers (keeps volumes)
docker compose down

# Full cleanup including volumes
docker compose down -v

Discord bot setup and configuration

Create Discord application:

  1. Go to https://discord.com/developers/applications
  2. Click New Application, name it "Claude-MultiProject"
  3. Navigate to Bot section
  4. Click Add Bot
  5. Enable Message Content Intent
  6. Copy the Bot Token (save securely)
  7. Copy the Application ID from General Information

Invite bot to your server:

  1. Go to OAuth2URL Generator
  2. Select scopes: bot, applications.commands
  3. Select permissions:
    • Send Messages
    • Use Slash Commands
    • Read Message History
    • Embed Links
    • Attach Files
    • Manage Messages (for cleanup)
  4. Copy generated URL and open in browser
  5. Select your server and authorize

Clone and configure the Discord bot:

cd ~/claude-infra
git clone https://github.com/zebbern/claude-code-discord.git discord-bot
cd discord-bot

Create environment configuration:

# ~/claude-infra/discord-bot/.env
DISCORD_TOKEN=your_bot_token_here
APPLICATION_ID=your_application_id_here

# Project routing configuration
PROJECT_A_CONTAINER=claudebox-project-a
PROJECT_B_CONTAINER=claudebox-project-b
PROJECT_C_CONTAINER=claudebox-project-c

# Channel mapping (will configure in code)
# This is just documentation
CHANNEL_PROJECT_A=project-a-dev
CHANNEL_PROJECT_B=project-b-dev
CHANNEL_PROJECT_C=project-c-dev

Create enhanced routing configuration:

// ~/claude-infra/discord-bot/config.ts
export interface ProjectConfig {
  container: string;
  workdir: string;
  profile: string;
  allowedRoles: string[];
  description: string;
  monorepo: {
    tool: 'turbo' | 'nx' | 'poetry' | 'cargo';
    packages: string[];
  };
}

export const PROJECT_CONFIGS: Record<string, ProjectConfig> = {
  'project-a-dev': {
    container: 'claudebox-project-a',
    workdir: '/workspace',
    profile: 'javascript typescript',
    allowedRoles: ['project-a-devs', 'admin', 'engineering'],
    description: 'JavaScript/TypeScript monorepo with Turborepo',
    monorepo: {
      tool: 'turbo',
      packages: ['@myorg/api', '@myorg/web', '@myorg/shared']
    }
  },
  'project-b-dev': {
    container: 'claudebox-project-b',
    workdir: '/workspace',
    profile: 'python ml',
    allowedRoles: ['project-b-devs', 'admin', 'data-science'],
    description: 'Python monorepo with Poetry for ML workflows',
    monorepo: {
      tool: 'poetry',
      packages: ['ml-pipeline', 'data-processing', 'inference-service']
    }
  },
  'project-c-dev': {
    container: 'claudebox-project-c',
    workdir: '/workspace',
    profile: 'rust',
    allowedRoles: ['project-c-devs', 'admin', 'systems'],
    description: 'Rust monorepo with Cargo workspaces',
    monorepo: {
      tool: 'cargo',
      packages: ['core', 'cli', 'server']
    }
  }
};

// Monorepo context templates
export const MONOREPO_CONTEXT = {
  turbo: `This is a Turborepo monorepo. When making changes:
- Use 'turbo run <task>' for builds/tests
- Changes may affect multiple packages
- Check package.json workspaces for dependencies
- Use 'turbo run build --filter=<package>' for specific packages`,
  
  nx: `This is an Nx monorepo. When making changes:
- Use 'nx run <project>:<target>' for tasks
- Check nx.json for task configuration
- Use 'nx affected:build' for changed packages`,
  
  poetry: `This is a Poetry monorepo. When making changes:
- Use 'poetry run <command>' for scripts
- Update pyproject.toml for dependencies
- Run 'poetry install' after dependency changes`,
  
  cargo: `This is a Cargo workspace. When making changes:
- Use 'cargo build --workspace' for all packages
- Update Cargo.toml in workspace root
- Use 'cargo build -p <package>' for specific crates`
};

Create enhanced bot with routing:

// ~/claude-infra/discord-bot/bot.ts
import { Client, GatewayIntentBits, REST, Routes, SlashCommandBuilder } from 'discord.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import { PROJECT_CONFIGS, MONOREPO_CONTEXT } from './config.ts';

const execAsync = promisify(exec);

const client = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

// Helper: Check user permissions
function hasPermission(member: any, allowedRoles: string[]): boolean {
  return member.roles.cache.some((role: any) => 
    allowedRoles.includes(role.name.toLowerCase())
  );
}

// Helper: Execute command in container
async function execInContainer(
  container: string,
  command: string,
  cwd: string = '/workspace'
): Promise<string> {
  const dockerCmd = `docker exec -w ${cwd} ${container} bash -c "${command.replace(/"/g, '\\"')}"`;
  
  try {
    const { stdout, stderr } = await execAsync(dockerCmd, {
      maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large outputs
      timeout: 300000 // 5 minute timeout
    });
    return stdout || stderr;
  } catch (error: any) {
    throw new Error(`Container execution failed: ${error.message}`);
  }
}

// Helper: Get project config from channel
function getProjectConfig(channelName: string) {
  const config = PROJECT_CONFIGS[channelName];
  if (!config) {
    throw new Error(`No configuration found for channel: ${channelName}`);
  }
  return config;
}

// Command: /claude
const claudeCommand = new SlashCommandBuilder()
  .setName('claude')
  .setDescription('Send a prompt to Claude Code')
  .addStringOption(option =>
    option.setName('prompt')
      .setDescription('Your prompt for Claude')
      .setRequired(true)
  )
  .addStringOption(option =>
    option.setName('mode')
      .setDescription('Execution mode')
      .addChoices(
        { name: 'normal', value: 'normal' },
        { name: 'auto-accept', value: 'auto' },
        { name: 'plan-only', value: 'plan' }
      )
  );

// Command: /status
const statusCommand = new SlashCommandBuilder()
  .setName('status')
  .setDescription('Check container and project status');

// Command: /shell
const shellCommand = new SlashCommandBuilder()
  .setName('shell')
  .setDescription('Execute shell command in project container')
  .addStringOption(option =>
    option.setName('command')
      .setDescription('Shell command to execute')
      .setRequired(true)
  );

// Command: /git
const gitCommand = new SlashCommandBuilder()
  .setName('git')
  .setDescription('Execute git command')
  .addStringOption(option =>
    option.setName('args')
      .setDescription('Git arguments (e.g., "status", "diff HEAD")')
      .setRequired(true)
  );

// Command: /monorepo-info
const monorepoInfoCommand = new SlashCommandBuilder()
  .setName('monorepo-info')
  .setDescription('Show monorepo structure and packages');

// Register commands
const commands = [
  claudeCommand,
  statusCommand,
  shellCommand,
  gitCommand,
  monorepoInfoCommand,
].map(cmd => cmd.toJSON());

// Handle interactions
client.on('interactionCreate', async interaction => {
  if (!interaction.isCommand()) return;
  
  try {
    const channelName = interaction.channel?.name || '';
    const config = getProjectConfig(channelName);
    
    // Check permissions
    if (!hasPermission(interaction.member, config.allowedRoles)) {
      await interaction.reply({
        content: `⛔ You don't have permission to use Claude Code on ${config.description}. Required roles: ${config.allowedRoles.join(', ')}`,
        ephemeral: true
      });
      return;
    }
    
    await interaction.deferReply();
    
    switch (interaction.commandName) {
      case 'claude': {
        const prompt = interaction.options.getString('prompt', true);
        const mode = interaction.options.getString('mode') || 'normal';
        
        // Add monorepo context to prompt
        const monorepContext = MONOREPO_CONTEXT[config.monorepo.tool];
        const enhancedPrompt = `${monorepContext}\n\nUser request: ${prompt}`;
        
        const modeFlag = mode === 'auto' ? '--dangerously-skip-permissions' : 
                        mode === 'plan' ? '--plan-only' : '';
        
        const output = await execInContainer(
          config.container,
          `claude "${enhancedPrompt}" ${modeFlag}`,
          config.workdir
        );
        
        // Split long responses
        const chunks = output.match(/[\s\S]{1,1900}/g) || [];
        for (const chunk of chunks) {
          await interaction.followUp({
            content: `\`\`\`\n${chunk}\n\`\`\``
          });
        }
        break;
      }
      
      case 'status': {
        const containerStatus = await execAsync(`docker inspect ${config.container} --format '{{.State.Status}}'`);
        const diskUsage = await execInContainer(config.container, 'df -h /workspace | tail -1');
        const gitStatus = await execInContainer(config.container, 'git status -s', config.workdir);
        
        await interaction.editReply({
          content: `📊 **Project Status: ${config.description}**\n\n` +
                  `Container: ${containerStatus.stdout.trim()}\n` +
                  `Disk: ${diskUsage}\n` +
                  `\`\`\`\n${gitStatus || 'No changes'}\n\`\`\``
        });
        break;
      }
      
      case 'shell': {
        const command = interaction.options.getString('command', true);
        
        // Safety check for destructive commands
        const dangerous = ['rm -rf', 'dd if=', 'mkfs', '> /dev/'];
        if (dangerous.some(d => command.includes(d))) {
          await interaction.editReply('⛔ Potentially destructive command blocked. Use with caution.');
          return;
        }
        
        const output = await execInContainer(config.container, command, config.workdir);
        await interaction.editReply({
          content: `\`\`\`bash\n$ ${command}\n${output.slice(0, 1900)}\n\`\`\``
        });
        break;
      }
      
      case 'git': {
        const args = interaction.options.getString('args', true);
        const output = await execInContainer(config.container, `git ${args}`, config.workdir);
        await interaction.editReply({
          content: `\`\`\`\n${output.slice(0, 1900)}\n\`\`\``
        });
        break;
      }
      
      case 'monorepo-info': {
        const packages = config.monorepo.packages.join('\n- ');
        const tree = await execInContainer(config.container, 'tree -L 2 -d', config.workdir);
        
        await interaction.editReply({
          content: `📦 **Monorepo Structure**\n\n` +
                  `Tool: ${config.monorepo.tool}\n` +
                  `Packages:\n- ${packages}\n\n` +
                  `\`\`\`\n${tree.slice(0, 1500)}\n\`\`\``
        });
        break;
      }
    }
    
  } catch (error: any) {
    console.error('Command error:', error);
    await interaction.editReply({
      content: `❌ Error: ${error.message}`
    });
  }
});

// Bot ready
client.on('ready', async () => {
  console.log(`✅ Bot logged in as ${client.user?.tag}`);
  
  // Register slash commands
  const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN!);
  
  try {
    await rest.put(
      Routes.applicationCommands(process.env.APPLICATION_ID!),
      { body: commands }
    );
    console.log('✅ Slash commands registered');
  } catch (error) {
    console.error('Failed to register commands:', error);
  }
});

// Start bot
client.login(process.env.DISCORD_TOKEN);

Start the Discord bot:

cd ~/claude-infra/discord-bot
deno run --allow-all bot.ts

Run bot as systemd service:

# /etc/systemd/system/claude-discord-bot.service
[Unit]
Description=Claude Code Discord Bot (Multi-Project)
After=network.target docker.service

[Service]
Type=simple
User=youruser
WorkingDirectory=/home/youruser/claude-infra/discord-bot
EnvironmentFile=/home/youruser/claude-infra/discord-bot/.env
ExecStart=/home/youruser/.deno/bin/deno run --allow-all bot.ts
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable claude-discord-bot
sudo systemctl start claude-discord-bot
sudo systemctl status claude-discord-bot

Discord server organization

Create structured channel layout:

Your Discord Server
│
├── 📁 INFRASTRUCTURE
│   ├── #system-status (bot health, container stats)
│   ├── #announcements (maintenance windows, updates)
│   └── #admin-commands (restricted admin-only channel)
│
├── 📁 PROJECT A - JavaScript Monorepo
│   ├── #project-a-dev (Claude commands)
│   ├── #project-a-logs (git commits, CI/CD)
│   └── #project-a-discuss (team chat)
│
├── 📁 PROJECT B - Python ML
│   ├── #project-b-dev
│   ├── #project-b-logs
│   └── #project-b-discuss
│
├── 📁 PROJECT C - Rust Systems
│   ├── #project-c-dev
│   ├── #project-c-logs
│   └── #project-c-discuss
│
└── 📁 RESOURCES
    ├── #documentation (setup guides, workflows)
    └── #troubleshooting (common issues, solutions)

Configure role-based permissions:

Create Discord roles:

  • admin - Full access to all projects
  • project-a-devs - Access to Project A
  • project-b-devs - Access to Project B
  • project-c-devs - Access to Project C
  • engineering - Read access to all projects
  • observers - View-only access

Set channel permissions:

  1. Each #project-X-dev channel: Only respective role can send messages
  2. Each #project-X-logs channel: Read-only for all, bot can post
  3. #admin-commands: Admin-only
  4. #system-status: Bot posts, all can view

Management scripts and automation

Create container management wrapper:

#!/bin/bash
# ~/claude-infra/manage.sh

set -e

COMPOSE_FILE="$HOME/claude-infra/docker-compose.yml"

function usage() {
  cat << EOF
Claude Multi-Project Manager

Usage: ./manage.sh <command> [options]

Commands:
  start [project]       Start all containers or specific project
  stop [project]        Stop all containers or specific project
  restart [project]     Restart containers
  status                Show status of all containers
  logs <project>        View logs for project
  exec <project> <cmd>  Execute command in project container
  shell <project>       Open shell in project container
  rebuild [project]     Rebuild container images
  backup                Backup all container state
  restore <file>        Restore from backup
  health                Run health checks on all projects
  update                Update claudebox and bot
  
Projects: project-a, project-b, project-c

Examples:
  ./manage.sh start project-a
  ./manage.sh logs project-b
  ./manage.sh exec project-c "git status"
  ./manage.sh shell project-a
EOF
}

function get_container() {
  case "$1" in
    project-a) echo "claudebox-project-a" ;;
    project-b) echo "claudebox-project-b" ;;
    project-c) echo "claudebox-project-c" ;;
    *) echo "Unknown project: $1" >&2; exit 1 ;;
  esac
}

case "$1" in
  start)
    if [ -z "$2" ]; then
      docker compose -f "$COMPOSE_FILE" up -d
      echo "✅ All containers started"
    else
      CONTAINER=$(get_container "$2")
      docker compose -f "$COMPOSE_FILE" up -d "$CONTAINER"
      echo "✅ Started $CONTAINER"
    fi
    ;;
    
  stop)
    if [ -z "$2" ]; then
      docker compose -f "$COMPOSE_FILE" stop
      echo "✅ All containers stopped"
    else
      CONTAINER=$(get_container "$2")
      docker compose -f "$COMPOSE_FILE" stop "$CONTAINER"
      echo "✅ Stopped $CONTAINER"
    fi
    ;;
    
  restart)
    if [ -z "$2" ]; then
      docker compose -f "$COMPOSE_FILE" restart
      echo "✅ All containers restarted"
    else
      CONTAINER=$(get_container "$2")
      docker compose -f "$COMPOSE_FILE" restart "$CONTAINER"
      echo "✅ Restarted $CONTAINER"
    fi
    ;;
    
  status)
    docker compose -f "$COMPOSE_FILE" ps
    echo ""
    echo "Resource usage:"
    docker stats --no-stream --format "table {{.Container}}\t{{.CPUPerc}}\t{{.MemUsage}}" \
      claudebox-project-a claudebox-project-b claudebox-project-c
    ;;
    
  logs)
    if [ -z "$2" ]; then
      echo "Error: Specify project" >&2
      exit 1
    fi
    CONTAINER=$(get_container "$2")
    docker compose -f "$COMPOSE_FILE" logs -f --tail=100 "$CONTAINER"
    ;;
    
  exec)
    if [ -z "$2" ] || [ -z "$3" ]; then
      echo "Error: Specify project and command" >&2
      exit 1
    fi
    CONTAINER=$(get_container "$2")
    shift 2
    docker exec -it "$CONTAINER" bash -c "$@"
    ;;
    
  shell)
    if [ -z "$2" ]; then
      echo "Error: Specify project" >&2
      exit 1
    fi
    CONTAINER=$(get_container "$2")
    docker exec -it "$CONTAINER" bash
    ;;
    
  rebuild)
    if [ -z "$2" ]; then
      docker compose -f "$COMPOSE_FILE" build --no-cache
      docker compose -f "$COMPOSE_FILE" up -d
    else
      CONTAINER=$(get_container "$2")
      docker compose -f "$COMPOSE_FILE" build --no-cache "$CONTAINER"
      docker compose -f "$COMPOSE_FILE" up -d "$CONTAINER"
    fi
    echo "✅ Rebuild complete"
    ;;
    
  backup)
    BACKUP_DIR="$HOME/claude-backups/$(date +%Y%m%d-%H%M%S)"
    mkdir -p "$BACKUP_DIR"
    
    # Backup container state
    for project in project-a project-b project-c; do
      CONTAINER=$(get_container "$project")
      echo "Backing up $CONTAINER..."
      docker export "$CONTAINER" | gzip > "$BACKUP_DIR/${CONTAINER}.tar.gz"
    done
    
    # Backup configurations
    cp -r ~/.claudebox "$BACKUP_DIR/config"
    
    echo "✅ Backup saved to $BACKUP_DIR"
    ;;
    
  health)
    echo "Running health checks..."
    echo ""
    
    for project in project-a project-b project-c; do
      CONTAINER=$(get_container "$project")
      echo "Checking $project..."
      
      # Container running?
      if docker ps --filter "name=$CONTAINER" --format '{{.Names}}' | grep -q "$CONTAINER"; then
        echo "  ✅ Container running"
      else
        echo "  ❌ Container not running"
        continue
      fi
      
      # Claude authenticated?
      if docker exec "$CONTAINER" bash -c "[ -f /root/.claude/.credentials.json ]"; then
        echo "  ✅ Claude authenticated"
      else
        echo "  ⚠️  Claude not authenticated"
      fi
      
      # Workspace accessible?
      if docker exec "$CONTAINER" bash -c "[ -d /workspace ]"; then
        echo "  ✅ Workspace mounted"
      else
        echo "  ❌ Workspace not mounted"
      fi
      
      # Git repository?
      if docker exec "$CONTAINER" bash -c "cd /workspace && git status >/dev/null 2>&1"; then
        echo "  ✅ Git repository valid"
      else
        echo "  ⚠️  Not a git repository"
      fi
      
      echo ""
    done
    ;;
    
  update)
    echo "Updating Claude Code CLI..."
    npm install -g @anthropic-ai/claude-code
    
    echo "Updating Discord bot..."
    cd ~/claude-infra/discord-bot
    git pull
    
    echo "Updating containers..."
    docker compose -f "$COMPOSE_FILE" pull
    docker compose -f "$COMPOSE_FILE" up -d
    
    echo "✅ Update complete"
    ;;
    
  *)
    usage
    exit 1
    ;;
esac

Make executable:

chmod +x ~/claude-infra/manage.sh

# Add alias for convenience
echo "alias claude-manage='~/claude-infra/manage.sh'" >> ~/.bashrc
source ~/.bashrc

Create automatic checkpoint script:

#!/bin/bash
# ~/claude-infra/auto-checkpoint.sh

# Commits work in progress for all projects every hour

for project in project-a project-b project-c; do
  CONTAINER="claudebox-$project"
  
  echo "Checkpointing $project..."
  docker exec "$CONTAINER" bash -c "
    cd /workspace && \
    git add -A && \
    git commit -m 'Auto-checkpoint: $(date +"%Y-%m-%d %H:%M:%S")' >/dev/null 2>&1 || true
  "
done

echo "Checkpoints created at $(date)"

Schedule with cron:

chmod +x ~/claude-infra/auto-checkpoint.sh

# Add to crontab
crontab -e

# Add line:
0 * * * * /home/youruser/claude-infra/auto-checkpoint.sh >> /home/youruser/claude-infra/checkpoint.log 2>&1

Create health monitoring:

#!/bin/bash
# ~/claude-infra/health-monitor.sh

WEBHOOK_URL="your_discord_webhook_url"

function send_alert() {
  curl -X POST "$WEBHOOK_URL" \
    -H "Content-Type: application/json" \
    -d "{\"content\": \"🚨 $1\"}"
}

# Check each container
for container in claudebox-project-a claudebox-project-b claudebox-project-c; do
  if ! docker ps --format '{{.Names}}' | grep -q "^${container}$"; then
    send_alert "Container $container is not running!"
    
    # Attempt restart
    docker start "$container"
    sleep 5
    
    if docker ps --format '{{.Names}}' | grep -q "^${container}$"; then
      send_alert "Container $container restarted successfully"
    else
      send_alert "Failed to restart container $container"
    fi
  fi
done

# Check disk space
DISK_USAGE=$(df -h /home | tail -1 | awk '{print $5}' | sed 's/%//')
if [ "$DISK_USAGE" -gt 85 ]; then
  send_alert "Disk usage is at ${DISK_USAGE}% - cleanup recommended"
fi

# Check memory
MEM_USAGE=$(free | grep Mem | awk '{printf "%.0f", $3/$2 * 100}')
if [ "$MEM_USAGE" -gt 90 ]; then
  send_alert "Memory usage is at ${MEM_USAGE}% - may need to restart containers"
fi

Schedule health checks:

chmod +x ~/claude-infra/health-monitor.sh

# Add to crontab (every 5 minutes)
crontab -e

# Add:
*/5 * * * * /home/youruser/claude-infra/health-monitor.sh

Operational workflows

Daily developer workflow:

  1. Check system status (via Discord):

    /status
    
  2. Start working on a feature:

    /git checkout -b feature/new-api-endpoint
    /claude Create a new REST endpoint for user profile updates in the api package. Follow our existing patterns.
    
  3. Review changes:

    /git diff
    /git status
    
  4. Run tests:

    /shell turbo run test --filter=@myorg/api
    
  5. Commit work:

    /git add .
    /git commit -m "feat(api): add user profile update endpoint"
    /git push origin feature/new-api-endpoint
    

Cross-project coordination:

When changes in one project affect another:

  1. Make changes in Project A:

    In #project-a-dev:
    /claude Update the API client types to match the new endpoint schema
    
  2. Export the changes:

    /git diff HEAD~1 src/types/api.ts > /tmp/api-changes.patch
    
  3. Apply to Project B:

    In #project-b-dev:
    /shell cat /tmp/api-changes.patch
    /claude Review this API change from project-a and update our integration accordingly: [paste diff]
    

Emergency rollback:

If Claude makes unwanted changes:

/git status
/git diff HEAD
/git checkout .
/git clean -fd

Or restore from auto-checkpoint:

/git reflog
/git reset --hard HEAD@{1}

Subscription usage monitoring:

Since you're using claude.ai subscriptions (not API), monitor usage via:

  1. Go to https://claude.ai/settings
  2. Check "Usage" tab for your subscription tier limits
  3. Usage resets monthly based on your subscription date

Each container shares the same subscription, so total usage is aggregate across all three projects.

Credential refresh:

When your 30-day authentication expires:

# On the VM
claude /login

# Restart containers to pick up new credentials
claude-manage restart

Troubleshooting

Container won't start:

# Check logs
claude-manage logs project-a

# Common issues:
# 1. Port conflict
docker ps -a | grep claudebox

# 2. Volume mount permissions
ls -la ~/projects/monorepo-a

# 3. Credentials missing
ls -la ~/.claude/.credentials.json

"Not authenticated" error in container:

# Re-authenticate on host
claude /login

# Verify credentials exist
cat ~/.claude/.credentials.json

# Restart containers
claude-manage restart

# Verify mount inside container
docker exec claudebox-project-a ls -la /root/.claude/

Discord bot not responding:

# Check bot process
sudo systemctl status claude-discord-bot

# View bot logs
sudo journalctl -u claude-discord-bot -f

# Common issues:
# 1. Invalid token
grep DISCORD_TOKEN ~/claude-infra/discord-bot/.env

# 2. Missing permissions
# Check bot has "Use Slash Commands" in Discord server settings

# 3. Commands not registered
# Wait 1 hour or restart bot
sudo systemctl restart claude-discord-bot

Claude Code command hangs:

# Check container CPU/memory
docker stats claudebox-project-a

# Kill hung process inside container
docker exec claudebox-project-a pkill -f claude

# Or restart container
claude-manage restart project-a

Monorepo build failures:

# Clear caches
claude-manage exec project-a "rm -rf node_modules .turbo && pnpm install"
claude-manage exec project-b "poetry cache clear --all pypi"
claude-manage exec project-c "cargo clean"

# Rebuild container from scratch
claude-manage rebuild project-a

Disk space issues:

# Check usage
df -h

# Clean Docker
docker system prune -a --volumes
docker volume prune

# Clean build artifacts
claude-manage exec project-a "turbo run clean"
claude-manage exec project-b "find /workspace -type d -name __pycache__ -exec rm -rf {} +"
claude-manage exec project-c "cargo clean"

Security considerations

Credential security:

  • Never commit .env files to git
  • Restrict ~/.claude/.credentials.json permissions: chmod 600 ~/.claude/.credentials.json
  • Use SSH keys for git operations, not passwords
  • Rotate Discord bot token if exposed

Container isolation:

  • Each container has separate network namespace
  • Firewall rules prevent unauthorized egress
  • Containers run as non-root user (UID 1000)
  • No privileged mode

Discord permissions:

  • Use role-based access control
  • Audit channel permissions monthly
  • Restrict /shell and destructive commands to admins
  • Enable 2FA for all Discord accounts

Git security:

  • Always work on feature branches
  • Require code review for main/master
  • Use GPG signing for commits
  • Never commit secrets or API keys

VM hardening:

# Setup UFW firewall
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp  # SSH
sudo ufw enable

# Disable password authentication
sudo sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart sshd

# Setup fail2ban
sudo apt install fail2ban
sudo systemctl enable fail2ban

Backup strategy:

# Weekly full backup
0 2 * * 0 /home/youruser/claude-infra/manage.sh backup

# Keep last 4 weeks
find ~/claude-backups -type d -mtime +28 -exec rm -rf {} +

Subscription usage optimization

Usage limits by tier:

  • Claude Pro: Approximately 5x more usage than free tier, resets monthly
  • Claude Team: Higher limits, usage pooled across team members
  • Usage tracked per subscription, not per container

Strategies to optimize usage:

  1. Use planning mode first:

    /claude --mode plan "Design the new API endpoint structure"
    

    Reviews design without making changes.

  2. Batch related tasks:

    /claude "Update all API endpoints to use the new error handling pattern, then update tests, then update documentation"
    

    Single conversation vs. three separate ones.

  3. Use shell commands for simple operations:

    /shell grep -r "TODO" src/
    /git log --oneline -10
    

    Don't waste Claude on simple lookups.

  4. Enable auto-checkpoints: Prevents needing to regenerate work if you hit usage limits mid-task.

  5. Schedule heavy operations: Run large refactors at the start of your billing cycle when usage resets.

Monitor usage per project:

Create a tracking webhook:

// Add to bot.ts
async function logUsage(project: string, prompt: string) {
  await fetch('YOUR_TRACKING_ENDPOINT', {
    method: 'POST',
    body: JSON.stringify({
      project,
      timestamp: new Date(),
      promptLength: prompt.length,
      user: interaction.user.id
    })
  });
}

This helps identify which projects consume the most usage.


Conclusion

This infrastructure provides production-grade multi-project Claude Code access with strong isolation, team collaboration, and subscription-based authentication. The multi-container architecture scales horizontally—add more projects by extending the docker-compose file and updating the bot's PROJECT_CONFIGS.

Quick reference:

# Management
claude-manage start              # Start all containers
claude-manage status             # Check system status
claude-manage logs project-a     # View logs
claude-manage shell project-b    # Open shell
claude-manage health             # Run health checks

# Discord commands
/claude <prompt>                 # Main interaction
/status                          # Project status
/shell <command>                 # Execute shell
/git <args>                      # Git operations
/monorepo-info                   # Show structure

# Maintenance
claude                           # Interactive mode (triggers auth if needed)
claude-manage restart            # Restart containers
claude-manage update             # Update everything

For advanced configurations, refer to the individual project documentation:


rdev: K3s Implementation Notes

This section documents our actual implementation running on k3s instead of a standalone VM.

Architecture Difference

The reference guide above describes a VM-based deployment with Docker Compose. Our implementation uses:

  • Kubernetes (k3s) instead of Docker Compose
  • StatefulSets instead of standalone containers
  • Longhorn PVCs instead of host volume mounts
  • GitHub Container Registry instead of local images
k3s cluster (orchard9-k3sf)
└── rdev namespace
    ├── claudebox-0 (StatefulSet pod)
    │   ├── Claude Code CLI
    │   ├── /workspace (PVC: 20Gi)
    │   └── /root/.claude (PVC: 1Gi)
    └── Future: discord-bot, claudebox-pantheon, claudebox-aeries

Key Commands

# REQUIRED: Set kubeconfig before any kubectl command
export KUBECONFIG=~/.kube/orchard9-k3sf.yaml

# Interactive Claude session (triggers OAuth if not authenticated)
kubectl exec -it -n rdev claudebox-0 -- claude

# Run Claude with a prompt
kubectl exec -it -n rdev claudebox-0 -- claude "your prompt here"

# Shell access
kubectl exec -it -n rdev claudebox-0 -- bash

# Check status
kubectl get pods -n rdev

# View logs
kubectl logs -n rdev claudebox-0

Authentication

Claude authenticates via OAuth on first run. Auth persists in the /root/.claude PVC:

kubectl exec -it -n rdev claudebox-0 -- claude
# Follow the URL to authenticate
# Auth persists across pod restarts

Image

ghcr.io/orchard9/rdev-claudebox:v0.1.0

Built for linux/amd64 (k3s node architecture).

Differences from Reference Guide

Reference Guide rdev Implementation
VM with Docker Compose k3s with Kustomize
docker exec kubectl exec
Host volume mounts Longhorn PVCs
~/.claude/.credentials.json PVC at /root/.claude
claudebox binary Custom Dockerfile
Deno Discord bot TBD (v0.4+)

Version History

See history/ directory for detailed release notes.