Security Vulnerability Remediation Agent
This workflow is a real security remediation agent, not a toy dependency bump. It takes a GitHub repository URL, the user's GitHub PAT, and an Anthropic API key, clones the repository inside an AerolVM sandbox, installs security tooling based on the detected repository type, scans for known vulnerabilities using OSV and ecosystem-specific scanners, lets Claude Code remediate the fixable issues, reruns the scans, validates the repository, and opens a GitHub pull request automatically.
Copy-paste TypeScript script
Section titled “Copy-paste TypeScript script”import { MicroVM } from "@aerol-ai/aerolvm-sdk";import { writeFile } from "node:fs/promises";
const apiUrl = process.env.SB_API_URL ?? "http://127.0.0.1:21212";const patToken = process.env.SB_PAT_TOKEN;const anthropicApiKey = process.env.ANTHROPIC_API_KEY;const githubToken = process.env.GITHUB_TOKEN;const githubRepoURL = process.env.GITHUB_REPO_URL;const anthropicModel = process.env.ANTHROPIC_MODEL ?? "claude-sonnet-4-6";const sandboxImage = process.env.SANDBOX_IMAGE ?? "node:22-bookworm";const remediationValidationCommand = process.env.REMEDIATION_VALIDATION_COMMAND ?? "";const postPullRequest = (process.env.POST_PULL_REQUEST ?? "true").toLowerCase() !== "false";
if (!patToken) { throw new Error("Set SB_PAT_TOKEN before running this example.");}
if (!anthropicApiKey) { throw new Error("Set ANTHROPIC_API_KEY before running this example.");}
if (!githubToken) { throw new Error("Set GITHUB_TOKEN before running this example.");}
if (!githubRepoURL) { throw new Error("Set GITHUB_REPO_URL before running this example.");}
const repo = parseGitHubRepositoryURL(githubRepoURL);const securityBranchName = process.env.SECURITY_BRANCH_NAME ?? `claude/security-remediation-${new Date().toISOString().slice(0, 10)}`;const sessionName = securityBranchName.replace(/[^a-zA-Z0-9._-]/g, "-");
const claudeSettings = JSON.stringify( { $schema: "https://json.schemastore.org/claude-code-settings.json", model: anthropicModel, }, null, 2,);
const claudeMemory = `# Claude Code security remediation instructions
You are running inside an AerolVM sandbox.
- Prioritize real vulnerabilities with clear evidence from scanner output.- Prefer advisories with CVE, GHSA, GO, or RUSTSEC identifiers and known fixed versions.- Use the pre-scan artifacts as the starting point, then run extra commands when needed.- Apply the smallest safe set of changes that actually removes or mitigates the issue.- Dependency upgrades are preferred; code changes are allowed when needed for compatibility or mitigation.- Write these files in the repository root: - security-remediation-summary.md - security-pr-title.txt - security-pr-body.md`;
const claudeTask = `Review the vulnerability artifacts in .aerolvm-security and remediate the fixable security issues.
Requirements:- Read the scanner outputs before making changes.- Prioritize vulnerabilities that are both actionable and supported by known advisory data.- Use OSV output as the cross-ecosystem baseline and correlate it with ecosystem-specific findings.- If a dependency upgrade changes behavior, update the code or tests so the project still validates.- security-remediation-summary.md must include these sections: Scanner evidence, Vulnerabilities fixed, Residual risk, Validation, Pull request summary.- security-pr-title.txt must contain exactly one line.- security-pr-body.md must be ready for gh pr create --body-file.- Modify only the files necessary to remediate the vulnerabilities.`;
const installSecurityToolingScript = `#!/usr/bin/env bashset -euo pipefail
repo_dir="${1:-/workspace/repo}"cd "$repo_dir"
packages=(git gh curl ca-certificates jq)
if ! command -v go >/dev/null 2>&1; then packages+=(golang-go)fi
if [[ -f requirements.txt || -f pyproject.toml ]] && ! command -v python3 >/dev/null 2>&1; then packages+=(python3 python3-pip python3-venv)fi
if [[ -f Cargo.toml ]] && ! command -v cargo >/dev/null 2>&1; then packages+=(cargo rustc pkg-config libssl-dev)fi
if [[ ( -f pom.xml || -f build.gradle || -f build.gradle.kts ) && ! command -v javac >/dev/null 2>&1 ]]; then packages+=(openjdk-21-jdk maven)fi
apt-get updateDEBIAN_FRONTEND=noninteractive apt-get install -y "${packages[@]}"
export PATH="$HOME/.local/bin:$PATH:/usr/local/bin:/root/go/bin:/root/.cargo/bin"
curl -fsSL https://claude.ai/install.sh | bash
if ! command -v osv-scanner >/dev/null 2>&1; then GOBIN=/usr/local/bin go install github.com/google/osv-scanner/v2/cmd/osv-scanner@latestfi
if [[ -f requirements.txt || -f pyproject.toml ]]; then python3 -m pip install --break-system-packages --upgrade pip pip-auditfi
if [[ -f go.mod ]]; then GOBIN=/usr/local/bin go install golang.org/x/vuln/cmd/govulncheck@latestfi
if [[ -f Cargo.toml ]]; then cargo install cargo-audit --features=fixfi
claude --versiongh --versionosv-scanner --version`;
const scanRepositorySecurityScript = `#!/usr/bin/env bashset -euo pipefail
repo_dir="${1:-/workspace/repo}"out_dir="${2:-/workspace/out}"
mkdir -p "$out_dir/scans"export PATH="$HOME/.local/bin:$PATH:/usr/local/bin:/root/go/bin:/root/.cargo/bin"cd "$repo_dir"
osv-scanner scan source -r "$repo_dir" --format json > "$out_dir/scans/osv-scanner.json" || true
if [[ -f package.json || -f package-lock.json || -f npm-shrinkwrap.json ]]; then if [[ ! -d node_modules ]]; then npm install --ignore-scripts || npm install fi npm audit --json > "$out_dir/scans/npm-audit.json" || truefi
if [[ -f requirements.txt || -f pyproject.toml ]]; then pip-audit --aliases --desc --format=json --output "$out_dir/scans/pip-audit.json" "$repo_dir" || truefi
if [[ -f go.mod ]]; then govulncheck -json ./... > "$out_dir/scans/govulncheck.json" || truefi
if [[ -f Cargo.toml ]]; then if [[ ! -f Cargo.lock ]]; then cargo generate-lockfile || true fi cargo audit --json > "$out_dir/scans/cargo-audit.json" || truefi
if [[ -f pom.xml || -f build.gradle || -f build.gradle.kts ]]; then printf '%s\n' 'Java project detected; OSV scanner output is the primary advisory feed in this workflow.' > "$out_dir/scans/java-note.txt"fi`;
const validateRepositoryScript = `#!/usr/bin/env bashset -euo pipefail
repo_dir="${1:-/workspace/repo}"out_file="${2:-/workspace/out/validation.log}"
export PATH="$HOME/.local/bin:$PATH:/usr/local/bin:/root/go/bin:/root/.cargo/bin"cd "$repo_dir"
if [[ -n "${REMEDIATION_VALIDATION_COMMAND:-}" ]]; then bash -lc "$REMEDIATION_VALIDATION_COMMAND" > "$out_file" 2>&1elif [[ -f package.json ]]; then npm run test --if-present > "$out_file" 2>&1elif [[ -f pyproject.toml || -f requirements.txt ]]; then if python3 -c 'import pytest' >/dev/null 2>&1; then python3 -m pytest > "$out_file" 2>&1 else python3 -m compileall . > "$out_file" 2>&1 fielif [[ -f go.mod ]]; then go test ./... > "$out_file" 2>&1elif [[ -f Cargo.toml ]]; then cargo test > "$out_file" 2>&1elif [[ -f pom.xml ]]; then mvn -q -DskipTests=false test > "$out_file" 2>&1elif [[ -f gradlew ]]; then chmod +x ./gradlew ./gradlew test > "$out_file" 2>&1else printf '%s\n' 'No validation command was provided and no default validation command was inferred.' > "$out_file"fi`;
const runSecurityRemediationScript = `#!/usr/bin/env bashset -euo pipefail
repo_dir="${1:-/workspace/repo}"out_dir="${2:-/workspace/out}"
export PATH="$HOME/.local/bin:$PATH:/usr/local/bin:/root/go/bin:/root/.cargo/bin"cd "$repo_dir"
bash /workspace/tools/scan-repository-security.sh "$repo_dir" "$out_dir/pre"
claude \ --model "$ANTHROPIC_MODEL" \ --dangerously-skip-permissions \ --append-system-prompt "$(cat /workspace/prompts/security-system-prompt.txt)" \ -p "$(cat /workspace/prompts/security-task.txt)"
test -f security-remediation-summary.mdtest -f security-pr-title.txttest -f security-pr-body.md
bash /workspace/tools/scan-repository-security.sh "$repo_dir" "$out_dir/post"bash /workspace/tools/validate-repository.sh "$repo_dir" "$out_dir/validation.log"
git status --porcelain > "$out_dir/git-status.txt"test -s "$out_dir/git-status.txt"
git config user.name "AerolVM Security Bot"git config user.email "security-bot@aerolvm.local"git add .git commit -m "fix(security): remediate known vulnerabilities"git push --set-upstream origin "$SECURITY_BRANCH_NAME"
if [[ "$POST_PULL_REQUEST" == "true" ]]; then gh pr create \ --repo "$GITHUB_REPO_SLUG" \ --title "$(head -n 1 security-pr-title.txt)" \ --body-file security-pr-body.md \ > "$out_dir/pull-request-url.txt"else printf '%s\n' 'PR creation disabled by POST_PULL_REQUEST=false' > "$out_dir/pull-request-url.txt"fi
git diff HEAD~1 HEAD --binary > "$out_dir/security-remediation.patch"cp security-remediation-summary.md "$out_dir/security-remediation-summary.md"tar -czf "$out_dir/security-scan-artifacts.tgz" -C "$out_dir" pre post validation.log pull-request-url.txt`;
async function main() { const client = new MicroVM({ apiUrl, patToken }); const sandbox = await client.create({ image: sandboxImage, cpu: 2, memoryMB: 4096, diskGB: 20, env: { ANTHROPIC_API_KEY: anthropicApiKey, ANTHROPIC_MODEL: anthropicModel, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken, GITHUB_REPO_SLUG: repo.repoSlug, POST_PULL_REQUEST: String(postPullRequest), REMEDIATION_VALIDATION_COMMAND: remediationValidationCommand, SECURITY_BRANCH_NAME: securityBranchName, }, lifecycle: { destroyIfIdleFor: 60 * 60 * 1_000_000_000, }, });
try { await sandbox.exec("mkdir -p /workspace/tools /workspace/prompts /workspace/out");
await Promise.all([ sandbox.uploadFile( "/workspace/tools/install-security-tooling.sh", installSecurityToolingScript, ), sandbox.uploadFile( "/workspace/tools/scan-repository-security.sh", scanRepositorySecurityScript, ), sandbox.uploadFile( "/workspace/tools/validate-repository.sh", validateRepositoryScript, ), sandbox.uploadFile( "/workspace/tools/run-security-remediation.sh", runSecurityRemediationScript, ), sandbox.uploadFile( "/workspace/prompts/security-system-prompt.txt", claudeMemory, ), sandbox.uploadFile( "/workspace/prompts/security-task.txt", claudeTask, ), ]);
await sandbox.exec({ command: [ "chmod +x /workspace/tools/*.sh", "gh auth setup-git", `gh repo clone ${repo.repoSlug} /workspace/repo -- --filter=blob:none`, `cd /workspace/repo && git checkout -b ${shellQuote(securityBranchName)}`, "mkdir -p /workspace/repo/.claude /workspace/repo/.aerolvm-security", "bash /workspace/tools/install-security-tooling.sh /workspace/repo", ].join(" && "), timeoutSeconds: 1800, });
await Promise.all([ sandbox.uploadFile( "/workspace/repo/.claude/settings.json", claudeSettings, ), sandbox.uploadFile( "/workspace/repo/CLAUDE.md", claudeMemory, ), ]);
const session = await sandbox.createSession({ name: sessionName, command: "bash /workspace/tools/run-security-remediation.sh /workspace/repo /workspace/out", workDir: "/workspace/repo", env: { ANTHROPIC_API_KEY: anthropicApiKey, ANTHROPIC_MODEL: anthropicModel, GITHUB_TOKEN: githubToken, GH_TOKEN: githubToken, GITHUB_REPO_SLUG: repo.repoSlug, POST_PULL_REQUEST: String(postPullRequest), REMEDIATION_VALIDATION_COMMAND: remediationValidationCommand, SECURITY_BRANCH_NAME: securityBranchName, }, });
const attach = sandbox.attachSession(session.id, { onStdout: (chunk) => { process.stdout.write(Buffer.from(chunk).toString("utf8")); }, onStderr: (chunk) => { process.stderr.write(Buffer.from(chunk).toString("utf8")); }, onError: (message) => { process.stderr.write(`${message}\n`); }, });
const exit = await attach.done; if (exit.code !== 0) { throw new Error(`security remediation session failed with exit code ${exit.code}`); }
const decoder = new TextDecoder(); const summary = decoder.decode( await sandbox.downloadFile("/workspace/out/security-remediation-summary.md"), ); const patch = decoder.decode( await sandbox.downloadFile("/workspace/out/security-remediation.patch"), ); const prURL = decoder.decode( await sandbox.downloadFile("/workspace/out/pull-request-url.txt"), ); const log = decoder.decode(await sandbox.sessionLog(session.id));
await writeFile("security-remediation-summary.md", summary); await writeFile("security-remediation.patch", patch); await writeFile("security-remediation-pr-url.txt", prURL); await writeFile("security-remediation.log", log);
console.log("\n===== security-remediation-summary.md =====\n"); console.log(summary);
console.log( JSON.stringify( { sandboxID: sandbox.id, repo: repo.repoSlug, branch: securityBranchName, pullRequestURLFile: "security-remediation-pr-url.txt", summaryFile: "security-remediation-summary.md", patchFile: "security-remediation.patch", logFile: "security-remediation.log", }, null, 2, ), ); } finally { await sandbox.destroy(); }}
main().catch((error) => { console.error(error); process.exitCode = 1;});
function parseGitHubRepositoryURL(repoURL: string) { if (repoURL.startsWith("git@github.com:")) { const path = repoURL.slice("git@github.com:".length).replace(/\.git$/, ""); const parts = path.split("/"); if (parts.length !== 2) { throw new Error(`Invalid GitHub repository URL: ${repoURL}`); }
return { owner: parts[0], repo: parts[1], repoSlug: `${parts[0]}/${parts[1]}`, }; }
const url = new URL(repoURL); if (url.hostname !== "github.com" && url.hostname !== "www.github.com") { throw new Error(`Unsupported GitHub host: ${url.hostname}`); }
const parts = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "").split("/"); if (parts.length < 2) { throw new Error(`Invalid GitHub repository URL: ${repoURL}`); }
return { owner: parts[0], repo: parts[1], repoSlug: `${parts[0]}/${parts[1]}`, };}
function shellQuote(value: string) { return `'${value.replace(/'/g, `'"'"'`)}'`;}What gets pushed to GitHub
Section titled “What gets pushed to GitHub”- The workflow creates a dedicated remediation branch using
SECURITY_BRANCH_NAME. - Claude writes a PR title and body file, and
gh pr createopens the pull request automatically whenPOST_PULL_REQUESTistrue. - The PR is meant for safe, high-confidence fixes only. If no remediated diff exists, the workflow fails instead of creating noise.
Deliverables
Section titled “Deliverables”security-remediation-summary.mdwith scanner evidence, fixes, residual risk, and validation status.security-remediation.patchwith the exact committed changes.security-remediation-pr-url.txtwith the created pull request URL.security-remediation.logwith the full Claude Code session log.