DEV Community

Cover image for AI Agents as Security Auditors: How LLMs Found 7 Real Cryptography Bugs in Cloudflare's CIRCL (And What Every Developer Should Build Next)
Manoranjan Rajguru
Manoranjan Rajguru

Posted on

AI Agents as Security Auditors: How LLMs Found 7 Real Cryptography Bugs in Cloudflare's CIRCL (And What Every Developer Should Build Next)

AI Agents as Security Auditors: How LLMs Found 7 Real Cryptography Bugs in Cloudflare's CIRCL (And What Every Developer Should Build Next)

Published: July 8, 2026 · 18 min read

AI robot examining cryptographic code on a holographic screen


Table of Contents

  1. The Bug That AI Found First
  2. The zkSecurity Experiment: Architecture & Setup
  3. The 7 Bugs Dissected — What AI Saw That Humans Missed
  4. Building Your Own LLM Security Audit Pipeline
  5. The "Skills" Architecture: Encoding Expert Knowledge into Prompts
  6. Why AI Severity Ratings Fail (And How to Compensate)
  7. The Better Models, Worse Tools Problem
  8. Multi-Model Review Chains: The New Production Standard
  9. Limitations, Pitfalls, and Honest Caveats
  10. The Future: Continuous AI Security Coverage
  11. Conclusion — Your Next Step

The Bug That AI Found First

Here is a one-line excerpt from Cloudflare's CIRCL library — a widely used, expert-reviewed, production cryptography codebase:

// tss/rsa/rsa_threshold.go
xi := int64(math.Pow(float64(x), float64(i)))
Enter fullscreen mode Exit fullscreen mode

This single line performs polynomial evaluation for threshold RSA secret sharing. The coefficients are big.Int. But the exponentiation slips through float64 — a type with only 53 bits of mantissa. For any player count above ~20, x^i silently overflows and rounds before the cast back to integer. The key shares generated are wrong. The protocol is broken.

A human expert could catch this. But teams at Cloudflare — who do deep cryptography for a living — did not catch it before this code shipped. An AI agent did.

On July 7th, 2026, zkSecurity published a detailed post documenting how their AI audit pipeline — powered by Claude Opus 4.6 and GPT-5.3 with expert-crafted "skills" — discovered 7 confirmed, non-trivial security vulnerabilities in Cloudflare's CIRCL library. All 7 are now patched. Some earned HackerOne bounties.

This is not a demo. This is not a cherry-picked toy example. This is LLM agents finding real bugs in real production cryptography, running on the frontier of what's now possible with LLM agents security auditing.

If you build software — especially software that touches cryptography, authentication, or any security-sensitive path — this post is your field guide to understanding what happened, why it worked, and how to apply these techniques in your own engineering practice.


The zkSecurity Experiment: Architecture & Setup

Architecture diagram of the LLM security audit pipeline

zkSecurity ran their experiment in two configurations against Cloudflare's CIRCL:

Mode 1: Raw LLM + Simple Prompt

"Review this file for security vulnerabilities."
Enter fullscreen mode Exit fullscreen mode

Plain, unstructured. The model reviews the code and produces whatever it finds.

Mode 2: LLM + Skills
Expert-authored "skill" modules encode specific vulnerability classes, reasoning patterns, and red flags that experienced cryptography auditors look for. These are injected as structured context before the code review begins.

The difference in output quality between the two modes is significant — Mode 2 found more bugs, fewer false positives, and produced more actionable reports. We'll dig into the Skills architecture in depth below.

After running both configurations, the team also ran zkao — their proprietary AI audit agent — over the same codebase. zkao not only found all 7 bugs the other runs had identified, but also caught additional complexity-level issues that simpler configurations missed entirely.

The Human-in-the-Loop Layer

One critical architectural note that zkSecurity emphasizes, and which every developer building on top of this pattern should internalize: AI produces candidate findings; humans produce trustworthy reports.

