Autonomous Claude Code on Mac Mini with GitHub Issue Control

Running Claude Code as an autonomous agent on dedicated Mac mini hardware enables powerful always-on development assistance. This guide covers setting up persistent Claude Code processes that can be steered through GitHub issues, creating a sophisticated human-in-the-loop automation system.

Architecture Overview

System Components

  1. Mac Mini Hardware

    • Dedicated machine for Claude Code agents
    • Always-on operation with remote access
    • Multiple agent instances for parallel processing
  2. Claude Code Agents

    • Headless mode operation
    • GitHub webhook integration
    • MCP server connections
  3. GitHub Control Plane

    • Issues as task definitions
    • PR-based code delivery
    • Webhook-driven triggers
  4. Monitoring Infrastructure

    • Remote access via SSH/VNC
    • Log aggregation
    • Performance metrics

Mac Mini Setup

Hardware Requirements

  • Mac mini M2 or newer (recommended for performance)
  • 16GB+ RAM for multiple agent instances
  • 500GB+ SSD for workspace and caching
  • Stable internet connection with static IP or dynamic DNS
  • UPS backup for uninterrupted operation

Initial Configuration

# Enable remote login
sudo systemsetup -setremotelogin on
 
# Configure automatic login (for GUI access if needed)
sudo defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser -string "claude-agent"
 
# Disable sleep and screen saver
sudo pmset -a sleep 0
sudo pmset -a disksleep 0
sudo pmset -a displaysleep 0
 
# Install development tools
xcode-select --install
 
# Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
 
# Install essential tools
brew install node git python3 rust
brew install --cask visual-studio-code

Claude Code Installation

# Install Claude Code globally
npm install -g @anthropic-ai/claude-code
 
# Create dedicated user for Claude agents
sudo dscl . -create /Users/claude-agent
sudo dscl . -create /Users/claude-agent UserShell /bin/zsh
sudo dscl . -create /Users/claude-agent RealName "Claude Agent"
sudo dscl . -create /Users/claude-agent UniqueID 505
sudo dscl . -create /Users/claude-agent PrimaryGroupID 20
sudo dscl . -create /Users/claude-agent NFSHomeDirectory /Users/claude-agent
sudo mkdir -p /Users/claude-agent
sudo chown -R claude-agent:staff /Users/claude-agent
 
# Set up API key
echo "export ANTHROPIC_API_KEY='your-api-key-here'" >> ~/.zshrc
source ~/.zshrc

Autonomous Agent Setup

Creating Persistent Services

Create LaunchDaemon for automatic startup:

<!-- /Library/LaunchDaemons/com.claude.agent.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.claude.agent</string>
    <key>ProgramArguments</key>
    <array>
        <string>/usr/local/bin/node</string>
        <string>/usr/local/bin/claude-agent-supervisor</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/var/log/claude-agent.log</string>
    <key>StandardErrorPath</key>
    <string>/var/log/claude-agent.error.log</string>
    <key>EnvironmentVariables</key>
    <dict>
        <key>ANTHROPIC_API_KEY</key>
        <string>your-api-key-here</string>
        <key>NODE_ENV</key>
        <string>production</string>
    </dict>
</dict>
</plist>

Agent Supervisor Script

#!/usr/bin/env node
// /usr/local/bin/claude-agent-supervisor
 
import { spawn } from 'child_process';
import { WebhookServer } from './webhook-server';
import { AgentManager } from './agent-manager';
import { MonitoringService } from './monitoring';
 
class ClaudeAgentSupervisor {
  private agents: Map<string, AgentInstance> = new Map();
  private webhookServer: WebhookServer;
  private monitoring: MonitoringService;
  
  async start() {
    // Initialize monitoring
    this.monitoring = new MonitoringService({
      port: 9090,
      metricsPath: '/metrics'
    });
    
    // Start webhook server for GitHub events
    this.webhookServer = new WebhookServer({
      port: 8080,
      secret: process.env.GITHUB_WEBHOOK_SECRET
    });
    
    // Handle incoming GitHub events
    this.webhookServer.on('issue.created', this.handleNewIssue.bind(this));
    this.webhookServer.on('issue.commented', this.handleIssueComment.bind(this));
    this.webhookServer.on('pull_request.opened', this.handlePR.bind(this));
    
    // Start initial agents
    await this.startDefaultAgents();
    
    console.log('Claude Agent Supervisor started successfully');
  }
  
