Back to Home
#AI#Agents#Security

Building Your Own AI Security Agent Homelab

Toni Nowak
Building Your Own AI Security Agent Homelab

How to create an autonomous vulnerability patching system using LM Studio 0.4.1, Devstral, and Claude CLI.

Enterprise AI security tools like Cogent Security just raised $42M to automate vulnerability management. Impressive. But what if you could build something similar for your own homelab — using open-source tools and local AI?

In this article, I'll show you how to create your own AI security agent that:

  • Scans Docker containers, Proxmox VMs, and LXC containers for vulnerabilities
  • Uses LM Studio 0.4.1 with Anthropic-compatible API — works natively with Claude CLI
  • Runs Mistral's Devstral Small 2 model locally (68% on SWE-bench Verified)
  • Costs $0 in API fees — everything runs on your hardware

This is the guide I wish existed when I started automating my homelab security.

What Changed in January 2026

LM Studio 0.4.1 (released January 29, 2026) introduced a game-changer: native Anthropic API compatibility.

This means:

  • ✅ Claude CLI works directly with local models
  • ✅ Use /v1/messages endpoint (same as Anthropic)
  • ✅ Drop-in replacement for Anthropic SDKs
  • ✅ Streaming support (message_start, content_block_delta, message_stop)

No more workarounds. No more OpenAI compatibility layer. Just native Claude integration with your local models.

The Stack

ComponentPurpose
LM Studio 0.4.1Local model server with Anthropic API
Devstral Small 224B parameter code-specialized model
Claude CLIAgent interface
TrivyContainer vulnerability scanner
GrypeAlternative vulnerability scanner
proxmoxerProxmox Python API

Part 1: Setting Up LM Studio 0.4.1+ with Anthropic API

Why LM Studio 0.4.1?

The January 29, 2026 release added native Anthropic API compatibility:

  • Endpoint: http://localhost:1234/v1/messages
  • Environment: ANTHROPIC_BASE_URL=http://localhost:1234
  • Auth: ANTHROPIC_AUTH_TOKEN=lmstudio (or any string)

This makes LM Studio a drop-in replacement for Anthropic's cloud API.

Installation

Linux (AppImage):

# Download LM Studio 0.4.1+
wget https://releases.lmstudio.ai/linux/0.4.1/LM-Studio-0.4.1-x86_64.AppImage
chmod +x LM-Studio-0.4.1-x86_64.AppImage
./LM-Studio-0.4.1-x86_64.AppImage

macOS:

brew install --cask lm-studio

Windows: Download installer from https://lmstudio.ai/

Installing LM Studio CLI

LM Studio 0.4.1+ includes the lms command-line tool:

# Verify CLI installation (after running LM Studio GUI at least once)
lms --help

# Check if server is running
lms ps

# List loaded models
lms ls

Downloading Devstral Small 2 Model

# Install Hugging Face Hub
pip install -U huggingface-hub

# Download Devstral Small 2 (Q4_K_M quantization)
huggingface-cli download \
  unsloth/Devstral-Small-2-24B-Instruct-2512-GGUF \
  Devstral-Small-2-24B-Instruct-2512-Q4_K_M.gguf \
  --local-dir ~/.lmstudio/models

# Alternative: Codestral-22B (smaller, faster)
huggingface-cli download \
  lmstudio-community/Codestral-22B-v0.1-GGUF \
  Codestral-22B-v0.1-Q4_K_M.gguf \
  --local-dir ~/.lmstudio/models

Enabling the Anthropic-Compatible Server

# Load model and start server
lms load unsloth/Devstral-Small-2-24B-Instruct-2512-GGUF \
  --gpu 1.0 \
  --context-length 32768

# Start server
lms server start --port 1234

Verify Server Status

lms ps
# Expected output:
# ✓ Server running on port 1234
# ✓ Model loaded: Devstral-Small-2-24B-Instruct-2512
# ✓ API format: Anthropic Messages

Testing the Anthropic-Compatible API