The AI is fast and cheap at generating a broad set of hypotheses. But each candidate finding still needs a human to:

  1. Validate exploitability (is this actually reachable?)
  2. Minimize the proof-of-concept (can we reproduce this?)
  3. Assess deployment-context risk (does the affected code path matter?)
  4. Handle responsible disclosure

Eliminating that human step entirely remains an open problem. The goal of systems like zkao is to minimize the human effort per confirmed finding — not to remove it.


The 7 Bugs Dissected — What AI Saw That Humans Missed

Let's walk through all seven confirmed vulnerabilities. The code is real. The fixes are committed. This is the highest-signal way to understand what AI-powered security auditing can do — and where its reasoning is surprising.

Bug 1: Float64 Precision Loss in RSA Threshold Signing (Low)

// Buggy code — tss/rsa/rsa_threshold.go
xi := int64(math.Pow(float64(x), float64(i)))
Enter fullscreen mode Exit fullscreen mode

A big.Int polynomial is evaluated with float64 exponentiation. float64 has a 53-bit mantissa (~15 decimal digits). For player counts above ~20, values like 100^26 = 10^52 overflow this mantissa by 36 orders of magnitude. The result is silently rounded before the cast back to integer. Key shares become wrong.

The fix: Horner's method evaluation kept entirely in big.Int. The codebase's own TODO comment suggested this approach.

What's interesting here: The AI rated this Critical. Cloudflare confirmed it as Low — because the specific parameter combinations required to trigger it are unlikely in practice. This is our first hint at the severity-calibration problem we'll explore below.

Bug 2: DLEQ Proof Forgery via Prover-Controlled Security Parameter (Low)

// Buggy code — zk/qndleq
type Proof struct {
    Z, C     *big.Int
    SecParam uint     // ← attacker controls this!
}
// During verification, challenge recomputed using proof's OWN SecParam
Enter fullscreen mode Exit fullscreen mode

The security parameter governing challenge bit-length lived inside the Proof struct — which the prover controls. Setting SecParam = 1 collapses soundness to a coin flip. The fix is structural: SecParam is removed from Proof and passed explicitly by the verifier.

Bug 3: BLS Aggregate Verification Without Message Distinctness (High)

This is the one the AI underrated — from Medium to High. The classic rogue key attack applies when aggregating BLS signatures without checking that all messages are distinct:

// Buggy: verifyAggregate checked pairing equation but NOT message distinctness
func VerifyAggregate(pks []PublicKey, msgs [][]byte, sig Signature) bool {
    // Missing: assert all msgs are distinct
    return checkPairingEquation(pks, msgs, sig)
}
Enter fullscreen mode Exit fullscreen mode

An adversary who sees victim public key pk_v and message m can register pk_a = g^sk_a - pk_v and forge an aggregate signature over (pk_v, m) and (pk_a, m) without knowing the victim's secret key.

Why did AI call it Medium? It correctly identified the missing check and even named the rogue key attack — but then anchored on "the caller is supposed to enforce distinctness per the spec," treating that as a mitigation. Context-free code analysis misses deployment risk.

Bug 4: DLEQ Soundness Break via FillBytes Sign Collision (Low — but stunning)

This is the most intellectually striking find in the batch. It requires reasoning across two independent layers simultaneously:

// The attack: present an honest proof π for statement S1 = (g, gx, h, hx)
// but pair it with the FORGED statement S2 = (g, -gx, h, hx)

gxNeg := new(big.Int).Neg(gx)  // -gx, attacker needs no knowledge of x
forgedAccepted := proof.Verify(g, gxNeg, h, hx, N)  // ACCEPTED!
Enter fullscreen mode Exit fullscreen mode

Why does this work? Two things align:

Layer 1 — Algebra: (-gx)^c mod N = (-1)^c * gx^c mod N. When c is even, (-1)^c = 1 and the attacker gets the same intermediate values as the honest prover.

Layer 2 — Serialization: The challenge is hashed using FillBytes, which writes the absolute value of a big.Int and strips the sign. So hash(-gx) == hash(gx).

