A tool for rewriting OkHttp responses on Android apps without rebuilding the app, without certificates, and without a proxy.
- Targets debuggable APKs
- The app's source code is untouched
- Rules disappear the moment the device process is killed (= session-scoped)
- Distributed as a single Go binary
- macOS / Linux / Windows (the CLI shells out to
adb/adb.exe) - The target app uses OkHttp 4.x or 5.x (both verified end-to-end)
- The target app is debuggable (debug builds are debuggable by default) and runs on API 28+
adbis onPATHand a device or emulator is connected
curl -fsSL https://raw.githubusercontent.com/tkhskt/forja/main/install.sh | bashInstalls the binary to $HOME/.local/bin/forja and the JVMTI agent to $HOME/.local/share/forja/agent/. Supports macOS and Linux on arm64 / amd64.
Re-run the same command later to update — install.sh always fetches the current latest tag, wipes the agent dir, and recopies, so old .so files never linger across versions.
For Windows, manual install, or building from source, see docs/install.md.
Verify:
forja --version
forja --helpIf ~/.local/bin isn't on your PATH yet:
export PATH="$HOME/.local/bin:$PATH"Rule definitions and per-device, per-app on/off state live in a .forja/ directory at the root of your project. forja never creates that directory on its own — forja init is the one-time setup step.
# 0. One-time setup: create .forja/ + .forja/rules.yml with a schema-commented
# template (also prints the recommended .gitignore entries to add by hand).
forja init
# 1. Add a rule to the catalog (yml only — nothing reaches the device yet)
forja rules add mock-failure \
--host example.com --path /foo \
--status 500 --body '{"message":"failure"}'
# 2. Open the TUI: pick an app from the device list,
# toggle the rule on, q to save & push
forja rulesNon-interactive equivalent of step 2: forja apply --app com.example.app --enable mock-failure.
# Patch any field — auto-pushes to every app where the rule is enabled
forja rules update mock-failure --status 502$EDITOR .forja/rules.yml
forja syncsync re-reads the yml and pushes to every app that already has rules enabled, without changing which rules are on.
See the rule schema reference for the full yml structure (match: / response: groups, bodyFile:, scope conflict resolution, etc.).
forja off --app com.tkhskt.sample_appThe app starts seeing the real responses again; the rule catalog (yml) stays intact, so you can re-enable later via the TUI or forja apply.
Rules are session-scoped on the device: kill the app and the rewrites disappear; relaunch and push again to get them back. Nothing is persisted in the app's filesystem long enough to survive a process restart.
docs/install.md— full install reference (macOS / Linux / Windows / from source) + bundle resolution orderdocs/usage.md— complete command reference, rule schema (.forja/rules.yml+ bundles, aliases), the MCP server (forja mcp) for driving forja from an AI client, recommended.gitignore, scope conflict resolutiondocs/internals.md— how the JVMTI attach + interceptor injection works, troubleshooting, module layout, release procedure, license check
Distributed under the Apache License 2.0.
Two third-party components are vendored under jvmti-agent/src/main/cpp/:
slicer/— the dex bytecode rewriter from AOSP'stools/dexter, under the Apache License 2.0 (the same license as forja). It's compiled intolibforja-agent.soand used to instrumentokhttp3.OkHttpClient.interceptors().jvmti.h— from OpenJDK under GPLv2 + the Classpath Exception. The Classpath Exception explicitly permits linking that file with code under any other license, so consumers of forja are not bound by GPLv2.
See NOTICE for the full third-party inventory.