  private async handleNewIssue(event: GitHubIssueEvent) {
    // Parse issue for Claude commands
    if (event.issue.body.includes('@claude')) {
      const task = this.parseTaskFromIssue(event.issue);
      
      // Spawn new agent for this task
      const agent = await this.spawnAgent({
        taskId: `issue-${event.issue.number}`,
        prompt: task.prompt,
        workDir: `/tmp/claude-tasks/issue-${event.issue.number}`,
        githubContext: {
          repo: event.repository.full_name,
          issue: event.issue.number
        }
      });
      
      // Monitor agent progress
      agent.on('complete', (result) => {
        this.postIssueComment(event.issue, result);
      });
    }
  }
  
  private async spawnAgent(config: AgentConfig): Promise<AgentInstance> {
    const agent = new AgentInstance(config);
    
    // Start Claude in headless mode
    const process = spawn('npx', [
      '@anthropic-ai/claude-code',
      '--headless',
      '--no-interactive',
      config.prompt
    ], {
      cwd: config.workDir,
      env: {
        ...process.env,
        CLAUDE_HEADLESS: 'true',
        CLAUDE_TASK_ID: config.taskId
      }
    });
    
    // Track agent
    this.agents.set(config.taskId, agent);
    
    // Monitor output
    process.stdout.on('data', (data) => {
      agent.appendOutput(data.toString());
      this.monitoring.recordAgentActivity(config.taskId, 'output', data.length);
    });
    
    process.on('exit', (code) => {
      agent.complete(code);
      this.agents.delete(config.taskId);
    });
    
    return agent;
  }
}
 
// Start supervisor
const supervisor = new ClaudeAgentSupervisor();
supervisor.start().catch(console.error);

GitHub Integration

Webhook Configuration

// webhook-server.ts
import express from 'express';
import { Webhooks } from '@octokit/webhooks';
import { Octokit } from '@octokit/rest';
 
export class WebhookServer {
  private app: express.Application;
  private webhooks: Webhooks;
  private octokit: Octokit;
  
  constructor(config: WebhookConfig) {
    this.app = express();
    this.webhooks = new Webhooks({
      secret: config.secret
    });
    
    this.octokit = new Octokit({
      auth: process.env.GITHUB_TOKEN
    });
    
    this.setupRoutes();
  }
  
  private setupRoutes() {
    this.app.post('/webhook', (req, res) => {
      this.webhooks.verifyAndReceive({
        id: req.headers['x-github-delivery'] as string,
        name: req.headers['x-github-event'] as any,
        signature: req.headers['x-hub-signature-256'] as string,
        payload: req.body
      }).then(() => {
        res.status(200).send('OK');
      }).catch((err) => {
        console.error('Webhook error:', err);
        res.status(400).send('Webhook validation failed');
      });
    });
  }
  
  async postComment(issue: number, comment: string) {
    await this.octokit.issues.createComment({
      owner: this.config.owner,
      repo: this.config.repo,
      issue_number: issue,
      body: comment
    });
  }
}

Issue Command Parser

interface ClaudeCommand {
  action: 'implement' | 'review' | 'fix' | 'refactor';
  target?: string;
  options?: Record<string, any>;
  priority?: 'low' | 'medium' | 'high';
}
 
class IssueParser {
  parseCommand(issueBody: string): ClaudeCommand | null {
    const claudeRegex = /@claude\s+(\w+)\s*(.*)?/i;
    const match = issueBody.match(claudeRegex);
    
    if (!match) return null;
    
    const [, action, args] = match;
    
    return {
      action: action.toLowerCase() as any,
      target: this.extractTarget(args),
      options: this.extractOptions(args),
      priority: this.extractPriority(issueBody)
    };
  }
  
  private extractTarget(args: string): string | undefined {
    // Extract file paths or feature names
    const targetMatch = args.match(/(?:file:|feature:)\s*([^\s]+)/);
    return targetMatch?.[1];
  }
  
  private extractOptions(args: string): Record<string, any> {
    const options: Record<string, any> = {};
    
    // Extract flags like --test, --no-commit, etc.
    const flagRegex = /--(\w+)(?:=([^\s]+))?/g;
    let match;
    
    while ((match = flagRegex.exec(args)) !== null) {
      options[match[1]] = match[2] || true;
    }
    
    return options;
  }
}

Remote Monitoring

Monitoring Dashboard

