
Frida Stalker Android
- 102 installs
- 6 repo stars
- Updated February 13, 2026
- yfe404/frida-stalker-skills
Helps with ai & agent building tasks during AI-assisted development.
About
frida-stalker-android is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- frida-stalker-android
- AI & Agent Building
- AI-coding skill
Frida Stalker Android by the numbers
- 102 all-time installs (skills.sh)
- +5 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #4,307 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yfe404/frida-stalker-skills --skill frida-stalker-androidAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 102 |
|---|---|
| repo stars | ★ 6 |
| Last updated | February 13, 2026 |
| Repository | yfe404/frida-stalker-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Frida Stalker (Android)
Overview
Use this skill when you need to trace native code execution on Android using Frida's Stalker API, with templates geared for ARM/ARM64 and performance-safe defaults.
This skill assumes Frida 17+ JavaScript semantics.
When To Use This Skill
- User explicitly asks for "Frida Stalker" on Android.
- You need to measure native call activity (who got called, how often) with low overhead.
- You need ordered call events (call graph reconstruction) or coarse coverage.
- You need to inject logic on basic-block compilation through
transform(iterator).
Quick Decision Guide
- Want call counts per target and don't care about ordering: use
onCallSummary. - Want ordered call/ret/block/compile events: use
onReceiveand decode withStalker.parse(). - Want instruction-level matching and callouts: use
transform(iterator)(heavy; do it narrowly). - Want to watch a small set of call targets: consider
Stalker.addCallProbe().
Core API Facts (Frida 17+)
Stalker.follow([threadId, options])- Provide exactly one callback:
onReceive(events)oronCallSummary(summary). Stalker.unfollow([threadId])Stalker.parse(events, { annotate, stringify })Stalker.flush()drains buffered events early (otherwise periodic draining is controlled byStalker.queueDrainInterval).Stalker.garbageCollect()should be called afterunfollow()to free accumulated memory at a safe point.Stalker.exclude({ base, size })excludes a memory range from stalking (useful to skip noisy/system modules).- Tuning:
Stalker.trustThresholddefault1(set-1for no trust,0to trust immediately, orNto trust afterNexecutions).Stalker.queueCapacitydefault16384events.Stalker.queueDrainIntervaldefault250ms (set0to disable periodic draining and callStalker.flush()manually).
Workflow
1. Define objective. 2. Choose thread(s). 3. Choose capture mode and filters. 4. Pick a template and adapt it. 5. Run, tune performance, and clean up.
If you are using the Frida MCP tools, also enable $frida-mcp-workflow and follow its phases (Idea -> Scripting -> Execution -> Notes).
Templates
Start from these and keep scripts file-based.
templates/stalker-call-summary.js: low-overhead call counting viaonCallSummary.templates/stalker-onreceive-parse.js: receive binary events and decode withStalker.parse().templates/stalker-start-stop-around-hook.js: follow/unfollow only during a specific hooked function call.templates/stalker-call-probe.js: observe calls to a single target viaStalker.addCallProbe().templates/stalker-transform-skeleton.js: minimaltransform(iterator)skeleton with ARM/ARM64 safety check.templates/stalker-filter-modules.js: helper to select "app modules" on Android and exclude the rest.
Quick Start
1. If you do not know the thread id yet, start with templates/stalker-start-stop-around-hook.js. 2. If you already know the thread id and want low overhead, use templates/stalker-call-summary.js. 3. If you need ordered events, use templates/stalker-onreceive-parse.js and keep event types narrow. 4. If you only care about calls to one target, start with templates/stalker-call-probe.js.
MCP Usage Notes (If Available)
When driving this through the Frida MCP tools, prefer this flow:
1. Create or attach a session (mcp__frida__create_interactive_session / mcp__frida__attach_to_process). 2. Load the selected template with mcp__frida__load_script. 3. Start tracing through RPC exports using mcp__frida__call_rpc_export (templates expose start() / stop() when appropriate). 4. Use mcp__frida__get_session_messages to consume output.
Keep a script ledger (what is loaded, purpose, and teardown path). This is enforced by $frida-mcp-workflow.
Android-Specific Notes (Practical)
- Thread choice matters more than you think.
- If you start stalking the wrong thread, you will see nothing, or only system noise.
- A safe pattern is to start stalking from inside an
Interceptor.attach()callback, usingProcess.getCurrentThreadId()to capture the thread that is actually executing your target function.
- Module filtering is essential.
- On Android, app code is usually in modules whose
pathcontains/data/app/,/data/data/, or an extracted APK split path. - Exclude common noise sources (
libart.so,libc.so,liblog.so, etc.) usingStalker.exclude()when you only care about your app's own native libs.
- 32-bit ARM note.
- If you use raw addresses on 32-bit ARM, Thumb functions require the low bit set. Prefer addresses returned by Frida APIs like
Process.getModuleByName(...).getExportByName(...).
- Avoid
Process.runOnThread()unless you know what you're doing. - It can interrupt a thread in non-reentrant code and cause deadlocks/crashes.
Performance Rules Of Thumb
- Avoid
events.execunless you truly need instruction-level traces. It produces huge volumes of data. - Prefer
onCallSummaryoveronReceivewhen you can. - Keep your callbacks lean; push heavy work to the host side when possible.
- Use
Stalker.exclude()aggressively to reduce time spent in system libraries. - Prefer manual draining (
Stalker.queueDrainInterval = 0+Stalker.flush()) when you need deterministic windows. - Call
Stalker.garbageCollect()after unfollowing, especially if you repeatedly start/stop.
Cleanup Checklist
Stalker.unfollow(threadId)Stalker.flush()Stalker.garbageCollect()
Troubleshooting
- No output at all.
- You are likely stalking the wrong thread, or your callback isn't being invoked (e.g., you followed a thread that never runs).
- Output is only system noise.
- Add module filters and exclusions. Start stalking from inside a hook where you know you're in app code.
- Target slows to a crawl or dies.
- Reduce enabled events, stop using
exec, and switch toonCallSummary. Exclude large/noisy modules.
For deeper notes, see:
references/stalker-api.mdreferences/android-filtering.md
# Editor / OS
.DS_Store
.idea/
.vscode/
*~
*.swp
# Logs / dumps
*.log
*.tmp
interface:
display_name: "Frida Stalker (Android)"
short_description: "Android native tracing with Frida Stalker"
default_prompt: "Use $frida-stalker-android to trace a target thread/module on Android using Frida Stalker."
Frida Stalker (Android) agent skill
An installable agent skill for tracing Android native code with Frida Stalker (Frida 17+): call summaries, event parsing, transforms, module filtering, and performance-safe start/stop patterns.
What's in here
SKILL.md: the skill definition (YAML frontmatter + instructions)templates/: script templates the agent can adaptreferences/: API + Android notes used by the agent while editing templates
Install (npx skills)
This repo follows the open Agent Skills format and is installable with the skills CLI.
List what’s installable from this repo:
npx skills add yfe404/frida-stalker-skills --listInstall globally to Codex (recommended):
npx skills add yfe404/frida-stalker-skills --skill '*' -g -a codex -yInstall into the current project only:
npx skills add yfe404/frida-stalker-skills --skill '*' -a codex -yVerify installation:
npx skills list -g -a codexTroubleshooting install errors
- If you see YAML errors about
descriptiontype, ensureSKILL.mdfrontmatter has a string description (quoted), not a YAML list.
Android Filtering Notes (Stalker)
Stalker gets unusable quickly if you trace "everything". Filtering is the difference between a usable trace and a dead app.
Identify "App Code" Modules
Heuristics that often work on Android:
- App native libs typically live under paths containing
/data/app/or/data/data/. - System libs typically live under
/system/,/apex/,/vendor/.
The templates/stalker-filter-modules.js template contains a practical getAppModules() helper.
Exclude Noise First
If your goal is "what did my app do", exclude obvious noise modules before you follow:
libart.so(ART runtime)libc.soliblog.solibdl.so
This is not universal; always validate which modules show up in your traces and adjust.
Start Stalking From The Right Thread
Avoid guessing thread ids.
Better pattern:
- Hook a native function that you know is executed in the thread of interest.
- Inside
onEnter, callProcess.getCurrentThreadId()and start stalking that thread. - Stop stalking in
onLeaveusingunfollow+flush+garbageCollect.
See templates/stalker-start-stop-around-hook.js.
Frida Stalker API Notes (Frida 17+)
Use this as a compact refresher while writing/patching Stalker scripts.
Follow / Unfollow Lifecycle
Stalker.follow([threadId, options])- Provide
options.eventsto enable specific event kinds. - Provide exactly one callback:
onReceive(events): receives a binary blob (one or more GumEvent structs).onCallSummary(summary): receives a key-value mapping of call target to call count for the current time window.
Stalker.unfollow([threadId]): stop stalking a thread.Stalker.flush(): drain buffered events now.Stalker.garbageCollect(): free accumulated memory at a safe point afterunfollow().
Practical stop pattern:
Stalker.unfollow(tid);
Stalker.flush();
Stalker.garbageCollect();Events
Typical event knobs used in options.events:
call: call instructionsret: return instructionsexec: every instruction (very high volume)block: basic block executedcompile: basic block compiled (useful for coverage-like workflows)
Parsing onReceive Buffers
Stalker.parse(events[, options]) parses the buffer.
Useful parse options:
annotate: true: include event type infostringify: true: pointer values as strings instead ofNativePointerobjects (less overhead if you're sending the result to the host)
Example:
onReceive(events) {
const decoded = Stalker.parse(events, { annotate: true, stringify: true });
send({ type: "stalker:events", decoded });
}Excluding Ranges
Stalker.exclude(range) excludes a { base, size } range.
Practical use:
- Exclude system libraries to reduce noise and overhead.
- Excluding means Stalker won't follow execution "inside" that range, but you can still see calls into it and returns back.
Example:
const libc = Process.getModuleByName("libc.so");
Stalker.exclude({ base: libc.base, size: libc.size });Performance Knobs
Stalker.trustThreshold-1: no trust (slow)0: trust code immediatelyN: trust after N executions
Stalker.queueCapacity: max queued events (default 16384).Stalker.queueDrainInterval: ms between periodic drains (default 250).- Set drain interval to
0to disable periodic draining and callStalker.flush()manually.
transform(iterator) (Advanced)
If you provide transform(iterator), it is called synchronously when Stalker recompiles a basic block.
Rules of thumb:
- Always call
iterator.keep()for instructions you want to keep. - Not calling
keep()drops the instruction (allows replacement, but can break correctness). - On ARM/ARM64, be careful with exclusive store sequences.
- A safe gating heuristic is to only emit callouts when
iterator.memoryAccess === "open".
Call Probes
Stalker.addCallProbe(address, callback[, data]) calls callback synchronously when a call is made to address.
- Returns an id; remove later with
Stalker.removeCallProbe(id). - For performance,
callbackmay be a native function pointer implemented usingCModule.
'use strict';
/*
* Call-probe template: observe calls to a specific target address with low overhead.
*
* This is useful when you only care about a small set of call targets and do not
* need full thread stalking.
*/
const TARGET_MODULE = 'libc.so';
const TARGET_EXPORT = 'open'; // TODO: change to your target
const target = Process.getModuleByName(TARGET_MODULE).getExportByName(TARGET_EXPORT);
send({ type: 'call-probe:installed', target: `${TARGET_MODULE}!${TARGET_EXPORT}`, address: target.toString() });
let callCount = 0;
const maxCalls = 5000;
function tryReadArgs(args, n) {
const out = [];
for (let i = 0; i < n; i++) {
try {
out.push(args[i].toString());
} catch (_) {
out.push(null);
}
}
return out;
}
const probeId = Stalker.addCallProbe(target, function (args) {
callCount++;
if (callCount > maxCalls) return;
send({
type: 'call-probe:hit',
tid: Process.getCurrentThreadId(),
address: target.toString(),
args: tryReadArgs(args, 6),
});
});
rpc.exports = {
remove() {
Stalker.removeCallProbe(probeId);
send({ type: 'call-probe:removed', probeId });
},
};
'use strict';
/*
* Low-overhead Stalker template for Android.
*
* What you get: call counts per target, over time windows.
* What you DON'T get: ordering of calls.
*
* Tip: Keep the callback cheap. Symbolication can be done host-side.
*/
const defaultConfig = {
symbolicate: false,
trustThreshold: 1,
queueCapacity: 16384,
queueDrainInterval: 250,
excludeModules: [
'libart.so',
'libc.so',
'liblog.so',
'libdl.so',
],
};
const state = {
following: new Set(),
config: { ...defaultConfig },
};
function applyConfig(cfg) {
if (cfg.trustThreshold !== undefined) Stalker.trustThreshold = cfg.trustThreshold;
if (cfg.queueCapacity !== undefined) Stalker.queueCapacity = cfg.queueCapacity;
if (cfg.queueDrainInterval !== undefined) Stalker.queueDrainInterval = cfg.queueDrainInterval;
}
function excludeConfiguredModules(cfg) {
for (const name of (cfg.excludeModules || [])) {
try {
const m = Process.getModuleByName(name);
Stalker.exclude({ base: m.base, size: m.size });
} catch (_) {
// Module not present, ignore.
}
}
}
function maybeSymbolicateSummary(summary, enabled) {
if (!enabled) return summary;
const out = {};
for (const [target, count] of Object.entries(summary)) {
try {
const sym = DebugSymbol.fromAddress(ptr(target)).toString();
out[sym] = count;
} catch (_) {
out[target] = count;
}
}
return out;
}
function start(threadId, config) {
const tid = Number(threadId);
if (!Number.isFinite(tid)) throw new Error('start(threadId): threadId must be a number');
if (state.following.has(tid)) return;
const cfg = { ...state.config, ...(config || {}) };
applyConfig(cfg);
excludeConfiguredModules(cfg);
Stalker.follow(tid, {
events: {
call: true,
ret: false,
exec: false,
block: false,
compile: false,
},
onCallSummary(summary) {
send({
type: 'stalker:call-summary',
tid,
summary: maybeSymbolicateSummary(summary, cfg.symbolicate),
});
},
});
state.following.add(tid);
state.config = cfg;
}
function stop(threadId) {
const tid = Number(threadId);
if (!Number.isFinite(tid)) throw new Error('stop(threadId): threadId must be a number');
if (!state.following.has(tid)) return;
Stalker.unfollow(tid);
Stalker.flush();
Stalker.garbageCollect();
state.following.delete(tid);
}
function status() {
return {
frida: Frida.version,
arch: Process.arch,
platform: Process.platform,
following: Array.from(state.following.values()),
config: state.config,
};
}
rpc.exports = {
start,
stop,
status,
};
send({ type: 'frida-stalker-android:ready', ...status() });
'use strict';
/*
* Helper functions for identifying app modules on Android.
*
* This is deliberately heuristic. Always print and validate on your target.
*/
function isProbablyAppModule(m) {
const p = String(m.path || '').toLowerCase();
if (p.includes('/data/app/')) return true;
if (p.includes('/data/data/')) return true;
// Some apps load libs from unpacked or relocated locations.
if (p.includes('/data/')) {
if (p.includes('/data/dalvik-cache/')) return false;
return true;
}
return false;
}
function getAppModules() {
return Process.enumerateModules().filter(isProbablyAppModule);
}
function toRange(m) {
return { base: m.base, size: m.size, name: m.name, path: m.path };
}
function excludeModulesByName(names) {
for (const name of names) {
try {
const m = Process.getModuleByName(name);
Stalker.exclude({ base: m.base, size: m.size });
} catch (_) {
// ignore
}
}
}
rpc.exports = {
getappmodules() {
return getAppModules().map(toRange);
},
excludemodules(names) {
excludeModulesByName(names || []);
},
};
'use strict';
/*
* Stalker template using onReceive() + Stalker.parse().
*
* Use this when you need ordered events and can afford more overhead.
*/
const defaultConfig = {
trustThreshold: 1,
queueCapacity: 16384,
queueDrainInterval: 250,
parse: {
annotate: true,
stringify: true,
},
maxDecodedEvents: 2000,
};
const state = {
following: new Set(),
config: { ...defaultConfig },
};
function applyConfig(cfg) {
if (cfg.trustThreshold !== undefined) Stalker.trustThreshold = cfg.trustThreshold;
if (cfg.queueCapacity !== undefined) Stalker.queueCapacity = cfg.queueCapacity;
if (cfg.queueDrainInterval !== undefined) Stalker.queueDrainInterval = cfg.queueDrainInterval;
}
function start(threadId, config) {
const tid = Number(threadId);
if (!Number.isFinite(tid)) throw new Error('start(threadId): threadId must be a number');
if (state.following.has(tid)) return;
const cfg = { ...state.config, ...(config || {}) };
applyConfig(cfg);
Stalker.follow(tid, {
events: {
call: true,
ret: true,
exec: false,
block: false,
compile: false,
},
onReceive(events) {
let decoded;
try {
decoded = Stalker.parse(events, cfg.parse);
if (Array.isArray(decoded) && cfg.maxDecodedEvents > 0 && decoded.length > cfg.maxDecodedEvents) {
decoded = decoded.slice(0, cfg.maxDecodedEvents);
}
} catch (e) {
send({ type: 'stalker:parse-error', tid, error: String(e) });
return;
}
send({ type: 'stalker:events', tid, decoded });
},
});
state.following.add(tid);
state.config = cfg;
}
function stop(threadId) {
const tid = Number(threadId);
if (!Number.isFinite(tid)) throw new Error('stop(threadId): threadId must be a number');
if (!state.following.has(tid)) return;
Stalker.unfollow(tid);
Stalker.flush();
Stalker.garbageCollect();
state.following.delete(tid);
}
rpc.exports = { start, stop };
send({ type: 'frida-stalker-android:onreceive-ready', frida: Frida.version, arch: Process.arch });
'use strict';
/*
* Start Stalker only while a specific native function executes.
*
* This pattern is the most practical way to stalk the "right" thread
* without guessing thread ids.
*/
const TARGET_MODULE = 'libc.so';
const TARGET_EXPORT = 'open'; // TODO: change to your target
const cfg = {
trustThreshold: 1,
queueCapacity: 16384,
queueDrainInterval: 0, // manual windows: drain on stop
excludeModules: [
'libart.so',
'libc.so',
'liblog.so',
'libdl.so',
],
};
const depthByTid = new Map();
function applyConfig() {
Stalker.trustThreshold = cfg.trustThreshold;
Stalker.queueCapacity = cfg.queueCapacity;
Stalker.queueDrainInterval = cfg.queueDrainInterval;
}
function excludeNoise() {
for (const name of cfg.excludeModules) {
try {
const m = Process.getModuleByName(name);
Stalker.exclude({ base: m.base, size: m.size });
} catch (_) {
// ignore
}
}
}
function startForCurrentThread() {
const tid = Process.getCurrentThreadId();
const depth = (depthByTid.get(tid) || 0) + 1;
depthByTid.set(tid, depth);
if (depth !== 1) return tid;
applyConfig();
excludeNoise();
Stalker.follow(tid, {
events: { call: true, ret: false, exec: false, block: false, compile: false },
onCallSummary(summary) {
send({ type: 'stalker:call-summary', tid, summary });
},
});
return tid;
}
function stopForThread(tid) {
const depth = (depthByTid.get(tid) || 0) - 1;
if (depth > 0) {
depthByTid.set(tid, depth);
return;
}
depthByTid.delete(tid);
Stalker.unfollow(tid);
Stalker.flush(); // triggers summary window drain
Stalker.garbageCollect();
}
const target = Process.getModuleByName(TARGET_MODULE).getExportByName(TARGET_EXPORT);
send({ type: 'hook:installed', target: `${TARGET_MODULE}!${TARGET_EXPORT}`, address: target.toString() });
Interceptor.attach(target, {
onEnter(args) {
this._stalkerTid = startForCurrentThread();
},
onLeave(retval) {
stopForThread(this._stalkerTid);
},
});
'use strict';
/*
* Minimal transform(iterator) skeleton.
*
* Warning: This is advanced and easy to break. Keep it narrow, and filter to
* app code ranges. Consider doing call summary/onReceive first.
*/
const defaultCfg = {
trustThreshold: 1,
queueCapacity: 16384,
queueDrainInterval: 250,
};
function applyConfig(cfg) {
Stalker.trustThreshold = cfg.trustThreshold;
Stalker.queueCapacity = cfg.queueCapacity;
Stalker.queueDrainInterval = cfg.queueDrainInterval;
}
function start(threadId, appRange) {
const tid = Number(threadId);
if (!Number.isFinite(tid)) throw new Error('start(threadId, appRange): bad threadId');
if (!appRange || !appRange.base || !appRange.size) throw new Error('start(...): appRange must have base/size');
const appStart = ptr(appRange.base);
const appEnd = appStart.add(ptr(appRange.size));
applyConfig(defaultCfg);
Stalker.follow(tid, {
transform(iterator) {
let insn = iterator.next();
/*
* On ARM/ARM64, exclusive store sequences are fragile; only emit noisy
* code/callouts when memory access is "open".
*/
const canEmitNoisyCode = (iterator.memoryAccess === 'open');
do {
const pc = insn.address;
const isAppCode = pc.compare(appStart) >= 0 && pc.compare(appEnd) < 0;
if (isAppCode && canEmitNoisyCode) {
// Example: emit a callout on every "ret" inside app code.
if (insn.mnemonic === 'ret') {
iterator.putCallout(onRet);
}
}
iterator.keep();
} while ((insn = iterator.next()) !== null);
},
});
}
function stop(threadId) {
const tid = Number(threadId);
if (!Number.isFinite(tid)) throw new Error('stop(threadId): bad threadId');
Stalker.unfollow(tid);
Stalker.flush();
Stalker.garbageCollect();
}
function onRet(context) {
send({
type: 'stalker:ret',
pc: context.pc.toString(),
sp: context.sp ? context.sp.toString() : undefined,
});
}
rpc.exports = { start, stop };