Ask an LLM to write a Frida script and you get something that looks right. The shape is convincing: Interceptor.attach, Java.perform, a callback with onEnter and onLeave. Then you run it. The method name is wrong, the parameter order is invented, and the SSL pinning bypass it "wrote from scratch" is a worse version of one that's been on Frida CodeShare for five years.
The model isn't dumb. It just has nothing to stand on. It's recalling a blurry average of every Frida snippet it saw during training, with no way to check a signature or look at a real example. Two failure modes fall out of that: it hallucinates APIs, and it reinvents solved problems badly.
ichioda/frodo is an MCP server that fixes both by giving the model the one thing it's missing, which is the source. It indexes ~670 real scripts from Frida CodeShare and the complete Frida JavaScript API, then serves them as search tools. The model stops guessing because it can look.
The two things a model gets wrong
Hallucinated APIs come from the gap between "I've seen this function" and "I know its exact signature." Module.findExportByName takes a module name and a symbol name. Or was it getExportByName? Does Memory.scan return a promise? What's the third argument to Interceptor.attach? A model trained on a mix of Frida versions averages all of it into something plausible and usually wrong.
Reinventing solved problems is the other half. SSL pinning bypass, root detection bypass, anti-debug: none of this is new. The community has battle-tested scripts targeting OkHttp, Conscrypt, TrustKit, RootBeer, every common library. A model writing one from memory gives you a generic sketch that misses the libraries the target actually uses. The good version already exists. The model just can't see it.
Both problems have the same root: no retrieval. So give it retrieval.
Indexing the corpus
Frida CodeShare has no public API, but the frida CLI itself does. Read the frida-tools source and the endpoint it uses to fetch a script by slug turns up:
GET https://codeshare.frida.re/api/project/{author}/{slug}/
frodo walks the paginated /browse pages to find every @author/slug, then pulls each script's source through that endpoint with a worker pool. About 670 scripts land in a local SQLite database: SSL pinning bypass, crypto hooking, network interception, class enumeration, the whole catalog of real techniques people actually run.
Enrichment: turning code into intent
Raw scripts are searchable by their code, but that's not how you think about a task. You don't search for checkServerTrusted. You search for "bypass SSL pinning on Android." The code and the intent are different vocabularies.
So frodo runs every script through a cheap LLM (Gemini 3.1 Flash Lite over OpenRouter) once, at index time, to pull out structured metadata:
{
"summary": "Hooks the Conscrypt TrustManagerImpl to disable certificate validation, allowing HTTPS interception with Burp Suite.",
"category": "ssl-pinning-bypass",
"use_cases": ["Intercept API traffic in Android banking apps", "..."],
"api_calls": ["Java.perform", "Java.use", "Interceptor.attach"],
"difficulty": "beginner",
"quality": 7
}This is the inverse of the gRPC argument I've made before. There, human readability was wasted overhead between machines. Here, machine-readable code is the wrong format for a human-shaped query, so we generate the readable layer on purpose. The enrichment turns a wall of JavaScript into something you can find by describing what you want to do.
The API contract
Examples tell the model what's possible. They don't tell it the exact signature. For that, frodo indexes the official Frida API from its TypeScript definitions (frida-gum, frida-java-bridge, frida-objc-bridge), parsed straight from the .d.ts files with a brace-balanced scanner. Each namespace and class becomes its own searchable entry carrying the real types and the JSDoc:
declare namespace Interceptor {
/**
* Intercepts calls to function/instruction at `target`.
* @param target Address of function/instruction to intercept.
* @param callbacksOrProbe Callbacks or instruction-level probe callback.
*/
function attach(
target: NativePointerValue,
callbacksOrProbe: InvocationListenerCallbacks | InstructionProbeCallback,
data?: NativePointerValue,
): InvocationListener;
}The .d.ts is the contract. It isn't a guess about what attach looks like. It's what it is, with exact parameter types and order. When the model needs to use an API, it pulls the definition instead of recalling a fuzzy average.
Search by what you want, not what you typed
Everything lives in one SQLite database with an FTS5 full-text index that covers the enriched summary, category, and extracted API calls, not just the raw code. That's the part that makes search work by intent. A query like "decrypt aes traffic android" matches a script whose summary describes exactly that, even if those words never show up in its code.
The server exposes a handful of MCP tools: search_scripts (find examples and docs by intent, with platform and category filters), get_api_doc (pull exact signatures), get_script (full source by id), and list_categories. Sub-millisecond, fully offline at query time, single Go binary.
Try it without installing anything
A public, read-only instance runs at https://frodo.udp.re/mcp over Streamable HTTP, no auth. Point any MCP client at it:
{
"mcpServers": {
"frodo": { "url": "https://frodo.udp.re/mcp" }
}
}It deploys as a Vercel Go function with the read-only SQLite database embedded in the binary, so there's no external service at query time. It's the same sub-millisecond search, just hosted.
The workflow
The tools alone don't teach a model how to use them well, so frodo ships an MCP prompt: a portable skill that travels with the server and gives every client the same loop:
- Find examples first. Search by intent. Don't write from memory.
- Read the best match. Pull the full source of a proven script.
- Confirm the API. Look up exact signatures before using anything.
- Adapt, don't reinvent. Modify a working script instead of starting blank.
Search grounds the approach in something that actually ran. get_api_doc grounds the syntax in the real contract. The model's job shrinks from "invent correct Frida" to "adapt a known-good script with verified APIs," which is a much smaller and much safer task.
What it looks like in practice
The real value shows up when you compose. A pentest setup often needs three things at once before the app will even talk: SSL unpinning, root-detection bypass, and emulator-detection spoofing. Three separate search_scripts calls surface the proven building blocks (the exact method names of com.scottyab.rootbeer.RootBeer from h4rithd's quality-9 RootBeer bypass, the device-spoof property map and su/magisk/qemu paths from fdciabdul's emulator script, the OkHttp + Conscrypt + SSLContext layering from a third) and the model stitches them into one grounded script instead of inventing class names:
Composed bypass: SSL unpin + root detection + emulator spoof (click to expand)
Not a single CodeShare script. Composed from the real class names, method signatures, and property maps frodo returned across the ssl-pinning-bypass and root-detection-bypass categories:
/*
* Universal pre-instrumentation bypass: SSL unpinning + root-detection
* bypass + emulator-detection spoofing. For authorized testing only.
*
* frida -U -f <package> -l universal-bypass.js
*/
setImmediate(function () {
Java.perform(function () {
const log = (m) => console.log("[bypass] " + m);
const hook = (name, fn) => {
try { fn(); log("ok " + name); }
catch (e) { log("skip " + name + ": " + e.message); }
};
// 1. SSL unpinning — OkHttp + Conscrypt + permissive SSLContext
hook("OkHttp CertificatePinner", function () {
const CP = Java.use("okhttp3.CertificatePinner");
CP.check.overload("java.lang.String", "java.util.List").implementation = function () {};
});
hook("Conscrypt TrustManagerImpl", function () {
const TM = Java.use("com.android.org.conscrypt.TrustManagerImpl");
TM.verifyChain.implementation = function (chain, anchors, host) {
log("trust " + host);
return chain;
};
});
hook("SSLContext TrustManager", function () {
const X509TM = Java.use("javax.net.ssl.X509TrustManager");
const SSLContext = Java.use("javax.net.ssl.SSLContext");
const TM = Java.use("javax.net.ssl.TrustManager");
const Permissive = Java.registerClass({
name: "re.udp.PermissiveTM",
implements: [X509TM],
methods: {
checkClientTrusted: function () {},
checkServerTrusted: function () {},
getAcceptedIssuers: function () {
return Java.array("java.security.cert.X509Certificate", []);
}
}
});
const tms = Java.array("javax.net.ssl.TrustManager", [Java.cast(Permissive.$new(), TM)]);
SSLContext.init.overload(
"[Ljavax.net.ssl.KeyManager;",
"[Ljavax.net.ssl.TrustManager;",
"java.security.SecureRandom"
).implementation = function (km, _ignored, sr) {
return this.init(km, tms, sr);
};
});
// 2. Root detection — RootBeer methods + su/magisk file probes
hook("RootBeer", function () {
const RootBeer = Java.use("com.scottyab.rootbeer.RootBeer");
[
"isRooted", "isRootedWithBusyBoxCheck", "checkForSuBinary",
"checkForMagiskBinary", "checkForBusyBoxBinary", "checkForDangerousProps",
"checkForRWPaths", "detectTestKeys", "checkForRootNative"
].forEach((m) => {
if (RootBeer[m]) RootBeer[m].implementation = function () { return false; };
});
});
hook("File.exists probes", function () {
const File = Java.use("java.io.File");
const blocked = ["su", "magisk", "busybox", "/dev/qemu_pipe", "qemu-props", "frida-server"];
File.exists.implementation = function () {
const path = this.getAbsolutePath();
if (blocked.some((b) => path.indexOf(b) !== -1)) { log("hide " + path); return false; }
return this.exists();
};
});
// 3. Emulator spoof — Build fields + system properties of a real device
hook("Build fields", function () {
const Build = Java.use("android.os.Build");
Build.FINGERPRINT.value = "samsung/starltexx/starlte:10/QP1A.190711.020/G960FXXSDFUG5:user/release-keys";
Build.MODEL.value = "SM-G960F";
Build.MANUFACTURER.value = "samsung";
Build.BRAND.value = "samsung";
Build.DEVICE.value = "starlte";
Build.PRODUCT.value = "starltexx";
Build.HARDWARE.value = "samsungexynos9810";
Build.TAGS.value = "release-keys";
});
hook("SystemProperties.get", function () {
const SP = Java.use("android.os.SystemProperties");
const spoof = {
"ro.kernel.qemu": "0",
"ro.secure": "1",
"ro.debuggable": "0",
"ro.build.tags": "release-keys",
"ro.hardware": "samsungexynos9810"
};
SP.get.overload("java.lang.String").implementation = function (key) {
return key in spoof ? spoof[key] : this.get(key);
};
});
log("universal bypass ready");
});
})Every class name, method, and spoofed property in that script came from a real, battle-tested CodeShare entry: RootBeer's checkForMagiskBinary, the samsungexynos9810 fingerprint, the Conscrypt verifyChain signature. A model writing from memory would invent half of them. Here it didn't have to. It retrieved the pieces, checked the names, and assembled them.
Why this works
The bet here is the one retrieval-augmented generation makes everywhere: a model with access to the right context beats a bigger model guessing. You don't need it to have memorized the Conscrypt bypass. You need it to find the one that exists, read it, and check the three API calls it touches.
It also keeps the knowledge fresh without retraining. New scripts on CodeShare, a new Frida API version, just re-index and the model sees them. Nothing is baked into weights.
How the dataset was built
The corpus is a four-stage pipeline, all of it reproducible from the CLI:
- Scrape. CodeShare has no public list endpoint, so frodo walks the paginated
/browsepages to enumerate every@author/slug, then pulls each script's source through the sameGET /api/project/{author}/{slug}/endpoint thefridaCLI uses: a worker pool fetching ~670 real scripts. A GitHub code-search pass widens the net. - Enrich. Each script goes through a cheap LLM (Gemini 3.1 Flash Lite over OpenRouter) once, at index time, to pull out the structured metadata (summary, category, use cases, exact API calls, difficulty, quality score) that makes intent-search possible.
- Index the API. The official Frida TypeScript definitions (
frida-gum,frida-java-bridge,frida-objc-bridge) are parsed with a brace-balanced scanner into 235 entries, one per class/namespace/method, carrying real signatures and JSDoc. - Export. Everything lands in one SQLite database with an FTS5 index, then exports to Parquet for publishing.
The result is 904 entries, 669 enriched CodeShare scripts plus 235 API-reference entries, published on Hugging Face as ichioda/frodo in three configs (full / enriched / raw):
from datasets import load_dataset
ds = load_dataset("ichioda/frodo", split="train")The same grounding the server uses works outside it too: for retrieval-augmented generation, for fine-tuning or evaluating code models on instrumentation tasks, or for studying the shape of real mobile-security tooling.
Limitations
A bad example retrieved confidently is still a bad example, which is why every script carries a quality score and the workflow says read before you trust. The enrichment is model-generated and sometimes wrong. And the API contract is only as current as the .d.ts files upstream.
It's also scoped to a purpose. Frida is for security research, debugging, and analysis on systems you own or are authorized to test. frodo is a tool for doing that work faster, not for pointing it somewhere it shouldn't go.
The point
A model writing Frida from memory is doing the hardest version of the job: reconstruct a correct script and a correct API from a blurry average of training data, with no way to check either. That's where the hallucinations and reinvented wheels come from.
Give it the source, real scripts to adapt and exact signatures to verify, and the job becomes tractable. Retrieval beats recall. Not because the model got smarter, but because it stopped having to guess.
Repo: github.com/ichioda/frodo · Hosted MCP: frodo.udp.re/mcp · Dataset: huggingface.co/datasets/ichioda/frodo