Python test:

#!/usr/bin/env python3
from anthropic import Anthropic

def test_lm_studio():
    client = Anthropic(
        base_url="http://localhost:1234",
        api_key="lmstudio"
    )
    try:
        message = client.messages.create(
            model="devstral-small-24b",
            max_tokens=1024,
            messages=[{"role": "user", "content": "What is CVE-2024-3094?"}]
        )
        print("✅ Connection successful!")
        print(f"Response: {message.content[0].text}")
        return True
    except Exception as e:
        print(f"❌ Connection failed: {e}")
        return False

if __name__ == "__main__":
    test_lm_studio()

cURL test:

curl http://localhost:1234/v1/messages \
  -H "Content-Type: application/json" \
  -H "x-api-key: lmstudio" \
  -H "anthropic-version: 2023-06-01" \
  -d '{
    "model": "devstral-small-24b",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Explain CVE scanning in one paragraph"}]
  }'

Part 2: Installing Claude CLI

Installation

# macOS, Linux, WSL
curl -fsSL https://claude.ai/install.sh | bash

# Verify
claude --version

Configuration

# Add to ~/.bashrc or ~/.zshrc
export ANTHROPIC_BASE_URL="http://localhost:1234"
export ANTHROPIC_AUTH_TOKEN="lmstudio"
source ~/.bashrc

Test Claude CLI

# Interactive session
claude

# Single command
claude "List all Python files and check for SQL injection vulnerabilities"

# Print mode (query and exit)
claude -p "Review this Dockerfile for security issues"

Verify Connection

DEBUG=1 claude -p "What is 2+2?"
# Expected output includes:
# → Connecting to http://localhost:1234/v1/messages
# → Model: devstral-small-24b

Part 3: Vulnerability Scanning Setup

Trivy (Recommended for Docker)

# Install latest Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | \
  sh -s -- -b /usr/local/bin

# Scan a Docker image
trivy image nginx:latest --format json --output trivy-report.json

# Scan all running containers
mkdir -p reports
docker ps --format '{{.Image}}' | while read img; do
  safe_name=$(echo "$img" | tr '/:' '_')
  trivy image "$img" --format json --output "reports/${safe_name}.json"
done

Grype (Alternative)

curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | \
  sh -s -- -b /usr/local/bin

grype nginx:latest -o json > grype-report.json

Proxmox Scanner

#!/usr/bin/env python3
import json, os
from typing import List, Dict, Any, Optional
from proxmoxer import ProxmoxAPI

class ProxmoxScanner:
    def __init__(self, host: str, user: str,
                 password: Optional[str] = None, verify_ssl: bool = False):
        password = password or os.getenv('PROXMOX_PASSWORD')
        if not password:
            raise ValueError("Password required (set PROXMOX_PASSWORD env var)")
        self.proxmox = ProxmoxAPI(host, user=user, password=password, verify_ssl=verify_ssl)

    def scan_lxc_containers(self) -> List[Dict[str, Any]]:
        results = []
        for node in self.proxmox.nodes.get():
            node_name = node['node']
            for container in self.proxmox.nodes(node_name).lxc.get():
                if container['status'] != 'running':
                    continue
                vmid = container['vmid']
                try:
                    result = self.proxmox.nodes(node_name).lxc(vmid).exec.post(
                        command='apt list --upgradable 2>/dev/null | grep -i security'
                    )
                    results.append({
                        'type': 'lxc', 'node': node_name, 'vmid': vmid,
                        'name': container.get('name', f'CT-{vmid}'),
                        'security_updates': result
                    })
                except Exception as e:
                    print(f"Error scanning LXC {vmid}: {e}")
        return results

if __name__ == "__main__":
    scanner = ProxmoxScanner(
        host=os.getenv('PROXMOX_HOST', '192.168.1.100'),
        user=os.getenv('PROXMOX_USER', 'root@pam'),
        password=os.getenv('PROXMOX_PASSWORD')
    )
    results = scanner.scan_lxc_containers()
    with open('proxmox-scan-results.json', 'w') as f:
        json.dump(results, f, indent=2)

