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
-
Mac Mini Hardware
- Dedicated machine for Claude Code agents
- Always-on operation with remote access
- Multiple agent instances for parallel processing
-
Claude Code Agents
- Headless mode operation
- GitHub webhook integration
- MCP server connections
-
GitHub Control Plane
- Issues as task definitions
- PR-based code delivery
- Webhook-driven triggers
-
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-codeClaude 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 ~/.zshrcAutonomous 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.plistOperational 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 updatedContinuous 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
-
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 -
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 -
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.