Introduction
Ferret is a Kotlin Multiplatform
network inspector that plugs directly into your Ktor HttpClient.
It captures every HTTP request, response, and WebSocket message — and
surfaces it all in a slick on-device debug UI, with no proxy, no
certificate, no separate build.
Ferret acts as a transparent Ktor plugin. Every request and response flows through it, gets persisted to a local Room (SQLite) database, and is immediately visible via a rolling system notification and a full in-app inspector screen — all entirely on-device.
How it works
-
1
Install the plugin
Call
install(Ferret) { ... }when you create yourHttpClient. That's all the code you write. -
2
Traffic is captured automatically
Every HTTP request, response, and WebSocket frame that flows through your client is intercepted and stored locally. No proxy, no certificate trust profile, no separate debug build variant required.
-
3
Inspect from anywhere
A rolling system notification summarizes recent activity — tap it to jump straight into the inspector. On Android a home-screen shortcut gives one-tap access even when the notification is dismissed.
Supported platforms
| Platform | Ktor Engine | Status |
|---|---|---|
| Android | OkHttp | ✅ Fully supported |
| iOS | Darwin | ✅ Fully supported |
Distributed via Maven Central
Ferret is published to Maven Central under the group
io.github.ferret-org. See Basic
Integration for the dependency snippet.
Features
Everything you need to inspect your app's network traffic — without leaving the device.
curl
command or as plain text for a bug report — one tap.
Unlike Charles Proxy or Proxyman, Ferret requires no SSL certificate trust profile, no Wi-Fi proxy configuration, and no desktop companion. It works inside the app process, on the device, in any environment.
Requirements
Ferret requires Ktor as your HTTP client. Here are the full version requirements before adding the dependency.
| Tool / Dependency | Minimum | Built & tested with |
|---|---|---|
| Kotlin | 1.9.0 | 2.4.0 |
| Ktor | 3.x | 3.5.1 |
| Compose Multiplatform | 1.10.x | 1.10.3 |
| Android Gradle Plugin | — | 9.2.1 |
| Gradle | — | 9.4.1 |
Android minSdk |
28 | 28 |
Android compileSdk / targetSdk |
37 | 37 |
Ferret declares minSdk 28 in its own manifest. Any app that adds
Ferret as a dependency inherits this constraint. Check your app's
minSdk before integrating.
Ferret is implemented as a Ktor HttpClientPlugin. It is not a system
proxy or a network VPN — your app must route HTTP traffic through a Ktor HttpClient
for Ferret to intercept it.
iOS targets
Ferret publishes pre-built artifacts for all three standard iOS targets:
iosArm64— physical iPhone / iPad devicesiosSimulatorArm64— Apple Silicon simulatorsiosX64— Intel Mac simulators
Basic Integration
Add the dependency from Maven Central and install the Ktor plugin — that's all it takes.
Step 1 — Version catalog
Declare the version and library alias in gradle/libs.versions.toml:
[versions]
ferret = "1.0.0"
[libraries]
ferret = { module = "io.github.ferret-org:ferret", version.ref = "ferret" }
Step 2 — Add the dependency
In your shared/common module's build.gradle.kts:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.ferret)
}
}
}
After adding the dependency, follow the platform-specific setup to install the plugin: Android Setup or iOS Setup.
Android Setup
Full setup guide for Android: manifest permissions, client configuration, and what Ferret configures automatically on first run.
1 · Add manifest permissions
Add both permissions to your app's AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
This permission is needed to display the rolling capture notification. Ferret still captures all traffic without it — the notification just won't appear. See Notification Permission for how to request it at runtime on Android 13+.
2 · Install the plugin
import com.ferret.intercept.Ferret
import com.ferret.intercept.install
import io.ktor.client.*
import io.ktor.client.engine.okhttp.*
val client = HttpClient(OkHttp) {
install(Ferret) {
context = applicationContext
}
}
Pass an Android Context to the context property. Using
applicationContext is recommended — it survives configuration changes
and Activity recreation.
3 · What Ferret configures automatically
The first time the plugin initializes it performs four setup steps automatically:
- Creates its Room database on-device to persist captured network records.
- Creates a notification channel (
"General"by default) on Android 8+. - Registers a dynamic launcher shortcut labeled "Open Ferret Inspector".
- Starts posting the rolling notification as traffic flows through the client.
Activity auto-merge
Ferret's inspector activity (com.ferret.ui.FerretActivity) is declared
in the library's own manifest and merges into your app automatically at build time.
No manual declaration in your manifest is needed.
If the inspector is opened before the plugin has been installed (e.g. via the shortcut before the app runs), Ferret shows a dialog explaining it isn't ready yet — it does not crash.
iOS Setup
Ferret works on iOS with minimal setup. No context property, no manifest changes — install the plugin and decide how to present the inspector.
Install the plugin
import com.ferret.intercept.Ferret
import com.ferret.intercept.install
import io.ktor.client.*
import io.ktor.client.engine.darwin.*
val client = HttpClient(Darwin) {
install(Ferret) {}
}
There is no context field on iOS. Ferret initializes automatically the
first time the plugin is installed.
Presenting the inspector
iOS has no equivalent of Android's home-screen app shortcuts, so you decide where and how to surface the inspector. Common approaches:
- A debug section in your app's settings screen
- A shake gesture (override
motionEndedin your root view controller) - A hidden tap sequence on a logo or version label
- A long-press on a designated debug element
To open the inspector, call FerretIosUi.open() from Kotlin or present
ferretViewController() from Swift:
FerretIosUi.open()
let vc = YourSharedFramework.ferretViewController()
present(vc, animated: true)
Notifications on iOS
Ferret uses UNUserNotificationCenter on iOS. To have Ferret request
notification permission at startup, set requestPermission = true:
install(Ferret) {
configuration = FerretConfiguration(
notifications = NotificationConfiguration(
requestPermission = true
)
)
}
Opening the Inspector
Several ways to open the Ferret inspector UI, depending on your platform and workflow.
Android Android
Notification tap
While Ferret is capturing traffic it posts a rolling system notification. Tapping it
opens FerretActivity directly — no setup required beyond installing the
plugin.
Home-screen shortcut
Ferret registers a dynamic launcher shortcut labeled "Open Ferret Inspector". Long-press your app's icon and select it to jump into the inspector at any time, even if the notification is dismissed.
Programmatic launch
Launch from a debug settings screen, a hidden gesture, or any other trigger:
startActivity(
Intent(context, Class.forName("com.ferret.ui.FerretActivity"))
)
iOS iOS
Notification tap
When Ferret is active and has notification permission, it posts a rolling
notification via UNUserNotificationCenter. Tapping it opens the
inspector directly — no extra code needed. Make sure requestPermission =
true is set in NotificationConfiguration, or request the
permission yourself before Ferret can show the notification.
From Kotlin / shared code
FerretIosUi.open()
From Swift
// Replace "YourSharedFramework" with your actual Kotlin/Native framework name
let controller = YourSharedFramework.ferretViewController()
present(controller, animated: true)
No system shortcut on iOS means you pick the trigger. Common patterns: shake the device, tap a version label five times, or add a "Debug" row in a settings screen that's only compiled in debug builds.
Ferret Configuration
All of Ferret's behavior is controlled through a single FerretConfiguration
data class. Pass it to install(Ferret) { configuration = ... }. If
omitted, defaults are used.
Full example
install(Ferret) {
context = applicationContext // Android only
configuration = FerretConfiguration(
notifications = NotificationConfiguration(
maxBufferSize = 5,
defaultPriority = NotificationPriority.HIGH,
defaultChannel = NotificationChannelSpec(
id = "ferret_debug",
name = "Ferret Debug"
),
defaultSmallIcon = R.drawable.ic_notification, // Android only
requestPermission = false
),
retentionDurationHours = 24L
)
}
FerretConfiguration properties
| Property | Type | Default | Description |
|---|---|---|---|
| notifications | NotificationConfiguration |
NotificationConfiguration() |
Controls the rolling notification. See Notification Settings. |
| retentionDurationHours | Long |
12 |
Records older than this many hours are automatically deleted on initialization. |
Retention
Ferret automatically purges records older than retentionDurationHours
each time the plugin initializes. The default (12 hours) is enough for a typical
development session while bounding on-device storage use. Increase it if you need to
retain history across multiple app launches.
You can always wipe all records immediately using the trash icon in the inspector UI, regardless of the retention setting.
Notification Settings
Control how the rolling capture notification looks and behaves using
NotificationConfiguration.
NotificationConfiguration properties
| Property | Default | Description |
|---|---|---|
| maxBufferSize | 5 |
Number of recent request entries shown in the notification body. |
| defaultPriority | HIGH |
Notification priority — controls heads-up behavior and ranking. See NotificationPriority below. |
| defaultChannel | NotificationChannelSpec() |
The Android notification channel Ferret posts to. Customise the
id and display name.
|
| defaultSmallIcon | System default | Drawable resource ID for the notification small icon. Android only. |
| requestPermission | false |
When true, Ferret requests notification permission from the
OS at initialization. iOS only — no-op on
Android.
|
NotificationPriority
Maps to NotificationCompat priority constants on Android and UNNotificationInterruptionLevel
on iOS.
| Value | Android behavior |
|---|---|
| MIN | No heads-up; minimal ranking. Does not make a sound. |
| LOW | No heads-up; lower-than-normal ranking. |
| DEFAULT | Standard notification ranking. May make a sound. |
| HIGH | May show as a heads-up notification. (default) |
| MAX | Full urgency — use only when you really need immediate attention. |
NotificationChannelSpec
Defines the Android notification channel. Channels are created once — changing properties after the channel exists requires the user to clear app data or reinstall.
NotificationChannelSpec(
id = "ferret_debug", // unique channel ID — used internally
name = "Ferret Debug" // display name shown in system notification settings
)
Once your app has been installed, changing the channel id creates a
new channel. The old channel — with the old settings the user may have
customized — persists until you explicitly delete it or the user reinstalls.
Keep the id stable across releases.
Notification Permission
Starting with Android 13 (API 33), apps must request POST_NOTIFICATIONS
at runtime before they can display any notification.
Ferret captures and stores all network traffic regardless of notification permission status. The rolling notification simply won't appear until the permission is granted. The in-app inspector and home-screen shortcut are always available.
Ferret's responsibility
Ferret declares POST_NOTIFICATIONS in its own manifest so it merges into
your app, but it does not request the permission at runtime. Your
app is responsible for requesting it — the same way you would for any of your own
notifications.
Requesting at runtime
If your app already requests notification permission for another feature, no extra work is needed. If not, add a runtime request somewhere sensible — first launch, an onboarding screen, or a debug settings screen.
Compose API
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
// handle the result
}
LaunchedEffect(Unit) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
permissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
Traditional API
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
REQUEST_CODE_NOTIFICATIONS
)
}
On iOS, set requestPermission = true in NotificationConfiguration.
Ferret will call UNUserNotificationCenter.requestAuthorization at
initialization time — no extra code from you.
Inspector Overview
The Ferret inspector is a full Compose Multiplatform UI shared between Android and iOS. Here's a tour of what it offers.
Network record list
The main screen shows every captured HTTP call and WebSocket event, newest first. Each row shows:
- HTTP method badge (
GET,POST, …) or a WebSocket indicator - Request URL / path
- Response status code, color-coded by range (2xx green, 4xx orange, 5xx red)
- Total request duration in milliseconds
Type tabs
| Tab | Shows |
|---|---|
| All | Every captured record — HTTP and WebSocket combined |
| HTTP | Plain HTTP request/response pairs only |
| WebSocket | WebSocket connections and message frames only |
Search
The search bar filters the active list by URL or path substring. Combine the tab filter with search to zero in on a specific request fast.
Detail screen — HTTP
Tap any HTTP record to open the detail view. Four tabs are available:
- Overview — method, full URL, status code, timing in ms, protocol, TLS info
- Request — all request headers and the request body
- Response — all response headers and the response body
- Timing — per-phase breakdown (DNS, connect, TLS, TTFB, transfer)
Detail screen — WebSocket
- Overview — URL, connection state, total duration
- Messages — all sent and received frames with timestamps, frame type, and byte size
Share
From the detail screen tap Share to export a request in two formats:
- cURL command — a ready-to-run shell command you can paste into a terminal or a colleague's chat
- Plain text — a human-readable summary suitable for filing a bug report
Copy to clipboard
Tap any individual field — URL, header name/value, body, status code — to copy it directly to the clipboard.
Clear history
The trash icon in the inspector toolbar wipes all captured records from the local database immediately. The inspector will accumulate new records as fresh requests are made.
All captured data is stored only in the on-device Room database. Nothing is ever transmitted to an external server, logged to a remote service, or shared outside the app process.
FAQ
Common questions about Ferret.
Yes. Ferret captures WebSocket connections and all message frames — both sent and received — in addition to regular HTTP traffic. Everything appears in the same inspector list. Use the WebSocket tab to filter to WebSocket-only events.
On Android there are two built-in options requiring no code:
tap the rolling notification, or long-press your app icon and select "Open
Ferret Inspector". You can also launch it programmatically with startActivity(Intent(context,
Class.forName("com.ferret.ui.FerretActivity"))). On
iOS there is no system shortcut — present ferretViewController()
from wherever makes sense in your app (a debug menu, shake gesture, etc.).
No. Ferret is implemented as a Ktor HttpClientPlugin. It is not
a system proxy or VPN — it cannot intercept traffic from OkHttp, Retrofit,
URLSession, or any client that doesn't go through a Ktor
HttpClient. Your networking layer must use Ktor for Ferret to
capture it.
Ferret is designed for development and internal builds. Gate the install(Ferret)
call behind a debug flag (e.g. if (BuildConfig.DEBUG)) or swap
in a plain HttpClient for release. If Ferret is not installed,
it adds zero overhead — no plugin, no interception, no database.
No. Everything is stored in an on-device Room (SQLite) database. Ferret never makes outbound connections for telemetry or syncing and never sends captured data to any server. You can wipe all records at any time from the inspector UI.
Yes. After the plugin is initialized,
FerretSdk.networkRecordRepository provides the full NetworkRecordRepository
interface — including observeAll(): Flow<List<NetworkRecord>>,
getById(id), and clear(). This is useful for
writing custom test assertions or building additional debug tooling on top
of Ferret.
Ferret is built and tested with Ktor 3.5.1 and requires Ktor 3.x. Ktor 2.x is not supported because the plugin API changed between major versions. Check the full Requirements table for all version details.
Developers
Ferret is built and maintained by the following developers. Contributions, issues, and feedback are welcome on GitHub.
Found a bug or want to improve Ferret? Open an issue or pull request on GitHub. All contributions are welcome.
About & License
Ferret is an open-source Kotlin Multiplatform library licensed under the Apache License 2.0.
Why Ferret?
Inspecting mobile network traffic has traditionally required external tools: Charles Proxy, Proxyman, or Wireshark — each needing SSL certificate trust profiles, Wi-Fi proxy configuration, and a desktop machine on the same network. Ferret eliminates that friction by living inside the app process with a self-contained on-device UI. Same visibility, no external dependencies.
Technology stack
| Technology | Version | Purpose |
|---|---|---|
| Kotlin Multiplatform | 2.4.0 | Shared business logic across Android and iOS |
| Compose Multiplatform | 1.10.3 | Shared inspector UI — one codebase, both platforms |
| Ktor | 3.5.1 | HTTP client plugin integration point |
| Room | 2.7.1 | Local SQLite persistence for captured records |
| kotlinx.coroutines | — | Async operations and Flow-based data streams |
| kotlinx.datetime | 0.7.1 | Cross-platform date/time formatting |
| kotlinx.serialization | 1.8.0 | JSON body parsing |
Maven coordinates
| Field | Value |
|---|---|
| groupId | io.github.ferret-org |
| artifactId | ferret |
| latest version | 1.0.0 |
| repository | Maven Central |
License
Ferret is licensed under the Apache License 2.0.