Part 4: Building the AI Security Agent

#!/usr/bin/env python3
import json, subprocess, os
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Any
from anthropic import Anthropic

class SecurityAgent:
    def __init__(self, lm_studio_url: str = "http://localhost:1234"):
        if not lm_studio_url.startswith(('http://', 'https://')):
            raise ValueError("Invalid LM Studio URL format")
        self.client = Anthropic(base_url=lm_studio_url, api_key="lmstudio")
        self.model = "devstral-small-24b"
        self.scans_dir = Path("scans")
        self.patches_dir = Path("patches")
        self.scans_dir.mkdir(parents=True, exist_ok=True)
        self.patches_dir.mkdir(parents=True, exist_ok=True)

    def call_devstral(self, system_prompt: str, user_message: str) -> str:
        try:
            message = self.client.messages.create(
                model=self.model, max_tokens=4096,
                system=system_prompt,
                messages=[{"role": "user", "content": user_message}]
            )
            return message.content[0].text
        except Exception as e:
            print(f"Error calling Devstral: {e}")
            return ""

    def scan_docker(self) -> List[Dict[str, Any]]:
        print("🔍 Scanning Docker containers...")
        result = subprocess.run(
            ["docker", "ps", "--format", "{{.Names}}\t{{.Image}}"],
            capture_output=True, text=True, check=False
        )
        containers = [
            {'name': parts[0], 'image': parts[1]}
            for line in result.stdout.strip().split('\n')
            if line and len(parts := line.split('\t')) == 2
        ]

        scan_results = []
        for container in containers:
            print(f"  Scanning {container['name']} ({container['image']})...")
            scan_file = self.scans_dir / f"docker_{container['name']}.json"
            subprocess.run(
                ["trivy", "image", container['image'],
                 "--format", "json", "--output", str(scan_file), "--quiet"],
                capture_output=True, check=False
            )
            if scan_file.exists():
                with open(scan_file) as f:
                    scan_data = json.load(f)
                vulnerabilities = [
                    {
                        'id': v.get('VulnerabilityID'),
                        'severity': v.get('Severity'),
                        'package': v.get('PkgName'),
                        'installed': v.get('InstalledVersion'),
                        'fixed': v.get('FixedVersion'),
                    }
                    for r in scan_data.get('Results', [])
                    for v in r.get('Vulnerabilities', [])
                ]
                scan_results.append({
                    'container': container['name'],
                    'image': container['image'],
                    'vulnerabilities': vulnerabilities
                })
        return scan_results

    def analyze_vulnerabilities(self, scan_results: List[Dict[str, Any]]) -> Dict[str, Any]:
        system_prompt = """You are a security expert AI. Analyze vulnerability scan results and
output a JSON remediation plan: {"summary":"...","critical_vulnerabilities":[...],"remediation_steps":[...],"automation_candidates":[...]}"""
        response = self.call_devstral(
            system_prompt,
            f"Analyze these scan results:\n\n{json.dumps(scan_results, indent=2)}"
        )
        try:
            start, end = response.find('{'), response.rfind('}') + 1
            if start >= 0 and end > start:
                return json.loads(response[start:end])
        except json.JSONDecodeError:
            pass
        return {"raw_analysis": response}

    def run_full_scan_and_patch_cycle(self) -> Dict[str, Any]:
        print("=" * 60)
        print("🤖 Homelab AI Security Agent - Starting Scan")
        print("=" * 60)

        scan_results = self.scan_docker()

        print("\n🧠 Analyzing vulnerabilities with Devstral...")
        analysis = self.analyze_vulnerabilities(scan_results)

        timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
        analysis_file = self.scans_dir / f"analysis_{timestamp}.json"
        with open(analysis_file, 'w') as f:
            json.dump(analysis, f, indent=2)

        print(f"\n✅ Analysis saved to {analysis_file}")
        return {'scan_results': scan_results, 'analysis': analysis}

