Continuous fuzzing does not scale on its own. Even projects enrolled in OSS-Fuzz for years hide critical bugs, and the reason is the same every time: somebody has to watch coverage, write harnesses for the code nobody is reaching, and triage the crashes that fall out. GitHub Security Lab has released an open-source pipeline that hands all three of those jobs to an LLM agent. It also warns, in the first screen of its documentation, that it must not be run on a machine you care about.
The Fuzzing Taskflow takes a GitHub owner/repo slug and does the rest: it clones the project, finds entry points, writes AFL++ harnesses, runs campaigns, reads coverage, rewrites harnesses to close gaps, triages crashes, and writes a vulnerability report per unique bug. The interesting part is not that an LLM writes a harness. It is the architecture that keeps the model’s judgement away from execution, and the fact that the whole thing runs afl-fuzz and model-chosen build commands on the host with no container in between.
Key takeaways:
- The design rule is that the agent owns decisions and MCP tools own execution. The agent never invokes AFL or clang directly.
- Time budgets double each iteration, from 30 seconds to roughly 32 minutes per target, and the loop stops on a coverage plateau.
- Crash triage is the part a human researcher used to spend the most time on: minimisation, ASan replay, stack-top hash dedup, and a verdict per crash.
- Suggested patches ship marked “review required.” The agent’s analysis is bounded by how well the model understood the target code, and it does get things wrong.
- Run it in a disposable Codespace or throwaway VM, with no elevated privileges. This is not a theoretical caution.
What it actually does
Written by GitHub Security Lab engineer Antonio Morales and published on September 24, 2026, the Fuzzing Taskflow targets C and C++ projects. It is built on the GitHub Security Lab Taskflow Agent, an open-source framework for LLM-driven security automation, and it ships as taskflows the agent runs end to end.
# Start a codespace from the repo, then:
./scripts/fuzzing/run_fuzzing.sh tukaani-project/xz
# Something small for a smoke test
./scripts/fuzzing/run_fuzzing.sh DaveGamble/cJSON
The argument is a repository slug and nothing else. From there the agent installs its own dependencies, clones the repository, identifies the most relevant functions, and creates fuzz targets for them.
The architecture: decisions and execution, separated
There are three layers:
- A shell driver,
run_fuzzing.sh, that chains the stages together - A set of taskflow YAMLs, one per stage, which are essentially the prompts that define each step
- A set of MCP tools the agent calls to do the actual work: run AFL, compile a harness, store a crash, read a coverage report
The rule Morales says he cared about most is that the agent owns decisions and the tools own execution. The agent decides what to fuzz, what harness to write, and which coverage gap to chase next. It never calls AFL or clang directly; it composes primitives like run_afl_for and compile_harness. All state lives in a SQLite database, fuzz_context.db, so stages exchange data through the database rather than passing it in memory.
One detail worth stealing regardless of what you think of LLM agents: every harness is compiled twice. AFL’s edge instrumentation guides the fuzzer but produces unreadable coverage data, so each harness also gets a coverage build.
# Binary that does the fuzzing
afl-clang-lto -fsanitize=address,undefined harness.c -o harness.afl
# Binary that replays the queue and produces real line/branch coverage
clang -fprofile-instr-generate -fcoverage-mapping harness.c -o harness.cov
The coverage feedback loop
This is the part that automates the most tedious manual work. Each iteration, for each harness, the agent runs AFL for a time budget, replays AFL’s queue against the coverage binary, reads the list of uncovered branches, and picks one of four actions:
- Add a seed crafted to reach an uncovered branch
- Edit the harness source to call an additional API
- Enrich the AFL dictionary with the magic constants a guard is comparing against
- Skip the gap if it is a cold error path or vendor code not worth chasing
The time budget doubles every iteration: 30s, 60s, 120s, 240s, 480s, 960s. Cheap short rounds first when there is obvious coverage to grab, long rounds later when the fuzzer needs time to break a hard guard.
Stopping is decided by plateau detection. Once two consecutive iterations each gain less than a configurable threshold (1% absolute line coverage by default), the loop moves on rather than burning hours for the last fraction of a percent.
Structure-aware input, without hand-written mutators
AFL’s default byte-level mutators work well on binary formats and badly on structured, text-based input. The usual fix is hand-writing a custom mutator per format. This pipeline takes four complementary approaches instead.
- Per-format dictionaries and mutators. For recognised input formats (JSON, XML, regex, PNG, length-prefixed binary), it ships prebuilt AFL dictionaries and
LLVMFuzzerCustomMutatorC files. The JSON mutator does token splicing and balanced-bracket duplication; the XML one knows tags, entities and billion-laughs tokens; the regex one carries real ReDoS patterns. Each delegates half its mutations back to AFL’s default byte mutator so the engine keeps its randomness. - A source-level dictionary. For unrecognised formats it generates a mutator by scanning the target’s own
.cand.hfiles, extracting string literals and 32-bit numeric constants from#define,caseandenum. The intuition is that the magic values a parser checks for are written down somewhere in its own source. - Coverage-driven dictionary enrichment. The same tokens are emitted as an AFL classic dictionary before the first iteration, with numeric constants in both endiannesses so a
memcmpagainst a four-byte magic can be satisfied regardless of host byte order. After each coverage step, guards near the uncovered lines (strncmp,memcmp,case 0xN,== 'X') are inspected and new tokens appended. The dictionary grows toward the code the fuzzer cannot yet reach. - A corpus-splice operator. The mutator can load files from a corpus directory and splice random sub-regions into the input, a recombination move AFL’s stock havoc does poorly.
The corpus persists between campaigns
Every harness gets a stable corpus directory that survives across iterations and across whole campaigns:
<workspace>/corpus/harness_<id>/
At the end of each iteration AFL’s queue is merged into that directory and passed through afl-cmin to keep its size bounded. Last week’s interesting inputs carry into this week’s run, and stopping and restarting a campaign costs you nothing.
Triage is where the real time was going
Finding a crash is half the job. Three stages run automatically once the fuzzing loop finishes.
- Every crash is minimised with
afl-tmin, replayed under AddressSanitizer to capture a stack trace, and deduplicated by a stack-top hash: the top normalised frames, with templates, inline namespaces and LTO suffixes stripped so semantically identical crashes collapse together. - Previously known crashes are replayed against the current binary to see whether an upstream fix has resolved them.
- The agent reads the harness source and the crashing function, walks the call chain back from the public API, and writes a per-crash markdown report.
Each report assigns one of seven verdicts: vulnerability, library_hardening, harness_bug, OOM, timeout, assertion_failure or duplicate. The distinction between a real vulnerability reachable through a public API and a harness_bug that lives in your own test code is exactly the judgement call that used to require tracing the code by hand.
Reports include a root-cause analysis with file:line references, a reachability argument, an exploitability assessment, a suggested fix as a unified diff, and a regression test sketch. The patches are labelled “review required” deliberately. Read them as a prepared starting point for a human, not as a result.
The security warning is the most important paragraph
GitHub’s own post puts this plainly: the taskflow runs afl-fuzz, clang and arbitrary build commands chosen by the LLM directly on the host, with no container in between. A prompt-injected agent could, in principle, do anything your user can. Run it only inside a disposable environment such as a Codespace or a throwaway VM, without elevated privileges.
Think about what that means structurally. You are pointing an agent at a repository you did not write, and the agent decides which build commands to run. The repository content is untrusted input that reaches a model that can emit shell commands. That is the same shape as a supply chain attack with a natural language interface, and it is why the isolation boundary matters more here than the prompt quality.
If you want to run this locally rather than in a codespace, put it behind the kind of boundary described in building a sandboxed AI agent on Ubuntu with Docker: read-only root, dropped capabilities, no network by default, and narrow mounts. Do not give it your SSH keys, your cloud credentials, or a writable path to anything you would miss.
Where the model choice matters
The default model is Claude Sonnet 5, chosen because it passed GitHub’s internal tests without triggering output guardrails. Other frontier models impose safety constraints that get in the way of a tool whose job is generating crash-inducing inputs. The model is configured in src/seclab_taskflows_fuzzing/configs/model_config.yaml.
Worth noting what the launch post does not claim: there is no bug count, no CVE, and no benchmark. GitHub has published separate work describing 24 Android vulnerabilities found using targeted taskflows with its open-source security agent, but this post itself is a tool release, not a results claim.
Try it yourself
The project lives at github.com/GitHubSecurityLab/seclab-taskflows-fuzzing, with the underlying agent framework at seclab-taskflow-agent. It is open source. If you maintain a C or C++ project that has never been fuzzed, this is a reasonable way to start; if yours is already in OSS-Fuzz, the coverage loop is the part likely to find something new. A live dashboard starts automatically on port 8765 and shows per-harness status, coverage trends, a crash heatmap and an iteration timeline.
For the wider problem of pointing autonomous tools at code and credentials they should not reach, see how AI coding agents are changing secure development workflows and how to protect Git repositories from AI agent abuse.
References
- AI-powered fuzzing with the GitHub Security Lab Taskflow Agent — Antonio Morales, GitHub Blog, September 24, 2026
- GitHubSecurityLab/seclab-taskflows-fuzzing — the fuzzing taskflows repository
- GitHubSecurityLab/seclab-taskflow-agent — the Taskflow Agent framework
- How we found 24 Android vulnerabilities using our open source AI security agent — GitHub Blog, September 2026
- AFL++ — the coverage-guided fuzzer the pipeline drives
Related Tools
Try these companion tools on DevStackIO Tools:
- API token generator — generate secure tokens for fuzzing agent authentication and API access.