// monitoring-service.ts
import { Server } from 'http';
import express from 'express';
import client from 'prom-client';
 
export class MonitoringService {
  private app: express.Application;
  private register: client.Registry;
  
  // Metrics
  private agentCount: client.Gauge;
  private taskDuration: client.Histogram;
  private tokenUsage: client.Counter;
  private errorRate: client.Counter;
  
  constructor(config: MonitoringConfig) {
    this.app = express();
    this.register = new client.Registry();
    
    this.initializeMetrics();
    this.setupRoutes();
  }
  
  private initializeMetrics() {
    this.agentCount = new client.Gauge({
      name: 'claude_agents_active',
      help: 'Number of active Claude agents',
      labelNames: ['status']
    });
    
    this.taskDuration = new client.Histogram({
      name: 'claude_task_duration_seconds',
      help: 'Duration of Claude tasks',
      labelNames: ['task_type', 'status'],
      buckets: [60, 300, 600, 1800, 3600, 7200]
    });
    
    this.tokenUsage = new client.Counter({
      name: 'claude_tokens_total',
      help: 'Total tokens used by Claude',
      labelNames: ['model', 'task_type']
    });
    
    this.errorRate = new client.Counter({
      name: 'claude_errors_total',
      help: 'Total errors encountered',
      labelNames: ['error_type']
    });
    
    // Register metrics
    this.register.registerMetric(this.agentCount);
    this.register.registerMetric(this.taskDuration);
    this.register.registerMetric(this.tokenUsage);
    this.register.registerMetric(this.errorRate);
  }
  
  private setupRoutes() {
    // Metrics endpoint
    this.app.get('/metrics', async (req, res) => {
      res.set('Content-Type', this.register.contentType);
      res.end(await this.register.metrics());
    });
    
    // Status dashboard
    this.app.get('/dashboard', (req, res) => {
      res.json({
        agents: this.getAgentStatus(),
        system: this.getSystemStatus(),
        recent_tasks: this.getRecentTasks()
      });
    });
  }
}

Remote Access Setup

# Enable SSH with key-based authentication
ssh-keygen -t ed25519 -f ~/.ssh/claude-agent-key
ssh-copy-id -i ~/.ssh/claude-agent-key.pub claude-agent@mac-mini.local
 
# Configure SSH for easier access
cat >> ~/.ssh/config << EOF
Host claude-mac-mini
  HostName mac-mini.local
  User claude-agent
  IdentityFile ~/.ssh/claude-agent-key
  ForwardAgent yes
EOF
 
# Set up VNC for GUI access (if needed)
sudo defaults write /var/db/launchd.db/com.apple.launchd/overrides.plist com.apple.screensharing -dict Disabled -bool false
sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.screensharing.plist

Operational Patterns

Task Steering via Issues

<!-- Example GitHub Issue -->
# Implement User Authentication
 
@claude implement feature: user-authentication
 
## Requirements
- JWT-based authentication
- Support for OAuth providers (Google, GitHub)
- Session management
- Password reset flow
 
## Options
--test=extensive
--documentation
--no-commit
 
## Priority
high
 
## Success Criteria
- All tests pass
- Security best practices followed
- Documentation updated

Continuous Operation

class AgentLifecycleManager {
  private readonly MAX_RUNTIME = 6 * 60 * 60 * 1000; // 6 hours
  private readonly COOLDOWN_PERIOD = 5 * 60 * 1000; // 5 minutes
  
  async manageAgent(agent: AgentInstance) {
    // Set maximum runtime limit
    const timeout = setTimeout(() => {
      this.gracefulShutdown(agent);
    }, this.MAX_RUNTIME);
    
    // Monitor resource usage
    const monitor = setInterval(() => {
      const usage = this.getResourceUsage(agent);
      
      if (usage.memory > 0.9 || usage.cpu > 0.95) {
        this.pauseAgent(agent);
        setTimeout(() => this.resumeAgent(agent), this.COOLDOWN_PERIOD);
      }
    }, 30000); // Check every 30 seconds
    
    agent.on('complete', () => {
      clearTimeout(timeout);
      clearInterval(monitor);
    });
  }
}

Security Considerations

API Key Management

// Secure key storage using macOS Keychain
import { exec } from 'child_process';
import { promisify } from 'util';
 
const execAsync = promisify(exec);
 
