This document describes how to set up a local development environment for the repository on macOS.
- Python 3+ (use
python3) - Node.js (LTS) and
npmorpnpmfor JS tooling (optional) - A physical device or emulator with
frida-serverfor rooted testing (common for Android) - Frida Gadget only for jailed/non-root scenarios
-
Sync dependencies and install frooky in editable mode:
uv sync
This creates a virtual environment at
.venvwith all dependencies installed. -
Compile the frooky agent:
uv run compile-agent --dev
-
Run the CLI:
Run
frookydirectly usinguv run:uv run frooky --help
Or activate the virtual environment:
source .venv/bin/activate which frookyThe output should be a path within
.venv, typically ending with.venv/bin/frooky.
By default, Frida runs the frooky agent in the QuickJS runtime. --runtime v8 runs it in V8 instead, which also applies to scripts loaded with -l.
--debug switches to V8 and opens a Chrome Inspector server for the frooky agent on port 9229. The server runs on the machine running frooky, not on the device, so no port forwarding is needed:
frooky -U -f org.owasp.mastestapp --debug hooks.yamlOpen chrome://inspect in Chrome. The agent is listed under Remote Target as localhost:9229, with an entry named after the frooky process, e.g. python3.13[12345]. If it isn't listed, add localhost:9229 under Configure.... When frooky runs in the devcontainer, VS Code forwards the port to your machine; check its Ports view if the target doesn't show up.
Click inspect on the entry. If the DevTools window stays blank, use inspect fallback. Alternatively, Open dedicated DevTools for Node connects to localhost:9229 on its own and reconnects after you restart frooky. The DevTools window lets you:
- record CPU profiles in the Performance tab and view them as a flame chart,
- take heap snapshots in the Memory tab,
- evaluate expressions in the Console tab.
Reading and stepping through the agent code in the Sources tab doesn't work yet, see Current Limitations.
Keep in mind:
- The agent is loaded and resolves the hook files before you can connect, so startup is not in a profile. To profile hook resolving, run with
-w, start recording, and save a changed hook file. - A paused breakpoint blocks every app thread that calls a hooked method, which can make the app stop responding.
- V8 compiles hot code to machine code and QuickJS doesn't, so V8 profiles show where the time goes, not how long it takes in the default runtime.
- Only one debugger can listen on port 9229. Stop other
frooky --debugor Node.js debugging sessions first.
These come from Frida (as of Frida 17.18 and frida-compile 19.0.5), not from frooky:
- Messages larger than 512 KiB are cut off. Frida's inspector server truncates every DevTools message at 524,287 bytes, and DevTools then waits forever for the rest. The agent bundle is about 1.3 MB (704 KiB of it is zod, 376 KiB frida-java-bridge), so the Sources tab lists
/src/build/index.frooky.jsbut never finishes loading it. You can't read the code or set breakpoints there. The flame chart still shows the function names; search for them infrooky/agent/src/. - Long CPU recordings break the same way. A profile of a busy agent grows by roughly 8 KiB per second, so keep recordings under about 30 seconds. Idle time adds almost nothing.
- No source maps. frida-compile declares
-S, --no-source-mapswith a default offalse, so it never includes source maps, with or without-S. Even with a source map in the bundle, Frida doesn't pass it on to DevTools, so the TypeScript files wouldn't show up anyway.
A possible workaround on frooky's side is to write frooky's own code (about 115 KiB) and the libraries as separate modules into the one bundle file. Frida's bundle format supports that, and each module then shows up as its own script in DevTools, but frida-compile can't produce it, so build.js would need its own bundling step.
The project consists of two testable components: a Frida agent written in TypeScript and a Python host.
The agent has its own dedicated unit tests that run directly on a target device, as the agent's functionality is tied to the runtime environment Frida operates in.
The host, on the other hand, serves as an integration test for the full application.
The following chapters describe how to write tests and target apps the tests should run against.
Tests usually require a target app which implements the feature that should be tested.
You find them in the folder tests/target-apps, together with instructions how to build them.
On Android, sync_target_apps.py builds the apps and installs the latest build on every attached device. Otherwise, build the app and install it with the Makefile:
cd tests/target-apps/<android|ios>
make installNote
Before proceeding, make sure Frida is available on the target device (Android) or on the local machine (iOS Simulator).
The agent's unit tests are the *.test.ts files next to the code in frooky/agent/src/. They use frida-test (describe, it, expect) and run inside a live app process on a device, so Java.use, Process and Module work in them. They can't run without a device.
Install the Node.js dependencies once, and again after package-lock.json changes:
cd frooky/agent
npm ciThen run the tests from frooky/agent:
npm run test:android # the only attached device, or $ANDROID_SERIAL
npm run test:android -- -s emulator-5554 # one of several devices (serials: scripts/list_android_devices.py)
npm run test:android -- -a # every attached device, one after another- The device needs a running frida-server, which
prepare_android_devices.pysets up. - The tests run in
com.google.android.dialer, or incom.android.dialeron images without the Google dialer, e.g. Android 12.FRIDA_TEST_APP=<package>picks another app. frida-test spawns the app, so it doesn't have to be running. - With several devices attached and neither
-snorANDROID_SERIAL, the script stops and lists the devices. - With
-a, it prints a summary per device and fails if any device failed. - Other arguments go to frida-test, e.g.
npm run test:android -- -a -o out.jsonwritesout-<serial>.jsonper device.npm run test:android -- -s emulator-5554 -t 300lowers the timeout from 600 to 300 seconds.
The script is frooky/agent/scripts/test-android.sh. It runs every test file under src/. To run only some files while iterating, call frida-test directly with paths:
npx frida-test -D emulator-5554 -f com.google.android.dialer ./src/shared/utils.test.ts ./src/android/decodersfrida-test bundles the test files and the agent code they import on its own, so the agent tests don't need npm run build:dev:android first. The host and the integration tests do, because they load dist/agent-android.js.
iOS isn't complete yet and has no agent test script.
Put the test next to the code it tests, e.g. src/shared/utils.test.ts for src/shared/utils.ts. npm run test:android picks it up without registering it anywhere. describe, it and expect are globals, typed through frida-test in tsconfig.json:
import { wildcardPatternToRegExp } from "./utils";
describe("wildcardPatternToRegExp()", () => {
it("matches '*' against exactly one dot-separated segment", () => {
expect(wildcardPatternToRegExp("org.owasp.*.HttpClient").test("org.owasp.network.HttpClient")).toBeTruthy();
});
});The test app is a system app, not one of the target apps. A test that needs a specific Java method or native function belongs in an integration test against a target app.
The Testing view of VS Code lists all tests below tests/. It needs the Python extension, which the devcontainer installs.
| Folder in the view | Tests |
|---|---|
host/unit |
tests/host/unit, no device needed |
host/integration |
tests/host/integration |
agent/unit |
the *.test.ts files in frooky/agent/src/ |
The agent tests stay next to the agent code. The pytest plugin tests/agent/unit/frida_test.py, which the conftest.py in the repository root loads, lists every it() of them as a pytest test, named after its describe() blocks, e.g. Utils > toHex() > returns '0x' for an empty array. The same works on the command line:
uv run pytest tests/agent/unit # all agent tests
uv run pytest tests/agent/unit -k "toHex" # some of them
uv run pytest "frooky/agent/src/shared/utils.test.ts::Utils > toHex() > returns '0x' for an empty array"- All selected agent tests run in one frida-test run through
scripts/test-android.sh, so the device is picked the same way: the only attached one, orANDROID_SERIAL. - frida-test runs whole files. Running a single test runs its file and shows only that test's result.
- The tests are found by reading the source, not by running it: a
describe(orit(has to start its line and have its title on the same line. A title with${...}is one test in the view and passes if every test it creates passes. uv run pytestwithout a path runs everything belowtests/, so the agent tests too.
The scripts in scripts/ set up Android devices and run the tests on one or several of them. Run them with uv run from the repository root. Each one prints its options with --help.
They reach the devices through adb and honor ADB_SERVER_SOCKET, so they also work from the devcontainer, where adb runs on the host. Most of them choose devices like adb does:
-s <serial>picks a device. Forprepare_android_devices.py,sync_target_apps.pyandplatform_check.pyyou can repeat it.- Without
-s, the setup scripts andplatform_check.pywork on every attached device.run_integration_tests.pyuses$ANDROID_SERIAL, else the only attached device, and needs-ato run on all of them.
A typical session with several emulators:
uv run scripts/list_android_devices.py # which serial is which Android version
uv run scripts/prepare_android_devices.py # root and frida-server on every device
uv run scripts/sync_target_apps.py # build and install the target apps everywhere
uv run scripts/run_integration_tests.py -s emulator-5556 -k receive_intPrints the serial, state, Android version, API level, ABI and model of every device that adb devices lists, sorted by API level. Use it to find the serial for -s:
$ uv run scripts/list_android_devices.py
SERIAL STATE ANDROID API ABI MODEL
emulator-5562 device 12 31 arm64-v8a sdk_gphone64_arm64
emulator-5554 device 15 35 arm64-v8a sdk_gphone16k_arm64
emulator-5558 device 17 37 arm64-v8a sdk_gphone16k_arm64Offline or unauthorized devices are listed with their state but without properties.
Sets up each device for frooky:
- Gets root, with
adb rootor elsesu. - Sets SELinux to permissive.
- Installs the newest frida-server with the host frida's major version to
/data/local/tmp/frida-server-<version>. - Stops frida-servers of other versions and starts that one.
- Checks that frida can reach it.
Within a major version, client and server are compatible, and newer servers carry fixes for newer Android versions. Downloads are cached in ~/.cache/frooky/frida-server/. The script is safe to rerun. Run it again after restarting an emulator, because frida-server doesn't survive a restart.
uv run scripts/prepare_android_devices.py # every attached device
uv run scripts/prepare_android_devices.py -s emulator-5554 # one device
uv run scripts/prepare_android_devices.py --frida-version 17.19.0 # a specific frida-server===== emulator-5554 =====
ok connected, Android 15, arm64-v8a
ok adbd runs as root
ok SELinux Permissive
ok /data/local/tmp/frida-server-17.22.0 installed
ok frida-server 17.22.0 running
ok frida connects (access: full)
===== Summary =====
ready emulator-5554Run it when frida reports unable to connect to remote frida-server or Need Gadget to attach on jailed Android, or when the app crashes right after spawn with Agent connection closed unexpectedly.
Builds the Android target apps with the Makefile in tests/target-apps/android. Then, on each device, it compares the SHA-256 of the installed APK with dist/<app>.apk. An app that is missing or differs is uninstalled, so nothing of the old build remains, and installed again. Apps that are up to date are left alone.
uv run scripts/sync_target_apps.py # all apps, all devices
uv run scripts/sync_target_apps.py value-passing-native # one app
uv run scripts/sync_target_apps.py -s emulator-5554 -s emulator-5556
uv run scripts/sync_target_apps.py -c # clean buildBuilds are incremental: they copy the sources over the previous build. Use -c after changing an app's build.gradle.kts.* or AndroidManifest.xml or removing a source file.
Runs the Android integration tests in tests/host/integration/android with pytest. It passes the device to the tests as ANDROID_SERIAL, so frooky, frida, adb and Appium all use the same device. Arguments it doesn't know go to pytest. Without a test path, it runs the whole folder.
uv run scripts/run_integration_tests.py -k receive_int # the only attached device or $ANDROID_SERIAL
uv run scripts/run_integration_tests.py -s emulator-5554 -k receive_int # one of several devices
uv run scripts/run_integration_tests.py -a -x # every device, stop each run at the first failure
uv run scripts/run_integration_tests.py tests/host/integration/android/test_frida_flags.py
uv run scripts/run_integration_tests.py --appium 192.168.1.10 # another Appium server (default port 4723)- With
-a, the tests run once per device, one after another. The script then prints a summary and fails if any device failed. - The Appium server is
--appium <ip>[:<port>], else$APPIUM_URL, else127.0.0.1:4723. The script stops right away if Appium isn't reachable. - The tests need the target apps (
sync_target_apps.py), a running frida-server (prepare_android_devices.py) and a current agent (uv run compile-agent --dev && uv sync). - A failed test saves a screenshot of the device to
test-failures/<test>.pngand, if an app crashed, the crash log totest-failures/<test>.crash.txt. The test's output also shows the crash log and the focused window. In CI, the folder is part of thetest-results-android-api<level>artifact.
Plain pytest works too. With several devices attached, set ANDROID_SERIAL:
ANDROID_SERIAL=emulator-5554 uv run pytest tests/host/integration/android -k receive_intRuns every test suite on the host and on every device and writes a report. See Checking a New Platform.
uv run scripts/platform_check.py # host and every attached device
uv run scripts/platform_check.py -s emulator-5554 --skip unit # one device, without the host unit tests
uv run scripts/platform_check.py --skip agent -k receive_int # only some integration tests
uv run scripts/platform_check.py --adb-host 192.168.1.20 --appium 192.168.1.20 # adb and Appium on another machineThe agent tests aren't in scripts/: run them with npm run test:android in frooky/agent, which takes -- -s <serial> or -- -a the same way (frooky/agent/scripts/test-android.sh).
To check frooky on a host OS, Android version or device it hasn't been tested on, run:
uv run scripts/platform_check.pyIt records the host (OS, architecture, Python, Node, Frida) and each device (Android version, API level, architecture), builds the agent, runs the host unit tests, and then runs the agent tests and the Android integration tests on every attached device, one after another. -s <serial> checks only that device and can be repeated; uv run scripts/list_android_devices.py shows which serial belongs to which Android version and model. The report is written to .platform-check/<timestamp>/report.md with a section per device, together with the test results (<serial>/) and the full logs. Attach the folder to an issue or PR.
- Requires a running
frida-serveron each device:uv run scripts/prepare_android_devices.pyinstalls and starts it. - The integration tests also need Appium (
APPIUM_URL) and the target apps:uv run scripts/sync_target_apps.pybuilds and installs them. Without them, the step is skipped and the report says why. - On a device without the Google dialer, the agent tests run in
com.android.dialer. --skip unit,agent,integrationskips steps,-k <pattern>runs only the matching integration tests.--adb-host <ip>[:<port>]uses the adb server on another machine (default port 5037),--appium <ip>[:<port>]the Appium server (default:$APPIUM_URL, else127.0.0.1:4723), and--out <dir>writes the report elsewhere.
frooky only supports 64-bit app processes (arm64, x86_64). The agent refuses to start in a 32-bit process.
A push runs the tests on Linux with Python 3.14 for the host unit tests, and x86_64 emulators with API 31 to 35 (Android 12, 12L and 13 to 15) for the agent and integration tests. To test the others, run the workflows manually under Actions → Run workflow:
- Verify frooky host: host unit tests on Linux, macOS and Windows, each with Python 3.12 and 3.14.
- Test agent Android and Test host Android: emulators with API 29 and 31 to 36, or the levels given as a comma-separated list, e.g.
36or29, 36.