September 16, 2026
I Built a Multi-Agent CVE Scanner Using an AI Assistant β Hereβs What I Learned
Designing parallel agent workflows for security research, and the non-obvious gotcha that will catch you out.
By Jovannamaria
6 min read
Security vulnerability triage is one of those tasks that looks simple on the surface β look up a CVE, check if your software is affected, decide what to do β but becomes genuinely tedious at scale. When a batch of CVEs drops, you're making the same sequence of lookups, greps, and SSH hops over and over, one by one.
I recently designed a CVE investigation skill for IBM Bob, an AI coding assistant, that automates this entire workflow. Along the way I hit a design problem that forced me to think differently about how AI agents share context and invoke tools β and the solution turned out to be more interesting than the original problem.
This post walks through the design, the parallel agent architecture I landed on, and the specific gotcha around shell commands in subagents that nobody tells you about.
The problem: CVE triage is repetitive and slow
For any given CVE, the investigation always follows the same pattern:
- Fetch the vulnerability description from the NVD database
- Find the affected package in your codebase and check the version
- Check the version actually installed on your running server
- Trace whether the vulnerable code path is even reachable in your application
- Write up findings and recommend remediation
Do this for one CVE and it takes maybe 20 minutes. Do it for five CVEs in the same sitting and you're doing the same grep, the same SSH hop, the same version comparison β sequentially, one at a time β for an hour.
The obvious solution: automate it. The interesting question: how do you structure an AI agent to do it well, especially for multiple CVEs at once?
Version 1: a simple sequential skill
The first version of the skill was a straightforward ordered list of steps. The agent would:
- Fetch the CVE from NVD
- Grep the codebase for the affected package
- SSH into the server and check the installed version
- Run a reachability grep on the source code
- Summarise everything
This worked fine for a single CVE. But it was entirely sequential β each step waited for the previous one to finish β and it only handled one CVE at a time.
The SSH check alone could take 30β60 seconds. While the agent was waiting for the remote host, it wasn't doing anything else. For five CVEs, that's five separate blocking SSH calls, one after another.
There was also a more fundamental issue: the steps weren't actually dependent on each other in the order they were written. The codebase grep doesn't need the SSH result. The SSH check doesn't need the codebase grep result. They're independent β they just happened to run sequentially because the skill was written that way.
Version 2: introducing parallel subagents
IBM Bob supports a spawn_subagent tool that creates an independent agent in its own isolated context. Crucially, if you call spawn_subagent multiple times in a single tool-call block, they run simultaneously.
The dependency graph for a single CVE investigation is actually:
The NVD fetch gates everything β you need the package name and vulnerable class to know what to grep for. But once you have that, the codebase search and the SSH check are completely independent. They can run at the same time.
This was a meaningful improvement for a single CVE. But it still didn't solve the multi-CVE problem.
Version 3: the Monitor Agent pattern
The real insight came when thinking about multiple CVEs. Each CVE's investigation is entirely independent of every other CVE's investigation. There's no reason CVE-1's codebase search needs to wait for CVE-2's NVD fetch to finish.
The pattern I landed on is what I call the Monitor Agent architecture:
Each Monitor Agent owns its CVE completely. It drives its own NVD fetch, spawns its own parallel sub-agents, and returns a fixed-format structured result. The Main Agent's job is just to fan out, wait, and write the final document.
For three CVEs with SSH checking enabled, you get up to 9 agents running across three waves instead of 9 sequential steps. The wall-clock time collapses dramatically.
The gotcha: subagents don't infer tool usage from context
This is the part nobody tells you, and it cost me time to figure out.
When you write a skill for a parent agent, the agent has access to all its tools β file reading, code searching, shell execution, web fetching β and it can use them naturally based on context. If you say "run this script," it knows to call execute_command.
Subagents are different. A subagent's entire world is its description string. It has no memory of the conversation, no inherited understanding of the codebase, and β critically β it will not infer which tool to use unless you tell it explicitly.
My first attempt at the installed-check agent description looked like this:
Run check-installed.sh with the following arguments:
username: root
hostname: myserver.example.com
install-path: /path-to-installation
package: open-source-packageRun check-installed.sh with the following arguments:
username: root
hostname: myserver.example.com
install-path: /path-to-installation
package: open-source-packageWhat went wrong: The subagent described what the script would do. It didn't run it. It had no context telling it that "run this script" meant "call execute_command with this exact string." It treated the instruction as informational.
The fix was to be completely explicit β not just about what to run, but how to run it:
Use the `execute_command` tool to run the following command
and return its full output:
command: bash .bob/skills/cve-check/check-installed.sh \
"root" "myserver.example.com" "mypassword" "path-to-installation" "open-source-package"
Do not describe the command or paraphrase it β call execute_command
directly with that exact string, substituting real values for each
placeholder. Do not use SSH manually; the script handles it.Use the `execute_command` tool to run the following command
and return its full output:
command: bash .bob/skills/cve-check/check-installed.sh \
"root" "myserver.example.com" "mypassword" "path-to-installation" "open-source-package"
Do not describe the command or paraphrase it β call execute_command
directly with that exact string, substituting real values for each
placeholder. Do not use SSH manually; the script handles it.Three things matter in that description:
- Name the tool explicitly β
execute_command, not "run" or "execute" - Give the exact command string β with all arguments already substituted in, not as placeholders
- Pre-empt the wrong interpretation β "do not describe the command" stops the agent from narrating what it would do instead of doing it
This is a general principle for subagent design: a subagent description is not a prompt, it is a specification. Every tool call the agent needs to make should be described in terms of the tool name and the exact parameters, not in terms of the outcome you want.
Writing good subagent descriptions
After working through several iterations, here's what I've found makes subagent descriptions reliable:
1. State the output format, not just the goal
Don't say "investigate this CVE." Say "return a structured block in this exact format:" and then show the format. A subagent that returns freeform prose is hard for the parent agent to parse reliably.
2. Bake in all context at spawn time
Every value the subagent needs β package name, version range, SSH credentials, file paths β must be embedded in the description when you spawn it. The subagent cannot ask you questions mid-execution. If it hits a gap, it will either halt or guess.
3. Use conditional sections for optional behaviour
If a step is optional (e.g. SSH check only if credentials were provided), encode both branches directly in the description: <IF_SSH_PROVIDED>...</IF_SSH_PROVIDED> and <IF_SSH_NOT_PROVIDED>Skip this step.</IF_SSH_NOT_PROVIDED>. The parent agent substitutes the right branch before spawning.
4. Explicit tool names for anything non-trivial
File reads and web searches are usually inferred correctly. Shell commands, API calls, and anything with side effects β name the tool explicitly and give the exact parameters.
The result
The finished skill handles a command like "check out CVE-2025β1234, CVE-2025β5678, CVE-2025β9012" by:
- Asking once for SSH details and preferences
- Spawning three Monitor Agents simultaneously
- Each Monitor Agent fetching its CVE from NVD, then running codebase and SSH checks in parallel
- Collecting all structured results and writing a single combined markdown report with a summary table and per-CVE detail sections
The output document looks like this:
| CVE | Severity | Component | Codebase | Installed | Verdict |
|----------------|--------------|---------------|-----------------|----------------|--------------------------|
| CVE-2025-1234 | 9.8 Critical | activemq 5.x | β οΈ 5.15 Vuln | β
5.16 Safe | β οΈ POTENTIALLY VULNERABLE |
| CVE-2025-5678 | 7.5 High | log4j 2.x | β
2.17 Safe | β
2.17 Safe | β
NOT AFFECTED |
| CVE-2025-9012 | 5.3 Medium | derby 10.x | β
10.16 Safe | NOT CHECKED | β
NOT AFFECTED || CVE | Severity | Component | Codebase | Installed | Verdict |
|----------------|--------------|---------------|-----------------|----------------|--------------------------|
| CVE-2025-1234 | 9.8 Critical | activemq 5.x | β οΈ 5.15 Vuln | β
5.16 Safe | β οΈ POTENTIALLY VULNERABLE |
| CVE-2025-5678 | 7.5 High | log4j 2.x | β
2.17 Safe | β
2.17 Safe | β
NOT AFFECTED |
| CVE-2025-9012 | 5.3 Medium | derby 10.x | β
10.16 Safe | NOT CHECKED | β
NOT AFFECTED |What used to be 45β60 minutes of sequential manual work is now a single command and a wait of however long the slowest SSH hop takes.
Key takeaways
- Map your dependencies first. Before designing an agent workflow, draw the dependency graph. Anything with no upstream dependency can run in parallel.
- The Monitor Agent pattern scales naturally. One agent per work unit, all spawned simultaneously, results collected and combined at the end. Works for CVEs, for code reviews, for research fan-outs.
- Subagent descriptions are specifications, not prompts. Name tools explicitly. Embed all values. Define the output format. Pre-empt wrong interpretations.
- Shell commands in subagents need explicit tool names. This is the one that will catch you β a subagent will not infer
execute_commandfrom "run this script." Tell it exactly which tool to call and with exactly what arguments. - Collect preferences before spawning. Ask for SSH credentials, options, and flags once in the parent agent. Bake the answers into each subagent's description at spawn time. Don't make subagents ask questions.