Neither layer is wrong in isolation. Together they break soundness for roughly 50% of all honestly generated proofs. The fix adds a checkBounds step: all inputs must satisfy 0 < x < N, which rejects negative inputs.

This is the kind of cross-boundary reasoning that makes LLM security auditing genuinely surprising. A focused human reviewer might check the algebra or check the serialization, but the leap between them takes a mental context-switch that's easy to skip.

Moving from subtle algebraic interaction bugs to a classic language trap:

The first four bugs required reasoning about cryptographic algebra, serialization semantics, and prover-verifier contracts. The next one is simpler on the surface — but no less impactful.

Bug 5: HPKE PSK Validation Bypassed by Bitwise-OR Switch (Medium — Duplicate)

A classic Go footgun: case a | b: in a switch statement is a single case whose value is the bitwise-OR of a and b, not two separate cases.

// Buggy — hpke/util.go
switch mode {
case modeBase | modeAuth:    // == 0x02, matches ONLY modeAuth (0x02)
    // modeBase (0x00) never matches
case modePSK | modeAuthPSK:  // == 0x03, matches ONLY modeAuthPSK (0x03)
    // modePSK (0x01) matches NO case at all!
    // PSK validation is silently skipped for modePSK
}
Enter fullscreen mode Exit fullscreen mode

SetupPSK(..., nil, nil) proceeds with an empty PSK instead of being rejected. The fix: comma-separated cases (case modePSK, modeAuthPSK:). This was confirmed as a duplicate of an independently filed report.

Bug 6: Lagrange Coefficients Computed in int64 (Medium)

Two independent bugs in one finding — and both in computeLambda:

// Buggy — tss/rsa/rsa_threshold.go
num := int64(1)
den := int64(1)
for _, s := range S {
    jprime := int64(s.Index)
    if jprime == j { continue }
    num *= i - jprime  // ← silently overflows int64 for ~21+ players
    den *= j - jprime
}
// Bug 2: division BEFORE multiplication by delta — truncates incorrectly
lambda.Div(big.NewInt(num), big.NewInt(den))
lambda.Mul(delta, &lambda)
Enter fullscreen mode Exit fullscreen mode

Bug A (overflow): With ~21 players, products exceed int64 ceiling (~9.2×10¹⁸) and wrap silently. No panic. Wrong coefficients.

Bug B (truncation order): Shoup's scheme guarantees δ × num is divisible by den — but num alone may not be. Computing num/den first, then multiplying by δ, truncates the result for non-consecutive share indices (the normal case).

The fix: move all arithmetic to big.Int and reorder so δ × num / den is computed left-to-right.

Bug 7: CP-ABE Access Control Break via AND-Share Bug (Critical)

This is the crown jewel — a critical vulnerability that zkao found on its own, without human-authored skills:

In Ciphertext-Policy Attribute-Based Encryption, access control is defined by a policy tree. AND nodes split secret shares among their children. A one-line off-by-one in the AND-share distribution meant that certain policy structures would always evaluate as satisfied, regardless of the user's actual attributes. An attacker without the required attributes could decrypt ciphertext they should never have access to — a complete access control break.

The commit diff tells the story clearly: the fix is a single-line correction to the child-share index offset. This is the kind of subtle logic error that lives in implementation details far from the mathematical specification, and that requires tracking invariants across the full policy evaluation tree to spot.


Building Your Own LLM Security Audit Pipeline

The zkSecurity experiment is compelling, but the patterns are replicable. Here's a concrete starting architecture for your own LLM security audit pipeline using Python and the Anthropic SDK:

import anthropic
import os
from pathlib import Path
from typing import Optional

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])

def load_skill(skill_path: str) -> str:
    """Load an expert skill module from disk."""
    return Path(skill_path).read_text()

