Reviewing code with a system of agents is becoming common, and the question I find interesting about it is not whether a swarm of reviewers finds more bugs than a single reviewer does. It is how the swarm fails. There are two ways. The agents can look past something entirely, and the step that turns their separate findings into one verdict can wave through a file that should have failed. Finding bugs makes a good demo. Knowing where a detector you built goes blind is harder, and more useful.
To have something concrete to work against, I built a small consensus reviewer that runs. Two agents read the same syntax tree, each with its own job, and a verifier turns their findings into a single PASS or REJECT. The agents here are deterministic AST passes standing in for what a production system would run as separate models. The point is the shape of the pipeline and the way it fails, not parity with a real one. It uses the standard library only, no API key, and it is accurate enough to be worth attacking. The code is at github.com/ghostintheprompt/blind_spot_lab.
The blind spot a text match never sees
Start with the baseline, since a lot of code review still comes down to a pattern matched over source text. Here is a reasonable grep-style reviewer run against a file that contains two command injections.
$ python3 naive_regex_review.py fixtures/command_injection.py
REJECT
L18 REGEX-CMD possible command injection
It found one. The hit is os.popen("tar czf /tmp/out.tgz " + user_path), a command built with string concatenation, the classic smell sitting right in the text. The one it missed is three lines up:
os.system(f"ping -c 1 {user_host}")
Same vulnerability, same class, CWE-78. The f-string has no + for a text pattern to catch, so the reviewer walks straight past it. This is not a contrived case. It is how a lot of adversarial code, and a lot of machine-generated code, clears text-based review. The dangerous construct gets shaped so the matcher was never looking at it.
The alternative is to stop matching text and read structure instead. The reviewer parses the file into a syntax tree and hands it to an agent that reasons about what a call actually does. The command-injection rule is about ten lines:
sink = self._resolve_shell_sink(node, name, root)
if sink and node.args:
module, func = sink
shell_true = any(
kw.arg == "shell" and isinstance(kw.value, ast.Constant) and kw.value.value is True
for kw in node.keywords
)
dynamic = not _is_constant_command(node.args[0])
if module == "os" and dynamic:
self._add("CTX-CMD-INJECTION", "CRITICAL", "CWE-78", node,
f"os.{func}() runs a non-literal command string (injectable)")
It never looks for the text os.system(. It checks whether a shell sink receives a command that is not a pure literal. The f-string is an ast.JoinedStr, so _is_constant_command returns False and the rule fires. Both injections come back, each marked CRITICAL. The same gap matters more as agents start reviewing model-generated code, which the OWASP Top 10 for LLM Applications files under improper output handling. A reviewer that works by matching text gives the evasion away for free.