Skip to content

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.

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 bash
set -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 update
DEBIAN_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@latest
fi
if [[ -f requirements.txt || -f pyproject.toml ]]; then
python3 -m pip install --break-system-packages --upgrade pip pip-audit
fi
if [[ -f go.mod ]]; then
GOBIN=/usr/local/bin go install golang.org/x/vuln/cmd/govulncheck@latest
fi
if [[ -f Cargo.toml ]]; then
cargo install cargo-audit --features=fix
fi
claude --version
gh --version
osv-scanner --version
`;
const scanRepositorySecurityScript = `#!/usr/bin/env bash
set -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" || true
fi
if [[ -f requirements.txt || -f pyproject.toml ]]; then
pip-audit --aliases --desc --format=json --output "$out_dir/scans/pip-audit.json" "$repo_dir" || true
fi
if [[ -f go.mod ]]; then
govulncheck -json ./... > "$out_dir/scans/govulncheck.json" || true
fi
if [[ -f Cargo.toml ]]; then
if [[ ! -f Cargo.lock ]]; then
cargo generate-lockfile || true
fi
cargo audit --json > "$out_dir/scans/cargo-audit.json" || true
fi
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 bash
set -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>&1
elif [[ -f package.json ]]; then
npm run test --if-present > "$out_file" 2>&1
elif [[ -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
fi
elif [[ -f go.mod ]]; then
go test ./... > "$out_file" 2>&1
elif [[ -f Cargo.toml ]]; then
cargo test > "$out_file" 2>&1
elif [[ -f pom.xml ]]; then
mvn -q -DskipTests=false test > "$out_file" 2>&1
elif [[ -f gradlew ]]; then
chmod +x ./gradlew
./gradlew test > "$out_file" 2>&1
else
printf '%s\n' 'No validation command was provided and no default validation command was inferred.' > "$out_file"
fi
`;
const runSecurityRemediationScript = `#!/usr/bin/env bash
set -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.md
test -f security-pr-title.txt
test -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, `'"'"'`)}'`;
}
  • The workflow creates a dedicated remediation branch using SECURITY_BRANCH_NAME.
  • Claude writes a PR title and body file, and gh pr create opens the pull request automatically when POST_PULL_REQUEST is true.
  • The PR is meant for safe, high-confidence fixes only. If no remediated diff exists, the workflow fails instead of creating noise.
  • security-remediation-summary.md with scanner evidence, fixes, residual risk, and validation status.
  • security-remediation.patch with the exact committed changes.
  • security-remediation-pr-url.txt with the created pull request URL.
  • security-remediation.log with the full Claude Code session log.