def audit_file(
    filepath: str,
    skills: list[str],
    model: str = "claude-opus-4-6",
    max_tokens: int = 8096,
) -> dict:
    """
    Run an LLM security audit on a single source file.

    Returns a dict with:
      - candidate_findings: list of potential vulnerabilities
      - severity_estimates: AI-rated severity for each finding
      - reasoning: the model's chain-of-thought per finding
    """
    code = Path(filepath).read_text()

    # Build system prompt: core auditor identity + injected skills
    skill_context = "\n\n---\n\n".join(skills)
    system_prompt = f"""You are a senior cryptography security auditor with deep expertise 
in detecting subtle vulnerabilities. Your goal is to identify real, exploitable bugs — 
not theoretical issues or style concerns.

## Specialist Knowledge

{skill_context}

## Output Format
For each finding, output:
- FINDING: One-line description
- FILE/LINE: Location in code
- SEVERITY: Critical / High / Medium / Low
- EXPLOIT: Brief description of how this is exploitable
- FIX: Recommended remediation
- CONFIDENCE: High / Medium / Low (your confidence this is a real bug)

Only report findings where CONFIDENCE >= Medium. Prioritize precision over recall.
"""

    user_message = f"""Audit the following source file for security vulnerabilities.
Focus on: integer overflows, precision loss, incorrect type usage, 
missing validation, protocol implementation errors, and logical access control bugs.

Enter fullscreen mode Exit fullscreen mode

{code}

"""

    response = client.messages.create(
        model=model,
        max_tokens=max_tokens,
        system=system_prompt,
        messages=[{"role": "user", "content": user_message}],
    )

    return {
        "filepath": filepath,
        "model": model,
        "raw_response": response.content[0].text,
        "input_tokens": response.usage.input_tokens,
        "output_tokens": response.usage.output_tokens,
    }


def audit_repository(
    repo_path: str,
    file_extensions: list[str],
    skill_paths: list[str],
    model: str = "claude-opus-4-6",
) -> list[dict]:
    """
    Walk a repository, audit each matching file, collect findings.
    """
    skills = [load_skill(p) for p in skill_paths]
    results = []

    for ext in file_extensions:
        for filepath in Path(repo_path).rglob(f"*{ext}"):
            print(f"Auditing: {filepath}")
            result = audit_file(str(filepath), skills, model)
            results.append(result)

    return results


# Example usage
if __name__ == "__main__":
    findings = audit_repository(
        repo_path="./circl",
        file_extensions=[".go"],
        skill_paths=[
            "./skills/integer_overflow.md",
            "./skills/cryptographic_protocols.md",
            "./skills/go_footguns.md",
        ],
    )

    for f in findings:
        print(f"\n{'='*60}")
        print(f"File: {f['filepath']}")
        print(f"Tokens used: {f['input_tokens']} in / {f['output_tokens']} out")
        print(f"\n{f['raw_response']}")
Enter fullscreen mode Exit fullscreen mode

