We put a fly's sense of smell in a security scanner
The circuit has an extraordinary nose for resemblance. Danger is the one thing it cannot smell.
It spots a warning it has seen before, or a function quietly copied into another project, thousands of times better than chance. Asked whether a piece of code is dangerous, it does no better than a coin.
| What we asked it to do | How it did | Pure guessing gets |
|---|---|---|
| Quiet repeated alerts without silencing a new one | Quieted 96 in every 100, and missed no new ones | nothing to compare |
| Find a snippet’s near-identical twin among 5,000 others | Right first time, 79 times in 100 | 2 times in 10,000 |
| Match a function to its copy in a different project | Right first time, 93 times in 100 | 4 times in 10,000 |
| Tell a real security flaw from the fix that removed it | Right 51 times in 100 | 50 times in 100 |
In plain language
No background needed
The problem
Software gets scanned automatically for security problems. The scanners are noisy. A big company might get thousands of warnings a week, and nearly all of them are the same few harmless things, reported over and over.
So engineers stop reading them. Then the one warning that mattered arrives, and nobody looks.
The usual fix is a mute button. Someone gets sick of a warning and switches that kind off. It works, and it is dangerous, because it switches that kind off everywhere, including the one place it mattered.
What a fly does instead
A fruit fly has the same problem with smells. It cannot react to the same harmless smell forever. It also cannot afford to miss a new one.
Its answer is not a mute button. It gets less and less responsive to that one smell, while staying fully alert to everything else. And if something important happens, its sensitivity comes straight back.
In 2017 researchers worked out how the fly does this, precisely enough to write it down as a recipe. Each smell gets turned into a pattern of a few dozen lit-up cells out of about two thousand. Similar smells light up overlapping patterns. Different smells light up separate ones.
We used that recipe on security warnings instead of smells.
A hardcoded password in a test file is not worth anyone’s time. The same warning about a password in the live payment settings absolutely is. Mute the category and you silence both.
Our version goes quiet on the first and stays loud on the second, because the two warnings light up mostly different patterns.
Did it work?
Yes. It removed about 96 warnings in every 100 while still catching every important one we had hidden in the pile, and it never silenced anything marked serious.
We also ran it over the real history of Django, a big well-known open-source project. Nearly 2,000 real changes, with the “did this matter?” answer taken from what the developers actually did afterwards rather than from anything we invented. It removed 97 in every 100.
Then we got greedy
Going quiet on repeats is one job. The tempting next one is judgement. If it can tell seen it from never seen it, can it tell dangerous from safe?
To find out we needed examples where someone trustworthy had already decided. Django’s own security fixes gave us 186. For each one we had the code while it was still broken, and the same code after the developers fixed it. Two versions of the same thing, one genuinely dangerous, and a human’s word for which was which.
Our system got 51 out of 100 right. A coin gets 50.
It cannot tell the difference at all. And looking back, it was never going to. Code does not look strange just because it is dangerous. Most security holes look perfectly ordinary. That is exactly why they get written.
What it is genuinely brilliant at
So we asked the opposite question, using the very same material. Here is a piece of code before the fix. Can you find the fixed version of it, hidden among five thousand other snippets?
It found the right one first try about 8 times in 10. Guessing would get it right twice in ten thousand.
Then a harder version. Developers often copy a whole library into their own project instead of linking to it, and the two copies slowly drift apart as each gets edited. We took a real case of this and asked our system to match up the functions between the two projects. It got it right first try more than 9 times in 10.
Same system, same code, same afternoon. Thousands of times better than guessing at find the one that matches. No better than a coin at tell me which one is dangerous.
It is a machine for spotting resemblance. It does not form opinions. That explains why the first job worked: have I seen this warning before? is a resemblance question. Is this code dangerous? is an opinion, and resemblance does not contain one.
Where it loses, and we should say so
If the same warning turns up in exactly the same words every time, a much simpler trick, just remembering what you have already seen, beats ours slightly. Ours only pulls ahead when warnings drift: when the code around them gets edited or renamed, so each one looks brand new to the simple method but still looks familiar to ours.
How much real warnings drift is something we have not measured yet. If the answer is “hardly at all,” the simple method wins and this was a scenic detour.
One thing we are careful about
Nothing here is a simulated fly. We borrowed one specific, well-documented trick from an insect’s sense of smell, because it happens to solve a problem that also shows up in software. Everything built on top of that trick is ordinary engineering, and we have tried to be strict about which is which.
It also matters for the obvious next question: would a bigger brain work better? Scientists finished mapping an entire fruit fly brain in 2024, all 139,255 cells of it. The biggest piece of human brain mapped in that kind of detail is about a grain of sand, roughly a millionth of the whole thing.
But the map was never the hard part. We never used the fly’s map. We used one circuit that somebody had already worked out the purpose of, and that explanation is what we could build from. A complete wiring diagram with no explanation attached would not have helped. Our own results show it: the trick we borrowed worked beautifully for the job it was explained for, and was worthless one step outside it.
Three examples
Real code, taken straight from the runs
Percentages are easier to trust when you can see one case. Here is what each of the three results looks like on an actual piece of code.
1. Why it cannot spot a flaw
This is a real Django security fix. The bug let an attacker send a deeply nested shape to a map field and tie up the server. Here is the code before the fix, and after it.
def converter(value, expression, connection):
if value is not None:
geom = GEOSGeometryBase(read(memoryview(value)), geom_class)
if srid:
geom.srid = srid
return geom def converter(value, expression, connection):
if value is not None:
geom = GEOSGeometryBase(
read(memoryview(value), max_geom_collections=None), geom_class
)
if srid:
geom.srid = srid
return geom CVE-2026-15830, django/contrib/gis/db/backends/mysql/operations.py
The whole fix is one argument: max_geom_collections=None. To our system the two
versions light up 66% of the same channels, which is to say they look almost the same. That
is the correct answer to the question it was asked, and the wrong answer to the question we
wanted. Nothing about the shape of the broken version looks unusual, because it is not
unusual. It is ordinary code that happens to be exploitable.
2. Why it is so good at spotting a copy
pip keeps its own copy of a library called packaging. Over time the two copies
drifted apart. This function was rewritten on one side and not the other: the return type
changed, and the body went from handing back results one at a time to collecting them in a
list.
def _cpython_abis(py_version, warn=False) -> Iterator[str]:
version = _version_nodot(py_version[:2])
...
yield f"cp{version}{threading}{debug}{pymalloc}{ucs4}" def _cpython_abis(py_version, warn=False) -> list[str]:
abis = []
version = _version_nodot(py_version[:2])
...
abis.append(f"cp{version}{threading}{debug}{pymalloc}{ucs4}") Asked to find the second one given the first, out of 2,285 candidate functions, it put the right answer first. Its score for the correct match was 0.59. The next-best guess scored 0.25, less than half. Different projects, different file paths, rewritten body, and it still knew.
3. Why it beats a mute button
This is the case that makes the whole thing worth building. The same scanner rule fires twice, on code that is almost word for word the same:
password = "hunter2"
client = PoolClient(user="test", password=password) password = "hunter2"
client = PoolClient(user="svc", password=password) Mute the rule and you have silenced both. Our system gives these two only 8% shared channels, because where a file sits is part of what it encodes. So it goes quiet on the first after a few dismissals and stays loud on the second. In the full test it got every one of these pairs right; rule muting got none of them.
The technical note
For people who build detection and triage systems
Why run this at all
Alert fatigue looks like a duplicate-removal problem, and removing duplicates does not fix it. Exact matching collapses identical findings and nothing else; rule-level muting collapses far too much. The gap between them is where a similarity measure should live, and the fly olfactory circuit is one of the few anyone has worked out in full.
That last point is why this is not an analogy. Dasgupta, Stevens and Navlakha showed in Science (2017) that the circuit is doing locality-sensitive hashing: turning each input into a short code, so that similar inputs get similar codes. Fifty odorant receptor types feed fifty projection neurons, which fan out to about 2,000 Kenyon cells through a sparse binary random matrix, a 40-fold expansion. One inhibitory neuron, APL, then silences all but the highest-firing 5%, and that surviving 5% is the odour’s tag. Similar inputs get similar tags. It beats conventional LSH on nearest-neighbour search, partly because sparse binary projections cost around 20× less than the dense Gaussian ones LSH normally uses.
So there is a real algorithm to borrow, and two questions worth asking of it:
- Can it suppress repeated findings without suppressing new ones?
- Does that same representation also help it detect? Can “unlike anything here” stand in for “dangerous”?
What we expected
Written down before each experiment ran, which is the only thing that makes a prediction worth anything:
| Experiment | Prediction | Outcome |
|---|---|---|
| Suppression vs dedup and rule-muting | Beats both on the combination of volume and recall | held |
| Telling a vulnerability from its fix | At chance, since a hunk is as unusual before a fix as after | held |
| Cross-project clone matching | Worse than same-project, because path features disagree | wrong on the net |
The third is covered below: the mechanism was right and the net effect was not, because the granularity changed at the same time.
How it is put together
Each finding is projected through a fixed random sparse binary matrix to 2,000 channels; the top 32 survive. Similar findings share most of their channels, unrelated ones share almost none, and no taxonomy is maintained anywhere:
| Pair of findings | Shared channels |
|---|---|
| Identical | 1.00 |
| Same finding, file renamed | 0.78 |
| Same finding, one token changed | 0.52 |
| Same rule, code rewritten | 0.10 |
| Same rule, test fixture vs production config | 0.08 |
| Unrelated | 0.00 |
Everything past the encoder is ours, not biology. Each channel carries an inhibition level; a finding’s response is its severity prior scaled by how inhibited its channels are, and below a threshold it is withheld. Inhibition decays with distance measured in pull requests, never wall clock. One design decision matters more than the rest:
Showing a finding habituates nothing. A finding has to be dismissed or ignored for inhibition to build, and one that gets acted on actively relieves it. That is what separates this from time decay, which forgets on a schedule regardless of whether anyone cared.
A property we did not design, and noticed only because a test failed: outcome gating is self-limiting. Once a class stops surfacing, nobody is dismissing it, so reinforcement drops to the much smaller suppressed-item increment and inhibition levels off near 0.61 instead of maxing out. That headroom is load-bearing: at forced saturation (~0.91) a near-miss finding still surfaces at 0.60 against a 0.35 threshold.
Result 1: suppression works
Twelve seeds, 600 pull requests each, against the two things teams actually do:
| Approach | Volume removed | New findings caught | Near-miss split |
|---|---|---|---|
| Exact-match dedup | 93.9% | 100% | 100% |
| Rule-level muting | 97.9% | 49% | 0% |
| Habituation | 95.8% | 100% | 100% |
Zero high-severity findings withheld on any seed. Rule muting, the industry default, buys the most volume by losing half the new findings and every near-miss.
The near-miss case is the one to dwell on: the same rule firing on a hardcoded credential in a test fixture and on one in a production config. Muting the rule silences both. The fly separates them, because file class and path push the two onto largely different channels.
We also ran it over the real history of 1,950 Django commits. The “did this matter?” signal came from what actually happened to each finding, not from labels we invented. There, habituation removed 97.0% of the volume with zero high-severity suppressions, against 91.4% for exact dedup. Only two of the five metrics are measurable there, since an ordinary repository carries no labels for the other three.
Result 2: judgement does not
The second question needed ground truth we did not have. The trick was to stop hunting the commit that created a flaw. Finding those means reaching for SZZ, a family of algorithms that tops out near 40% accuracy. Use the commit that fixed the flaw instead. A security fix says, with a maintainer’s authority, that this code was vulnerable and this code is not. Pre-fix and post-fix versions of the same hunk are a labelled positive and a negative matched on file, project, author, era and style. Nothing but the flaw separates them.
Django tags its fixes Fixed CVE-YYYY-NNNNN, which makes them exactly
identifiable: 186 matched pairs across 77 CVEs in 67 source files. The
analysis is paired, which needs no base rate. For each pair, did the detector rank the
vulnerable side above the fixed one? Chance is 50%.
| Detector | Ranked the vulnerable side higher | Could not separate |
|---|---|---|
| Habituation encoder (the fly) | 51.1% [43.9–58.3], n=182 | 4 pairs |
| Claude Haiku 4.5 | 100% [77.2–100], n=13 | 173 pairs |
The model flagged the vulnerable side 10.2% of the time against 3.2% for the fixed side: real signal, weak, silent on most pairs. A six-line fragment is hard for anyone to judge with no way to check whether untrusted input can actually reach it.
The fly is at chance, with an interval straddling 50%. As predicted, and for the predicted reason: a hunk is about as unusual before a fix as after it.
Results 3 and 4: the tasks it is actually for
So we asked the complementary question on identical data. Given a pre-fix hunk, find its post-fix counterpart among 5,000 spans from the same repository. The pair is a near-duplicate by construction, which makes this similarity search, the exact problem the circuit was shown to solve.
| Metric | Result | Chance |
|---|---|---|
| Correct match at rank 1 | 79.0% [72.6–84.3] | 0.02% |
| Correct match in top 10 | 92.5% | 0.2% |
| Median rank of the true match | 1 | 2,500 |
That is a generous test: same function, same file, same repository. The harder version
uses vendoring. pip carries a copy of the packaging library inside
src/pip/_vendor/. The two copies have since drifted apart by 4 to 69 lines per
module: copied once, then edited separately by each project. That is a near-clone, not an
identical one, which is the harder case. The vendoring is the ground truth.
| Encoder | recall@1 | recall@10 | MRR |
|---|---|---|---|
| Default | 92.9% [90.3–94.8] | 99.2% | 0.953 |
| Path- and repo-blind | 96.0% [93.9–97.3] | 99.6% | 0.975 |
519 queries against 2,285 blocks, chance 0.044%. Run locally.
This is where the prediction failed. We expected cross-project matching to score worse, because the encoder weights directory and repository features and those disagree by construction across two projects. Turning those features off does gain 3.1 points, so the reasoning was right. But the size of the chunks changed too. This test matches whole functions; the earlier one matched arbitrary 13-line windows, and a whole function is far more distinctive. The two headline numbers are therefore not comparable, and the ablation is the only controlled claim here.
The practical consequence is immediate: an encoder aimed at cross-project matching should drop path and repository features. They exist to separate findings within a repository and can only mislead between two.
What it adds up to
Three similarity tasks far above chance. One judgement task exactly at chance. Same encoder throughout, and on the CVE pairs literally the same spans an hour apart.
Stated plainly, this is not a disappointment. It is the organising fact. Suppression works because “is this the same finding again?” is a similarity question. Detection fails because “is this dangerous?” is a judgement, and no amount of channel overlap contains one.
One casualty: different jobs, you want both does not survive. On real code the two detectors flag genuinely disjoint sets, Jaccard overlap 0.000, so the two signals are unrelated. But unrelated is not the same as complementary. Complementarity would need the fly to catch real issues the model misses, and against human labels it catches them at chance. We had written that claim into an earlier draft on the strength of a synthetic benchmark, and it was wrong.
What we are not claiming
- No biological fidelity. The connectome is measured wiring from a dead brain. The encoder is a published algorithm; the inhibition dynamics, the finding-to-channel mapping and the salience signals are ours. Nothing here simulates a fly.
- The advantage over dedup is the drift rate, and nothing else. On byte-identical repeats, dedup wins. We have not measured how much real findings mutate, so we cannot say which regime production is in.
- A severity ceiling needs context-aware severity. On real data a rule-level analyzer marks 87% of findings high or critical, while 265 of 317 sit in test files. The safety ceiling then becomes a switch between doing nothing and hiding thousands of high-severity findings.
- An earlier version of the detection comparison was retracted. It ran on a synthetic stream whose negatives were vulnerability-shaped code labelled benign because of which directory it sat in. Any detector reasoning about code rather than location scored backwards against it. The CVE pairs exist because of that failure.
- We deviate from the biology on sparsity, and measured why. The fly keeps the top 5%, 100 of 2,000 cells. We keep 32, about 1.6%. Sweeping k from 16 to 128 made things steadily worse above about 32. This is the clearest case where copying the circuit more faithfully would have made the tool worse.
Which circuit next
Forward-looking: none of this is measured yet
The obvious reading of “borrow a circuit from a brain” is that the constraint is brain data, and that the frontier runs from insects toward mammals toward us. The wiring diagrams do line up that way. A complete adult fruit fly brain was published in 2024 by the FlyWire consortium: 139,255 neurons and roughly 50 million chemical synapses, sorted in a companion paper into 8,453 cell types. The largest human reconstruction is one cubic millimetre of temporal cortex: about 57,000 cells, 150 million synapses, 1.4 petabytes of imagery, and roughly one-millionth of a human brain.
Why a circuit and not the brain
We did not use the fly connectome, and the reason is not that it was hard to obtain. It is public, and the counts above came from it. The reason is that a wiring diagram is not an algorithm.
A connectome tells you which neuron connects to which, and how strongly. It does not tell you what the network computes, what its inputs mean, or which of the many things it does is the one worth copying. There is no run button on a graph of 139,255 nodes. To build anything from it you would first have to work out the function, and that is both the hard part and the part the data does not contain.
What we used instead was a circuit somebody had already explained. Dasgupta, Stevens and Navlakha reduced the fly’s olfactory pathway to a stated algorithm, sparse random projection followed by winner-take-all, and showed what it computes. That reduction is a few lines of linear algebra. It runs on a laptop, inside a scanner, on every finding, in under a millisecond. A simulation of 139,255 neurons does none of those things, and would need timing, neurotransmitter dynamics and plasticity rules that the wiring diagram does not carry anyway.
Our own results are the strongest form of this argument. The circuit did not fail for want of capacity. It was hundreds of times better than chance at every similarity task we gave it and exactly at chance at judgement, on identical inputs. That gap is a category boundary, not a size limit, and a hundred times more neurons would not turn a resemblance engine into a detector. A different explained circuit might. So the axis that matters is not how much wiring has been mapped. It is how much of it anyone has worked out the purpose of, and that number is far smaller and growing far more slowly.
But the fly brain is playing Doom
It is. In September 2026 Google and the Howard Hughes Medical Institute released MaleCNS, a map of an adult male fly’s brain and nerve cord: about 166,000 neurons and 125 million synapses. Within days an engineer called Alex Wormuth wired it into a simulator and pointed it at Doom.
That is a great hack, and it makes our point for us. Look at what he had to supply that the map does not contain. Which neurons count as input. How a frame of Doom becomes activity on them. Which activity becomes a movement command. A learning signal, which he built by pulsing two dopamine cells whenever the player takes damage. The connectome gave him the graph. He supplied everything that made it mean anything. Six thousand attempts in, it still has not beaten the game.
We went the other way round. Our paper is from 2017, seven years before the first whole fly brain, so it cannot have come from a connectome. The fact it rests on, that the wiring from antennal lobe to mushroom body is random rather than organised, came from Caron, Ruta, Abbott and Axel in 2013, who traced Kenyon cells one at a time. Decades of lab work had already worked out what that pathway does. The 2017 paper spotted that it adds up to locality-sensitive hashing and wrote it down as an algorithm. So what we borrowed was never in a wiring diagram. It was in the literature, because people spent thirty years asking what the circuit is for and not just what it connects to.
A brain yields two separable things, and they come from different work. A map says who connects to whom. A characterisation says what the connected thing computes. Mapping has become fast, industrial and largely automated. Characterising is slow, manual, and done one circuit at a time.
Doom is built on the map. This piece is built on the characterisation, and never needed the map at all. Which is why the interesting number is not how many neurons have been reconstructed. It is how many circuits anyone can currently write down as an algorithm, and that number is still short enough to list.
Provenance: the connectome figures above are read from FlyWire, which publishes the dataset at codex.flywire.ai and its annotations at flyconnectome/flywire_annotations. We used the counts and nothing else. No connectome data enters the encoder, and no result in this piece depends on it.
So the near-term candidates are not bigger brains but other circuits somebody has already worked out, and insects are where most of those are:
- The central complex keeps track of which way the insect is facing, and adds up each step it takes so it can fly straight home. It is a way of holding onto a position over time. The software version is an agent keeping a stable sense of where it is in a long task.
- The mushroom body is the same sparse code we used, with a chemical signal writing “this was good” or “this was bad” onto it. That is learning from a handful of examples. The software version is a tool that improves from a few corrections instead of a retraining run.
- The locust looming detector (LGMD) computes time-to-collision from raw visual flow with two neurons, and has already been built into collision-avoidance hardware. It is the clearest existing proof that this kind of borrowing produces working engineering.
Human cortex is a poor next step for a second reason beyond scale. The fly was solvable partly because every fly is wired almost the same way, so one diagram describes the whole species. Cortex is not like that, so even a complete human connectome would be a map of one person, and working out what the wiring does, already the hard part, gets harder still.
The honest version of the ambition is narrower, and more useful, than “copy a brain.” Find a circuit somebody has already explained. Check whether what it computes is something your problem actually needs. Then expect it to be useless one step outside that job. We got one of those three right on the first try and had to measure our way to the other two.