if __name__ == "__main__":
    agent = SecurityAgent()
    agent.run_full_scan_and_patch_cycle()

Part 5: Using Claude CLI for Autonomous Patching

# Scan nginx and apply patches for CRITICAL/HIGH CVEs
claude -p "Scan nginx container for vulnerabilities using trivy, then apply security patches if any HIGH or CRITICAL CVEs are found"

# Create Proxmox patch playbook
claude -p "Create an Ansible playbook that updates all security packages on Proxmox LXC containers. Include error handling and rollback capabilities."

Python wrapper:

#!/usr/bin/env python3
import subprocess, os

def run_claude_agent(prompt: str, workdir: str = ".") -> str:
    env = os.environ.copy()
    env["ANTHROPIC_BASE_URL"] = "http://localhost:1234"
    env["ANTHROPIC_AUTH_TOKEN"] = "lmstudio"
    result = subprocess.run(
        ["claude", "-p", prompt],
        cwd=workdir, env=env, capture_output=True, text=True, check=False
    )
    return result.stdout

print(run_claude_agent("Scan docker-compose.yml for security issues"))

Part 6: Automated Scheduling

Systemd Timer

# /etc/systemd/system/security-agent.service
[Unit]
Description=Homelab AI Security Agent
After=network.target docker.service

[Service]
Type=oneshot
ExecStart=/usr/bin/python3 /opt/security-agent/main.py
WorkingDirectory=/opt/security-agent
Environment="ANTHROPIC_BASE_URL=http://localhost:1234"
Environment="ANTHROPIC_AUTH_TOKEN=lmstudio"
# /etc/systemd/system/security-agent.timer
[Unit]
Description=Run security agent daily at 6 AM

[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now security-agent.timer
sudo systemctl list-timers security-agent.timer

Telegram Notifications

import requests, os

def send_telegram_alert(message: str) -> bool:
    bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
    chat_id = os.getenv("TELEGRAM_CHAT_ID")
    if not bot_token or not chat_id:
        return False
    try:
        r = requests.post(
            f"https://api.telegram.org/bot{bot_token}/sendMessage",
            json={"chat_id": chat_id, "text": message, "parse_mode": "HTML"},
            timeout=10
        )
        return r.status_code == 200
    except Exception:
        return False

send_telegram_alert("🚨 <b>5 critical vulnerabilities found in nginx</b>")

Hardware Requirements

  • Minimum: 16GB VRAM for Q4_K_M quantization
  • Recommended: 32K context length (≈ 18-20GB VRAM total)
  • Claude CLI works best with 25K+ context

Troubleshooting

LM Studio Server Issues

lms ps
lms server stop && lms server start --port 1234

Claude CLI Connection Issues

echo $ANTHROPIC_BASE_URL   # should be http://localhost:1234
echo $ANTHROPIC_AUTH_TOKEN  # should be lmstudio
DEBUG=1 claude -p "test"
sudo lsof -i :1234           # check port

OOM / Model Loading Issues

nvidia-smi  # check VRAM
# Reduce context or quantization
lms load model-name --gpu 0.7 --context-length 16384

Key Takeaways

  • LM Studio 0.4.1 has native Anthropic API support — no workarounds needed
  • Claude CLI works directly with local models via ANTHROPIC_BASE_URL
  • Devstral Small 2 scores 68% on SWE-bench Verified
  • 16GB VRAM minimum for Q4_K_M quantization
  • 25K+ context required for best Claude CLI performance
  • Total API cost: $0 — everything runs on your hardware

I'm a senior architect specializing in AI/ML, Linux systems, and data centers. I help organizations build secure, automated infrastructure using open-source tools.

#AI #AIAgents #Cybersecurity #Homelab #Proxmox #Docker #LMStudio #Devstral #ClaudeCLI #Anthropic #OpenSource #DevSecOps #Automation