This is a straightforward starting point, but three critical engineering decisions will determine whether your pipeline produces signal or noise:

  1. Skills quality over prompt length — A 500-token, precisely written skill beats a 5000-token generic security prompt every time.
  2. File chunking strategy — Large files need intelligent splitting that preserves semantic context (keep functions together; don't split mid-struct).
  3. Deduplication and ranking — Multiple audit passes on the same code produce overlapping findings; build a dedup layer before human review.

The "Skills" Architecture: Encoding Expert Knowledge into Prompts

The single biggest differentiator in zkSecurity's pipeline is the Skills abstraction. Rather than a monolithic prompt, skills are modular, expert-authored knowledge modules that encode specific vulnerability classes. Here's what a real skill document looks like:

# Skill: Integer Overflow in Cryptographic Arithmetic

## What to Look For
- Native integer types (int, int32, int64, uint) used in arithmetic that 
  may involve large player counts, coordinate values, or field elements
- Implicit conversions from big.Int or arbitrary-precision types to 
  bounded types (int64, float64, uint32)
- Multiplication chains where intermediate values may overflow before 
  reduction

## Red Flag Patterns (Go)
Enter fullscreen mode Exit fullscreen mode


go
// DANGEROUS: float64 used in crypto arithmetic
xi := int64(math.Pow(float64(x), float64(i)))

// DANGEROUS: int64 accumulator in product loop
num := int64(1)
for _, s := range participants { num *= s.Index }

// DANGEROUS: implicit truncation in big.Int division order
result.Div(big.NewInt(num), big.NewInt(den))
result.Mul(bigDelta, result) // should multiply BEFORE dividing


## Correct Patterns
Enter fullscreen mode Exit fullscreen mode


go
// SAFE: Horner's method entirely in big.Int
result := new(big.Int)
for i := degree; i >= 0; i-- {
result.Mul(result, x)
result.Add(result, coefficients[i])
}

// SAFE: multiply before dividing to preserve exact divisibility
result.Mul(delta, num)
result.Div(result, den)


## Severity Guidance
- Any precision loss in key generation or secret sharing: Critical/High
- Precision loss in signature verification: Medium (harder to exploit directly)
- In test code only: Low
Enter fullscreen mode Exit fullscreen mode


markdown

This skill structure gives the model:

  • What pattern to look for (conceptual description)
  • Concrete red-flag code (few-shot examples of the bug)
  • Correct patterns (contrast anchors)
  • Severity calibration guidance (reduces the miscalibration problem)

Build a skill library covering: integer/float precision, serialization sign-stripping, access control logic, hash input canonicalization, parameter injection via user-controlled structs, and language-specific footguns (Go switch-case, Rust integer wrapping in release mode, Python integer promotion, etc.).


Why AI Severity Ratings Fail (And How to Compensate)

Severity rating comparison chart

The zkSecurity experiment exposed a systematic pattern in AI severity miscalibration that every practitioner should understand:

Bug AI Severity Confirmed Severity Direction
Float64 precision in TSS/RSA Critical Low Over-rated
DLEQ SecParam injection High Low Over-rated
BLS missing distinctness check Medium High Under-rated
FillBytes sign collision High Low Over-rated
HPKE bitwise-OR switch Medium Medium (Dup) Correct
int64 Lagrange overflow High Medium Over-rated
CP-ABE access-control break Critical Critical Correct

The pattern: AI over-rates bugs that are locally obvious in code (wrong types, clear overflow potential) and under-rates bugs that require understanding deployment context (who calls this? what contracts exist between caller and callee?).

The BLS distinctness bug is the clearest example. The model correctly understood the attack. It even named the rogue key attack by name. But then it anchored on the spec language — "the caller is responsible for ensuring distinctness" — and treated that as a deployed mitigation. It failed to reason: in practice, most callers won't know they need to do this, and CIRCL ships no proof-of-possession infrastructure as a fallback.

Practical Compensations

1. Add deployment-context prompting:

deployment_context = """
This library is used as a dependency by external developers who may not 
have read the full specification. Assume callers may omit steps that 
are documented as "caller's responsibility" unless enforced by the API.
Severity should reflect real-world exploit likelihood, not spec-compliance.
"""
Enter fullscreen mode Exit fullscreen mode

2. Severity override by vulnerability class:
Build a post-processing layer that overrides AI severity for known patterns:

  • Any attack enabling signature forgery without private key → minimum High
  • Any access control bypass (decrypt without attributes) → minimum Critical
  • Any key material exposure → minimum Critical
  • Float precision loss in non-security-critical paths → maximum Medium

3. Cross-model severity consensus:
Run the same finding through two different models and take the higher severity when they disagree. The models tend to miscalibrate in different directions, so this is a cheap source of signal.


The Better Models, Worse Tools Problem

While building LLM pipelines that depend on consistent tool-calling behavior, there's a critical trend every AI engineer needs to internalize: newer frontier models can be measurably worse at using custom tools than their predecessors — and the root cause is a direct side-effect of how RL post-training works.

Armin Ronacher (creator of Flask) documented this on July 4th in a post that's been circulating heavily in the developer community. His AI coding harness Pi uses a nested edits[] array schema for file editing. With older models (Opus 4.5), this worked flawlessly. With Opus 4.8 and Sonnet 5, the model began inventing spurious extra fields at ~20% frequency in agentic contexts:

// What the schema expects:
{
  "oldText": "text to replace",
  "newText": "replacement text"
}

// What Opus 4.8 actually sends (in ~20% of long agentic sessions):
{
  "oldText": "text to replace",
  "newText": "replacement text",
  "requireUnique": true,       // invented  not in schema
  "in_file": "path/to/file"   // invented  not in schema
}
Enter fullscreen mode Exit fullscreen mode

The hypothesis — which is compelling — is that RL post-training optimized Anthropic's newer models specifically against Claude Code's own tool schema. Claude Code uses flat, simple schemas and aggressively tolerates malformed calls with retry loops and silent corrections. Models trained in this environment have a strong prior toward Claude Code's specific schema shapes. A different schema — even a semantically identical one — becomes increasingly off-distribution.

Practical implications for building AI security audit pipelines:

  1. Test your tool schemas against each new model release. Don't assume API compatibility means behavioral compatibility.
  2. Prefer flat schemas. Nested arrays of objects (edits[]) are higher-risk than flat string parameters for schema drift.
  3. Enable strict mode where available. The Anthropic API supports strict tool invocation — it eliminates the extra-field problem in testing, but may have tradeoffs in certain model versions.
  4. Build schema validation middleware. Before passing tool call results into your pipeline, validate them against the expected schema and log anomalies. Don't silently correct — observe.
import jsonschema

EDIT_SCHEMA = {
    "type": "object",
    "properties": {
        "oldText": {"type": "string"},
        "newText": {"type": "string"}
    },
    "required": ["oldText", "newText"],
    "additionalProperties": False  # ← reject invented fields
}

def validate_tool_call(args: dict) -> tuple[bool, list[str]]:
    """
    Validate a model tool call against expected schema.
    Returns (is_valid, list_of_violations).
    """
    try:
        jsonschema.validate(args, EDIT_SCHEMA)
        return True, []
    except jsonschema.ValidationError as e:
        return False, [str(e.message)]
Enter fullscreen mode Exit fullscreen mode

Multi-Model Review Chains: The New Production Standard

Multi-model review chain workflow

One of the most pragmatic engineering patterns emerging from advanced practitioners in 2026 is multi-model cross-review. Simon Willison describes it well: have one model review the work of another. Use Anthropic's best model to review OpenAI's output, and vice versa. The models miscalibrate in different directions, making their disagreements highly informative.

For an LLM security audit pipeline, here's a concrete implementation:

import anthropic
import openai

anthropic_client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
openai_client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"])


def multi_model_audit(
    filepath: str,
    skills: list[str],
    primary_model: str = "claude-opus-4-6",
    review_model: str = "gpt-5",  # OpenAI reviewer
) -> dict:
    """
    Run a two-pass multi-model security audit.
    Pass 1: Primary model (Opus) generates candidate findings.
    Pass 2: Review model (GPT) validates, rejects false positives,
            catches things the primary model missed.
    """
    # ── Pass 1: Primary audit (Anthropic / Claude) ──────────────────────
    primary_result = audit_file(filepath, skills, model=primary_model)

    # ── Pass 2: Cross-model review (OpenAI / GPT) ───────────────────────
    review_prompt = f"""You are a second-opinion security reviewer. 
Another AI model produced the following candidate security findings for this codebase.
Your job is to:

1. CONFIRM findings that are genuinely exploitable
2. REJECT findings that are false positives, explain why
3. ADD any findings the first model missed
4. CORRECT any severity mis-ratings

--- ORIGINAL CODE ---
{Path(filepath).read_text()}

--- CANDIDATE FINDINGS ---
{primary_result['raw_response']}

Provide your validated finding list. Be conservative: only confirm what you 
are confident is exploitable. Precision over recall.
"""

    review_response = openai_client.chat.completions.create(
        model=review_model,
        messages=[
            {
                "role": "system",
                "content": "You are a senior cryptography security auditor. "
                           "Your reviews are precise, conservative, and deployment-aware."
            },
            {"role": "user", "content": review_prompt}
        ],
        max_tokens=4096,
    )

    return {
        "filepath": filepath,
        "primary_findings": primary_result["raw_response"],
        "review_findings": review_response.choices[0].message.content,
        "total_cost_estimate": estimate_cost(primary_result, review_response),
    }


def estimate_cost(primary_result: dict, review_response) -> str:
    """Rough cost estimate for audit transparency."""
    # Claude Opus 4.6: ~$15/M input, $75/M output
    primary_cost = (
        primary_result["input_tokens"] / 1_000_000 * 15 +
        primary_result["output_tokens"] / 1_000_000 * 75
    )
    # GPT-5: approximate pricing
    review_tokens = review_response.usage.total_tokens
    review_cost = review_tokens / 1_000_000 * 30

    total = primary_cost + review_cost
    return f"~${total:.4f}"
Enter fullscreen mode Exit fullscreen mode

In practice, the disagreements between models are as informative as the agreements. When Opus flags something as Critical and GPT calls it Low, that specific tension points toward a severity-calibration issue worth a deeper human look — not a dismissal of the finding.


Limitations, Pitfalls, and Honest Caveats

There is a version of this post that reads like a vendor brochure. This is not that post. Here are the honest limits of LLM agents security auditing as it stands in mid-2026:

1. False Positive Rate is Non-Trivial
zkSecurity reports that their pipeline "produced many candidate findings" for CIRCL — with 7 confirmed true positives. The exact false positive rate is not disclosed. In practice, expect 3–10x as many candidates as confirmed findings even with well-tuned skills. The human review step is not optional overhead; it is load-bearing.

2. AI Cannot Replace Domain Expertise — It Amplifies It
The skills that made Mode 2 so much better than Mode 1 were written by zkSecurity's own expert auditors. The AI is a force-multiplier for human expertise, not a replacement for it. If you don't have cryptography expertise in-house, AI audit tools will help — but they won't substitute for hiring or consulting someone who does.

3. Severity Miscalibration Requires Systematic Compensation
As documented above, AI severity ratings are systematically wrong in predictable directions. Treat them as unreliable and apply post-processing rules anchored in your own deployment context.

4. Context Window Limits Constrain Whole-Program Analysis
The bugs in this experiment were found at the file and function level. Whole-program data flow analysis — tracking how a tainted value propagates across 50 files and 10 abstraction layers — remains out of reach for pure LLM approaches. For that class of vulnerability, static analysis tools (CodeQL, Semgrep, Joern) remain essential companions.

5. Models Change; Pipelines Need Regression Testing
The "Better Models, Worse Tools" problem is real. A pipeline that works well on Opus 4.6 may behave differently on Opus 4.8 due to post-training drift. Build model regression tests into your CI/CD: run a set of known vulnerable code snippets against your pipeline and assert that the findings come back correctly after every model version bump.


The Future: Continuous AI Security Coverage

Despite these limitations — which are real and worth respecting — the trajectory is clear. The constraints above are engineering problems, not fundamental limits. And the pattern that solves most of them is already emerging.

Continuous AI security coverage feedback loop

The most interesting long-term trajectory here is not one-shot auditing — it's continuous coverage.

The fundamental insight from zkao's positioning is that AI security coverage should compound over time. Here's why that matters architecturally:

  • A vulnerability class that models couldn't reason about in January may be fully within their capability by June as models improve and skills libraries expand.
  • New real-world audit findings become new skills, which retroactively improve coverage of previously audited codebases.
  • Changes to your codebase trigger targeted re-audits of affected files, not full re-scans.

Think of it like dependency vulnerability scanning (Dependabot, Snyk) — but for logical implementation flaws, not just known CVEs. The architecture for this looks like:

import hashlib
from datetime import datetime

class ContinuousAuditEngine:
    """
    Maintains a registry of audited files + findings.
    Re-audits files when: code changes, skills update, or model improves.
    """

    def __init__(self, db_path: str):
        self.db_path = db_path
        # In production: use a real DB (Postgres, SQLite, etc.)
        self.audit_registry: dict[str, dict] = {}

    def file_hash(self, filepath: str) -> str:
        return hashlib.sha256(Path(filepath).read_bytes()).hexdigest()

    def skills_hash(self, skill_paths: list[str]) -> str:
        combined = "".join(Path(p).read_text() for p in skill_paths)
        return hashlib.sha256(combined.encode()).hexdigest()

    def needs_reaudit(
        self,
        filepath: str,
        skill_paths: list[str],
        model_version: str,
    ) -> bool:
        """Check if a file needs re-auditing based on what's changed."""
        key = filepath
        if key not in self.audit_registry:
            return True  # Never audited

        record = self.audit_registry[key]

        if record["file_hash"] != self.file_hash(filepath):
            return True  # File changed

        if record["skills_hash"] != self.skills_hash(skill_paths):
            return True  # Skills updated

        if record["model_version"] != model_version:
            return True  # Model upgraded

        return False  # Everything current

    def record_audit(
        self,
        filepath: str,
        skill_paths: list[str],
        model_version: str,
        findings: list[dict],
    ) -> None:
        self.audit_registry[filepath] = {
            "file_hash": self.file_hash(filepath),
            "skills_hash": self.skills_hash(skill_paths),
            "model_version": model_version,
            "audited_at": datetime.utcnow().isoformat(),
            "findings": findings,
        }

    def run_continuous_audit(
        self,
        repo_path: str,
        file_extensions: list[str],
        skill_paths: list[str],
        model_version: str = "claude-opus-4-6",
    ) -> list[dict]:
        """Run audit only on files that need it. Return new/changed findings."""
        new_findings = []
        skills = [load_skill(p) for p in skill_paths]

        for ext in file_extensions:
            for filepath in Path(repo_path).rglob(f"*{ext}"):
                fp = str(filepath)
                if self.needs_reaudit(fp, skill_paths, model_version):
                    print(f"Re-auditing: {fp}")
                    result = audit_file(fp, skills, model=model_version)
                    self.record_audit(fp, skill_paths, model_version, [result])
                    new_findings.append(result)
                else:
                    print(f"Skipping (current): {fp}")

        return new_findings
Enter fullscreen mode Exit fullscreen mode

When combined with a GitHub Actions workflow that triggers on PRs and model version bumps, this gives you a continuously improving security posture without the cost of full re-scans on every commit.


Conclusion — Your Next Step

The zkSecurity experiment is a watershed moment for LLM agents security auditing. Seven confirmed vulnerabilities in Cloudflare's production cryptography library — including a critical access-control break — found by AI agents running on frontier models with expert-crafted skills. All patched. Some bounty-rewarded. Real code. Real impact.

What this tells us, clearly, is that the value is not in "AI replacing security engineers." It's in AI dramatically lowering the cost of the first sweep — the broad, systematic hunt for vulnerability patterns across an entire codebase — so that human expertise can be applied where it's irreplaceable: validating exploitability, assessing deployment-context risk, and handling responsible disclosure.

The architectural patterns are clear:

  • LLM + Skills dramatically outperforms raw LLM prompting
  • Multi-model review chains catch what single models miss
  • Severity calibration post-processing is not optional
  • Continuous coverage compounds value over time
  • Human-in-the-loop remains load-bearing for now

The tooling is accessible today. The Anthropic and OpenAI APIs are in your requirements.txt. The skills library you build over the next three months will be an asset that improves your security posture indefinitely — because every new model release makes it more powerful at zero additional cost.

Start today: audit one file in your most security-sensitive module. Write one skill that captures a known footgun in your language of choice. Run it. See what comes back.

The AI found seven bugs that humans missed. The only question is what it will find in your codebase.


Liked this deep dive? Follow me on dev.to for more technical explorations at the frontier of AI engineering. Have feedback or war stories from building your own audit pipeline? Drop them in the comments — I read every one.


References:

Top comments (0)