Hooky is a dynamic analysis tool for mobile application security testing and runtime instrumentation, built on Frida 17.x. Available as a terminal UI (TUI) and a browser-based web UI.
- TUI + Web UI — Textual-based terminal UI and a separate FastAPI + WebSocket SPA accessible in any browser
- Java Hooking — Intercept Java/Kotlin methods with argument capture, blocking wait, continue/modify/drop/override control, and timeout visibility
- ObjC Hooking — Intercept Objective-C methods on iOS targets
- Native Hooking — Hook native functions by address, module+offset, symbol, or
module!symbolviaInterceptor.attach(); optional return value override pre-set at hook creation - Class Discovery — Enumerate all loaded Java/ObjC classes; live filter; click a class to auto-load its methods
- Method Discovery — Enumerate declared methods with signatures; select and hook on demand; Force Call directly from the table
- Return Value Capture — Actual return value streamed back after each hooked call and shown live in History/Monitor; Java objects are expanded with
toString(), class, and readable fields when possible - Force Call — Programmatically invoke Java methods with custom arguments and inline request/response result views
- Complex Object Args — For complex Java parameters, pass a new instance by providing constructor args and optional constructor type hints
- Auto Replace Rules — Persisted rules that automatically replace selected argument values, return values, or both when matching hooked calls arrive
- Manual Calls History — Dedicated history for every Force Call with class, method, full args, result, and static/instance tag in both TUI and Web Monitor
- Multi-hook — Multiple concurrent hooks on a single session, each with independent filters and scope
- Respawn — Kill and re-spawn the target app; all active Java and native hooks re-injected automatically
- Project save/load — Store session fields, active hooks, hooked-method metadata, native hook settings, and auto replace rules in a project JSON
- Pre-compilation — TypeScript agent compiled in background when an app is selected, so injection is instant
- Platform detection — iOS/Android auto-detected on device connect; active package/bundle id and PID shown in session indicators
- File logging — Intercepted calls (JSON) and log messages written to a session log file
- pip install — Installable as a Python package;
hookyandhooky --webcommands available system-wide
Hooky/
├── hooky/ # Python package (pip-installable)
│ ├── __init__.py
│ ├── __main__.py # python -m hooky
│ ├── tui.py # Textual TUI — main app
│ ├── bridge.py # Frida 17.x session bridge + JS agents
│ ├── hooky.tcss # TUI stylesheet
│ └── web/ # Web UI (FastAPI + SPA)
│ ├── __init__.py
│ ├── server.py # FastAPI + WebSocket backend
│ └── static/
│ ├── index.html # Dark-themed SPA
│ └── app.js # WebSocket + REST client
├── pyproject.toml # Package metadata + entry points
├── package.json # npm deps (frida-java-bridge for Compiler)
├── tsconfig.json # TypeScript config for Frida Compiler
├── uv.lock # Locked Python dependencies
└── hooky.bat # Windows launcher shortcut
- Python 3.12+
- Node.js 18+ with npm (required for
frida-java-bridgeTypeScript compilation — Java hooks only) - frida-server 17.x running on the target device
uvpackage manager (or plainpip)
# Editable install (dev)
uv pip install -e .
# Or pip
pip install .
# Install Node.js runtime dependency (frida-java-bridge) — run once
hooky --setup
hooky --setupinstallsfrida-java-bridgeinto~/.hooky/node_modules/. Native and ObjC hooks do not require npm or frida-java-bridge.
adb push frida-server-17.x.y-android-arm64 /data/local/tmp/frida-server
adb shell "chmod 755 /data/local/tmp/frida-server"
adb shell "su -c '/data/local/tmp/frida-server &'"hooky # after pip install
uv run hooky # from source via uv
python -m hooky # alternativeFour top-level pages (tabs in the header):
[ Home ] [ Device & Process ] [ Intercept & Monitor ] [ Logs ]
- PROJECT — load/save/create project files (JSON with session fields, hooks, auto rules)
- OPTIONS — auto-attach, file logging, respawn mode, debug mode
- Continue to Device & App — navigate to device selection
┌─ DEVICES ──────────────┬─ APPLICATIONS ───────────────────────────────────┐
│ Pixel 6a (🤖 Android) │ Search: [____________] [x] Non-system │
│ Local │ [x] Running only │
│ │ Name Package PID │
│ │ MyApp com.example.app 4172 │
│ ├──────────────────────────────────────────────────│
│ │ HOOK SETUP │
│ │ Target: [com.example.app] Mode: [Spawn ▼] │
│ │ Class: [com.example.Auth] (optional) │
│ │ Filters:[login,auth.*] Scope:[All ▼] │
│ ├──────────────────────────────────────────────────│
│ │ [ Attach & Inject ] [ Detach ] │
└────────────────────────┴──────────────────────────────────────────────────┘
Platform icon (🍎 iOS / 🤖 Android) appears in the header subtitle on all pages after device connect.
Inner tabs:
| Tab | Purpose |
|---|---|
| History | All intercepted calls (Java + native); selected pending calls auto-focus; separate return type/value columns; request/return detail panel with Burp-style diff |
| Method Discovery | Enumerate class methods, multi-select, hook on demand, Force Call from table with request/response result view |
| Class Discovery | Enumerate all loaded Java/ObjC classes, live filter, click to load methods instantly |
| Dynamic Hooks | Nested subviews for History, Java, ObjC, and Native hook forms; active hooks and hooked-method metadata |
| Native Libs | Browse loaded modules and their exports; hook any export directly |
| Auto Rules | Automatic argument replacement and return override rules |
| Manual Calls | Full history of every Force Call — class, method, args per-line, response body, static/instance tag |
Dedicated page with full scrollable Frida log output. Copy All and Copy Last buttons.
- Open Home — configure project/options if needed
- Continue to Device & App
- Select device — app list populates; platform detected automatically
- Click an app — Target auto-fills; mode set automatically (running = Attach, not running = Spawn)
- Enter Java class (e.g.
com.example.AuthManager) and optional method filters - Attach & Inject — hook loads; calls appear in History with live return values
- Select a call — detail panel shows args, return value, Burp-style diff
- Choose action: Continue / Modify Args / Override Return / Modify Both. Complex object args can be replaced with a new Java instance by entering constructor args JSON.
- Select device + app, leave Java class blank
- Attach & Inject
- Go to Class Discovery → Enumerate → filter → click a class row (methods auto-load on the right)
- Select a method → Hook Selected Method
- Force Call — opens modal for argument input; result shown as a request/response view and logged to Manual Calls tab
- Respawn — kills + re-spawns with all hooks re-injected from process start
- Hook a Java method, then open Auto Rules
- Configure:
- Arg index / value / type — replace a single argument before call executes
- Return value / type — override the return value
- Add Auto Rule — matching calls handled automatically; marked modified/overridden/mod+override in History
- Attach to a session
- Dynamic Hooks → Native subview
- Fill in address/symbol, label, num args, optional return override
- Add Native Hook — appears in History as
✓ logged(non-blocking)
Address formats:
0xdeadbeef— absolute addresslibnative.so+0x1234— module base + offsetSSL_write— export by name (any module)libssl.so!SSL_write— export in specific module
| Key | Action |
|---|---|
q |
Quit |
r |
Refresh devices |
c |
Clear call history |
d |
Detach session |
X |
Reset All — detach, clear history, hooks, discovery, logs |
| Option | Description |
|---|---|
| Auto-attach on app select | Automatically attaches when clicking an app (requires class to be set) |
| File logging | Writes log lines and intercepted call JSON to the specified file |
| Log file name | Path/filename for the session log (default: hooky_session.log) |
| Respawn app | Kill the running process before spawning — forces fresh start, catches early-init hooks |
| Debug mode | Dumps every raw Frida message to the log panel |
Projects are saved as JSON from the Home page. Default path: hooky_project.json.
{
"version": 2,
"session": {
"target": "com.example.app",
"mode": "spawn",
"class": "",
"filters": "",
"scope": "all"
},
"hooks": [
{
"type": "java",
"target_class": "com.example.AuthManager",
"method_filters": ["login"],
"mode": "all",
"hooked_methods": [
{
"name": "login",
"params": ["java.lang.String", "java.lang.String"],
"return_type": "boolean",
"class_name": "com.example.AuthManager"
}
]
}
],
"auto_rules": [
{
"id": 1,
"class_pattern": "AuthManager",
"method_pattern": "login",
"arg_replacements": [{"index": 1, "value": "secret", "type": "string"}],
"return_override": {"value": true, "type": "boolean"}
}
]
}Browser-based interface. Same Frida bridge, real-time updates via WebSocket.
hooky --web # serve at http://localhost:8000
hooky --web --port 8080 --host 0.0.0.0 # custom host/port
hooky-web # alias| Tab | Purpose |
|---|---|
| Setup | Connect device, browse apps, attach/spawn session, add Java/ObjC/Native hooks |
| Monitor | Live call history table; interactive intercept controls; hooked methods list; Force Call/replay; manual call history |
| Hooks | Active hooks with method filters, hooked-method metadata, remove actions, duplicate hook forms |
| Auto Rules | Add/remove automatic argument replacement and return override rules, matching the terminal Auto Rules workflow |
| Discovery | Enumerate Java classes; click a class to auto-load methods; hook all methods or a selected method; Force Call from method table |
| Native Libs | Enumerate modules and exports; hook any export with one click |
| Logs | Real-time log stream; Copy All / Copy Last / Clear |
The Web Monitor tab can interactively hold Java calls, similar to the terminal History tab:
- Enable Intercept in Monitor.
- Trigger a hooked Java method in the target app.
- Hooky highlights the Monitor tab and shows the number of pending calls in the status bar.
- The newest pending call is auto-selected.
- Choose Continue, Modify Args, Override Return, Modify Both, or Drop.
When a method is pending, the Frida agent waits up to 300 seconds before returning a safe default and marking the call as timed out.
Complex Java object arguments can be replaced with a new instance. For constructor input, provide either a JSON array:
["value", 123]or an object with explicit types for overloaded constructors:
{"class_name":"com.example.Type","args":["value"],"arg_types":["java.lang.String"]}Force Call results appear inline in Monitor/Discovery and are also recorded in the Manual Calls panel.
REST endpoints (all under /api/):
| Method | Path | Description |
|---|---|---|
| GET | /api/devices |
List Frida devices |
| POST | /api/devices/{id}/connect |
Connect to device |
| GET | /api/apps |
List applications on connected device |
| POST | /api/session/spawn |
Spawn and attach |
| POST | /api/session/attach |
Attach to running process |
| POST | /api/session/detach |
Detach session |
| GET | /api/intercept/settings |
Read web intercept toggle |
| POST | /api/intercept/settings |
Enable/disable web intercept mode |
| POST | /api/calls/{call_id}/respond |
Continue, modify, override, modify both, or drop a pending call |
| POST | /api/force-call |
Force-call a Java method |
| GET | /api/auto-rules |
List auto replace rules |
| POST | /api/auto-rules |
Add auto replace rule |
| DELETE | /api/auto-rules/{id} |
Remove auto replace rule |
| DELETE | /api/auto-rules |
Clear auto replace rules |
| GET | /api/hooks |
List active hooks |
| POST | /api/hooks/java |
Add Java hook |
| POST | /api/hooks/objc |
Add ObjC hook |
| POST | /api/hooks/native |
Add Native hook |
| DELETE | /api/hooks/{id} |
Remove hook |
| GET | /api/hooks/methods |
List all hooked methods |
| GET | /api/discovery/classes |
Enumerate Java classes |
| GET | /api/discovery/classes/{name}/methods |
Enumerate class methods |
| GET | /api/discovery/objc/classes |
Enumerate ObjC classes |
| GET | /api/discovery/objc/{name}/methods |
Enumerate ObjC methods |
| GET | /api/discovery/modules |
Enumerate native modules |
| GET | /api/discovery/modules/{name}/exports |
Enumerate module exports |
| POST | /api/project/save |
Save project JSON to file |
| POST | /api/project/load |
Load project JSON from file |
| POST | /api/project/new |
Reset project state |
WebSocket at /ws — broadcasts events: call, return, call_resolved, intercept_settings, auto_rules, log, error, detached, hooked_methods.
hooky/bridge.py provides a standalone Frida 17.x-compatible API usable in custom scripts.
All JavaScript/TypeScript agents are built inline — no separate .js or .ts files required.
from hooky.bridge import FridaBridge, InterceptedCall
bridge = FridaBridge()
# Wire callbacks
bridge.on_call = lambda call: print(f"Intercepted: {call.method_name}")
bridge.on_log = lambda level, msg: print(f"[{level}] {msg}")
bridge.on_return = lambda call_id, ret_val: print(f"#{call_id} returned: {ret_val}")
bridge.on_detached = lambda reason: print(f"Detached: {reason}")
# Connect and inject a Java hook
bridge.connect_device("usb")
bridge.spawn_and_attach("com.example.app")
bridge.resume()
hook_id = bridge.inject_java_hook(
target_class="com.example.AuthManager",
method_filters=["login", "auth.*"],
mode="all", # or "primitives"
)
# Inject a native hook
native_id = bridge.inject_native_hook(
address_spec="SSL_write",
label="SSL_write",
num_args=3,
ret_override="0x1", # None = no override
)
# Enumerate all loaded classes
classes = bridge.enumerate_loaded_classes()
# Enumerate methods of a class
methods = bridge.enumerate_class_methods("com.example.AuthManager")
# Force-call a Java method
result = bridge.force_call_method(
class_name="com.example.AuthManager",
method_name="login",
params=["java.lang.String", "java.lang.String"],
args=["admin", "secret"],
)
# Respond to a pending Java call
bridge.respond_to_call(
call_id=1000001,
action="override_return",
return_override={"value": True, "type": "boolean"},
)
# Other response actions:
# continue
# modify_input modified_args=[{"index": 0, "value": "new", "type": "string"}]
# override_return return_override={"value": True, "type": "boolean"}
# modify_both modified_args=[...], return_override={...}
# drop return a safe default without calling the original method
#
# Complex object replacement:
# modified_args=[{
# "index": 0,
# "type": "new_instance",
# "value": {
# "class_name": "com.example.Type",
# "args": ["value"],
# "arg_types": ["java.lang.String"]
# }
# }]
bridge.detach()| Type | Agent | Blocking | Return capture | Use case |
|---|---|---|---|---|
Java (inject_java_hook) |
TypeScript + frida-java-bridge | Yes, 300s pending-call wait | Yes — on_return callback |
Java/Kotlin method interception with full control |
ObjC (inject_objc_hook) |
Plain JS | Yes | Yes | Objective-C method interception |
Native (inject_native_hook) |
Plain JS — no compiler needed | No | Logged in native_call message |
Native function tracing and return override |
| Old (16.x JS) | New (17.x TS/JS) |
|---|---|
recv(callback) |
rpc.exports = { respond(...) {...} } |
script.post({...}) |
script.exports_sync.respond(...) |
Java global (auto) |
import Java from 'frida-java-bridge' via frida.Compiler |
| Native hooks via TS | Plain JS session.create_script() — no compiler needed |
hooky --setup # pip install users
npm install # source users (run in Hooky directory)Native and ObjC hooks do not require frida-java-bridge.
frida-ps -Ua | grep <appname>
adb shell "su -c '/data/local/tmp/frida-server &'"Use Class Discovery to enumerate all loaded classes, or via REPL:
frida -U -f com.example.app --no-pause -q -e \
"Java.perform(() => Java.enumerateLoadedClasses({ onMatch: n => { if(n.includes('Auth')) console.log(n); }, onComplete: () => {} }));"Use Method Discovery — it shows exact parameter types. Copy them into the filter.
Symbol may be unexported or stripped. Use Native Libs tab to browse exports, or hook by address:
frida -U -p <pid> -e "Module.enumerateExports('libnative.so').filter(e => e.name.includes('ssl')).forEach(e => console.log(e.name, e.address))"App may not have Java loaded yet. Attach to a running process, or trigger app activity before clicking Enumerate.
Class not instantiated yet, or GC collected all instances. Trigger a code path that creates the object, then retry. Static methods always work.
Hooky waits up to 300 seconds for pending Java calls in TUI and Web intercept mode. If the call times out, the agent returns a safe default for the method return type and the UI marks the call as timed_out.
Common causes:
- A newer pending call was not selected/responded to.
- The browser or terminal was left open while the app triggered a hook.
- The hooked method runs on a sensitive UI or startup thread and the app cannot proceed until you continue/drop/override.
The Monitor tab pulses when at least one intercepted Java call is waiting for a response. Open Monitor and use Continue, Modify Args, Override Return, Modify Both, or Drop.
adb shell "ps | grep frida"
adb shell "su -c 'killall frida-server && /data/local/tmp/frida-server &'"
adb shell getenforce- Frida Documentation
- OWASP MASTG
- Android Security Samples
- Regex101 — test method filter patterns