class KeychainManager {
  async storeAPIKey(key: string): Promise<void> {
    await execAsync(
      `security add-generic-password -a claude-agent -s anthropic-api-key -w "${key}"`
    );
  }
  
  async retrieveAPIKey(): Promise<string> {
    const { stdout } = await execAsync(
      'security find-generic-password -a claude-agent -s anthropic-api-key -w'
    );
    return stdout.trim();
  }
}

Network Security

# Nginx reverse proxy configuration
server {
    listen 443 ssl;
    server_name claude-agent.yourdomain.com;
    
    ssl_certificate /etc/ssl/certs/claude-agent.crt;
    ssl_certificate_key /etc/ssl/private/claude-agent.key;
    
    # Webhook endpoint
    location /webhook {
        proxy_pass http://localhost:8080;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
    
    # Monitoring (restricted)
    location /metrics {
        allow 10.0.0.0/8;
        deny all;
        proxy_pass http://localhost:9090;
    }
}

Best Practices

1. Resource Management

  • Limit concurrent agents based on hardware
  • Implement token usage budgets
  • Monitor memory and CPU usage
  • Use task queuing for overload scenarios

2. Error Handling

  • Implement automatic retry with backoff
  • Log all errors for analysis
  • Create GitHub issues for critical failures
  • Send alerts for system-level problems

3. Task Prioritization

  • Parse priority from issue labels
  • Implement queue-based scheduling
  • Reserve capacity for urgent tasks
  • Balance long-running vs quick tasks

4. Maintenance Windows

  • Schedule regular restarts
  • Implement graceful shutdown
  • Backup critical state
  • Update dependencies automatically

Troubleshooting

Common Issues

  1. Agent Hangs

    # Check process status
    ps aux | grep claude-code
     
    # View recent logs
    tail -n 100 /var/log/claude-agent.log
     
    # Force restart if needed
    sudo launchctl stop com.claude.agent
    sudo launchctl start com.claude.agent
  2. High Memory Usage

    # Check memory usage
    top -o mem
     
    # Clear caches
    rm -rf /tmp/claude-tasks/*
     
    # Restart with lower limits
    export CLAUDE_MAX_MEMORY=8G
  3. GitHub Webhook Failures

    # Test webhook connectivity
    curl -X POST https://claude-agent.yourdomain.com/webhook \
      -H "Content-Type: application/json" \
      -d '{"test": true}'
     
    # Check webhook logs
    grep webhook /var/log/claude-agent.log

Performance Optimization

Caching Strategy

class TaskCache {
  private cache: LRUCache<string, TaskResult>;
  
  constructor() {
    this.cache = new LRUCache({
      max: 100,
      ttl: 1000 * 60 * 60 * 24, // 24 hours
      updateAgeOnGet: true
    });
  }
  
  async getCachedOrExecute(
    task: Task,
    executor: () => Promise<TaskResult>
  ): Promise<TaskResult> {
    const cacheKey = this.generateCacheKey(task);
    const cached = this.cache.get(cacheKey);
    
    if (cached && !this.isStale(cached, task)) {
      return cached;
    }
    
    const result = await executor();
    this.cache.set(cacheKey, result);
    return result;
  }
}

Parallel Processing

class ParallelTaskProcessor {
  private readonly MAX_PARALLEL = 4;
  private running = 0;
  private queue: Task[] = [];
  
  async processTask(task: Task): Promise<void> {
    if (this.running >= this.MAX_PARALLEL) {
      await this.waitForSlot();
    }
    
    this.running++;
    
    try {
      await this.executeTask(task);
    } finally {
      this.running--;
      this.processNext();
    }
  }
}

Future Enhancements

1. Multi-Mac Cluster

  • Distribute tasks across multiple Mac minis
  • Implement load balancing
  • Share state via Redis/PostgreSQL
  • Coordinate through master node

2. Advanced Monitoring

  • Real-time dashboard with Grafana
  • Predictive resource allocation
  • Anomaly detection
  • Cost optimization analytics

3. Enhanced GitHub Integration

  • Support for GitHub Actions
  • Automatic PR merging
  • Issue template parsing
  • Project board integration

Conclusion

Running Claude Code autonomously on Mac mini hardware with GitHub issue control creates a powerful development automation system. This setup enables 24/7 AI assistance while maintaining human oversight through familiar GitHub workflows. The combination of dedicated hardware, robust monitoring, and issue-driven control provides an ideal balance of automation and supervision.

References