-
Notifications
You must be signed in to change notification settings - Fork 5
Embedded Mode & Event Hooks
This guide explains how to use ffl as an embeddable transfer core inside your own GUI, mobile app, MCP server, automation service, or any other host application.
💡 Tip: If you only want to share files from a terminal, the normal CLI is enough. If you want your own program to start
ffl, receive progress updates, get the share link, handle errors, provide previews, or generate thumbnails, this is the guide you should read.
In the normal CLI workflow, users run ffl directly from a terminal:
ffl ./photo.jpgIn this mode, ffl prints text output, QR codes, error messages, and share links by itself. This is convenient for command-line usage, but it is not enough for applications and services, because the host program usually needs to:
- Show transfer progress in its own UI.
- Update the screen or notification when a share link is created.
- Display a structured and understandable error state when something fails.
- Generate thumbnails, manifests, sidecars, or platform-specific preview data by itself.
- Start
fflas a subprocess while keeping lifecycle control in the parent process. - Reuse the same transfer core across Android, desktop GUI, MCP servers, or other environments.
Embedded Mode means keeping ffl focused on being the transfer core, while the host application handles platform integration, UI, file permissions, event handling, previews, and lifecycle management.
🧩 Design idea:
fflshould not need to know whether it is running inside a terminal, Android service, MCP server, or desktop GUI. The host application owns the platform-specific behavior;fflowns the transfer.
You can think of it like this:
Your App / MCP Server / Android Service / GUI
│
│ 1. Start ffl as a subprocess
▼
ffl core
│
│ 2. Report events through Hook
▼
Your Hook Server / Event Handler
│
│ 3. Update UI, store state, return custom routes
▼
Your App Logic
The main Embedded Mode interfaces are:
| Interface | Purpose |
|---|---|
--hook |
Lets the host application receive ffl events, and optionally respond to some control requests. |
--json |
Writes stable final output that a UI or service can read as a fallback. |
--qr |
Writes the QR code as an image file so the host app can display it. |
--vfs / vfs://...
|
Lets the host application provide a virtual file system, usually for Android content:// URIs or other non-standard file sources. |
This page focuses on Embedded Mode and Event Hooks. The details of VFS are covered separately in VFS-Implementation-Guide.md.
In a CLI, a human can read terminal output. In an app, the program needs structured events.
For example, when ffl finishes creating a share link, the app should not have to parse a long text stream and guess where the link is. It should receive a structured event like this:
{
"event": "/share/link/create",
"timestamp": 1710000000.123,
"data": {
"link": "https://...",
"fileName": "photo.jpg",
"fileSize": 123456
}
}This is what --hook is for.
There are two common ways to use --hook.
The host application starts a local HTTP server first, then passes the URL to ffl:
ffl ./photo.jpg --hook http://127.0.0.1:18080/eventsWhen an event happens, ffl sends it to this endpoint with HTTP POST.
This is the most common mode for GUIs, Android apps, and MCP servers.
🔒 Security Note: For embedded applications, the Hook server should normally bind to
127.0.0.1only. It is an internal IPC channel, not a public web API.
If you do not want to run an HTTP server, you can write events to a local file:
ffl ./photo.jpg --hook events.jsonlThis creates one JSON object per line. It is useful for:
- Debugging.
- Automation scripts.
- Tests.
- Batch jobs that do not require real-time interaction.
A minimal Embedded Mode workflow usually looks like this:
1. The host application starts an HTTP server and waits for /events.
2. The host application starts ffl as a subprocess.
3. The ffl arguments include --hook http://127.0.0.1:<port>/events.
4. ffl sends events for link creation, transfer progress, errors, and completion.
5. The host application updates UI or internal state based on those events.
6. When sharing ends, or when the user cancels, the host application stops the ffl subprocess and cleans up the Hook server.
A typical command line generated by the host application may look like this:
ffl ./photo.jpg \
--json /tmp/ffl.json \
--qr /tmp/ffl_qr.png \
--hook http://127.0.0.1:18080/eventsIn this setup:
-
--hookprovides real-time events. -
--jsonprovides stable result output, useful as a final fallback. -
--qrlets the host application display a QR image directly. - The parent process is responsible for start, stop, retry, error classification, and UI updates.
The basic webhook payload looks like this:
{
"event": "/event/name",
"timestamp": 1710000000.123,
"data": {
"...": "..."
}
}The host application should first read these fields:
| Field | Description |
|---|---|
event |
The event name, usually in /xxx/yyy form. |
timestamp |
The time when the event happened. |
data |
The event-specific payload. |
After handling the event, the Hook server usually returns:
{"ok": true}If the Hook server only receives events and updates UI, returning {"ok": true} is enough.
✅ Practical Start: Your first Hook server can be very small: parse JSON, dispatch by
event, update state, and return{"ok": true}. Add advanced behavior only when you need it.
However, Hooks are not limited to passive event reporting. In more advanced Embedded Mode flows, the Hook server can also return data to ffl, such as custom endpoint routes, sidecar data, or instructions that tell ffl that a certain platform-specific capability is handled by the host application.
A minimal Hook server only needs to support:
POST /events
Content-Type: application/json
The request handling flow is simple:
Read HTTP body
│
▼
Parse JSON
│
▼
Extract event and data
│
▼
Pass them to the app's event handler
│
▼
Return JSON
The Android EventServer implementation follows exactly this pattern. It only handles POST /events, calls onEvent(eventName, eventData) when an event arrives, and returns the JSON result from the handler. If the handler does not return anything special, it returns a default success response.
This design is intentionally small. A Hook server does not need to be a full web framework. In most embedded applications, it only runs on 127.0.0.1 and only serves the ffl subprocess started by the same application.
💡 Tip: Keep the Hook server boring. The less framework code it depends on, the easier it is to embed into mobile apps, services, and tests.
ffl-mcp is a good reference implementation for Embedded Mode.
Its role is to wrap ffl as an MCP tool so that an AI Agent can share files through ffl. In this environment, the MCP server should not ask the agent to parse terminal output. Instead, it needs to know, in a stable and structured way:
- Whether the
fflsubprocess started successfully. - Whether the share link has been created.
- Which events happened recently.
- How to stop a session.
- How to provide preview manifest and thumbnails when needed.
To do this, ffl-mcp follows this model:
- Start its own Hook server.
- Generate a hook URL with Basic Auth.
- Start the
fflsubprocess with--hook <url>. - Store the link when
/share/link/createis received. - Return custom routes such as
/manifestand/thumbwhen/hook/server/endpoints/registeris received. - Expose MCP tools so the agent can query sessions, stop sessions, and get the share link.
Conceptually:
AI Agent
│
▼
MCP Tool: share_file(...)
│
▼
ffl-mcp starts Hook Server
│
▼
ffl-mcp starts ffl --hook http://user:pass@127.0.0.1:<port>/events
│
▼
ffl sends /share/link/create
│
▼
ffl-mcp stores the link and returns it to the AI Agent
This example demonstrates one of the most important uses of Hooks:
They turn ffl from an interactive CLI program into a programmatically controlled transfer service.
🤖 Real-world validation: This is why the MCP integration is a useful reference: it shows how an agent-facing service can control
fflwithout scraping terminal output.
Android is a typical Embedded Mode use case because Android file sources are often not normal file paths. They are usually content:// URIs.
The Android app uses three main paths.
For a normal local path:
User selects a normal local path
│
▼
Start EventServer
│
▼
ffl <path> --hook http://127.0.0.1:<port>/events
For a content:// file or folder:
User selects a content:// file or folder
│
▼
Start VfsServer
│
▼
Get vfs://127.0.0.1:<port>
│
▼
ffl vfs://127.0.0.1:<port> --hook http://127.0.0.1:<port>/events
For multiple content:// files:
User selects multiple content:// files
│
▼
VfsServer exposes them as one virtual folder
│
▼
ffl shares the virtual folder
The important part is the design concept, not the Android-specific code:
-
ffldoes not need to understand Androidcontent://directly. - The app converts Android file sources into a
vfs://source thatfflcan read. - The app receives
fflevents through Hook. - The app displays progress, errors, notifications, and QR codes using its own UI.
- The app can use native Android APIs to generate thumbnails and video frames.
This keeps the ffl core cross-platform, while Android handles the platform-specific parts.
📱 Android Note: Android storage is a good stress test for Embedded Mode. If the design can handle
content://, multiple selected URIs, foreground service lifecycle, notifications, and preview generation, it is usually flexible enough for simpler desktop or server integrations too.
At first, you can think of Hook as an event notification channel.
In Embedded Mode, however, Hook can also play a more advanced role: it lets the host application provide custom endpoints.
The flow is:
ffl asks the Hook server: which endpoints can you provide?
│
▼
The Hook server returns routes
│
▼
ffl forwards selected requests to the Hook server
│
▼
The Hook server responds using platform-native capabilities
For example, the host application may respond to /hook/server/endpoints/register like this:
{
"routes": [
{
"method": "GET",
"path": "/manifest",
"encryptResponse": true
},
{
"method": "GET",
"path": "/thumb",
"encryptResponse": true
}
]
}This means:
-
/manifestis provided by the host application. -
/thumbis provided by the host application. - If E2EE is enabled, the response can be encrypted by
fflbefore it is sent out.
This is especially useful for previews and thumbnails, because thumbnail generation is often platform-specific:
- Android can use
ContentResolver.loadThumbnail(). - Android can use
MediaMetadataRetrieverto extract video frames. - A desktop GUI may already have its own image library.
- A server may already have a preview cache.
- An MCP server can generate thumbnails using Python libraries.
If all of this logic were built directly into the ffl core, the core would become much heavier and more platform-dependent. With Hook endpoint routing, ffl only needs to know: “this endpoint is provided by the host application.”
🧠 Rule of thumb: Put transfer protocol logic in
ffl; put platform-native preview logic in the host application.
In upload mode, some preview data is better generated by the host application instead of the ffl core.
The Android implementation uses Hooks for the sidecar flow:
ffl sends /upload/tell
│
▼
The app estimates sidecar size and starts thumbnail generation asynchronously
│
▼
ffl sends /upload/sidecar/fetch
│
▼
The app waits for thumbnails, builds the sidecar, and returns it to ffl
A simplified view:
/upload/tell
-> return sidecar size
-> start thumbnail generation in the background
/upload/sidecar/fetch
-> return base64 sidecar data
-> return manifest offset map
This design has two important benefits.
First, ffl does not need to know how Android reads images, decodes videos, or handles content:// URIs.
Second, the app can control preview quality, caching, timeouts, and error handling.
⚙️ Implementation Note: Sidecar generation is allowed to be asynchronous.
/upload/tellcan start the work and report an estimated size, while/upload/sidecar/fetchwaits for the prepared result.
In practice, this allows the Android app to support:
- A single
content://file. - A tree URI folder.
- A virtual folder made from multiple independent URIs.
- Image and video thumbnails.
- Encrypted preview responses when E2EE is enabled.
If you want to embed ffl into your own project, this is a good starting architecture:
App Controller
│
├── Hook Server
│ └── POST /events
│
├── Optional VFS Server
│ ├── GET /meta
│ ├── GET /list
│ ├── GET /stat
│ ├── GET /open
│ └── GET /file
│
├── Process Manager
│ ├── build ffl args
│ ├── start subprocess
│ ├── read stdout/stderr
│ └── stop/kill/cleanup
│
└── UI / API Layer
├── show progress
├── show link
├── show QR code
├── show errors
└── expose session state
⚠️ Important: The most error-prone part is usually not Hook itself, but lifecycle management:
- The Hook server should be ready before starting
ffl. - When the subprocess exits, the Hook server should be stopped.
- When the user intentionally cancels, do not report the killed subprocess as a transfer error.
-
--jsonand--qrshould use temporary paths and be cleaned up after the session. - If VFS is used, the VFS server should be tied to the same session lifecycle.
- If the app supports multiple share sessions, each session should have its own hook port, state, and cleanup path.
A host application does not need to understand every event at first.
A good starting point is to handle these categories.
When the share link is created:
- Store the link.
- Update the UI.
- Show a notification.
- Start a health check or availability check if your app needs one.
- If
--qris enabled, notify the UI that the QR image is ready to read.
When progress events arrive:
- Update the progress bar.
- Show speed, remaining time, or transferred size.
- Avoid expensive UI redraws for every event; use throttling if needed.
When an error event arrives:
- Map it into your app's internal error category.
- Show a user-friendly message.
- Keep the raw message in debug logs.
- Clean up the session.
When the transfer is completed or the session is closed:
- Update state to idle or completed.
- Stop health checks.
- Clean up temporary files.
- Stop Hook and VFS servers.
- If the user stopped the session intentionally, do not show an error.
Embedded Mode usually runs locally, but you should still define a clear security boundary.
🔒 Security Note: Treat Hook and VFS as internal control/data channels. They may carry file names, local paths, share links, preview metadata, and other sensitive information.
Prefer:
127.0.0.1
Do not bind the Hook server to 0.0.0.0 unless you are very sure you want to expose it beyond the local machine.
If the hook URL may be visible to other local processes, use Basic Auth or a random path:
http://user:random-password@127.0.0.1:18080/events
or:
http://127.0.0.1:18080/events/<random-token>
Hook events may contain:
- File names.
- Local paths.
- Share links.
- Authentication settings.
- Error messages.
- Upload session metadata.
Full logging is useful while debugging. Production apps should filter carefully.
If you support endpoint routing, use an explicit allowlist:
GET /manifest
GET /thumb
GET /file # only enable this when appropriate for your security model
Do not turn the Hook server into an arbitrary proxy, and do not allow arbitrary local file reads.
🛠️ Debugging Tip: When diagnosing Embedded Mode issues, check startup order first: Hook/VFS server readiness, then subprocess arguments, then event delivery, then cleanup.
Check:
- The Hook server is ready before
fflstarts. - The
--hookURL is correct. - The server is bound to
127.0.0.1. - The path matches, for example
/events. - If Basic Auth is used, the username and password are correct.
- Firewall or platform sandbox rules are not blocking loopback.
Use two sources when possible:
- Get the link from Hook events.
- Fall back to the
--jsonoutput file.
This way, even if a Hook event is missed for some reason, the app can still recover the final link from the JSON result.
This is usually a lifecycle issue.
When the user intentionally stops sharing, the subprocess may be terminated by SIGTERM or SIGKILL, and the exit code may not be zero. The app should keep an isStopping flag, or something equivalent, so the exit handler knows this was an intentional stop and should not be treated as a transfer error.
Check:
- The Hook handler correctly handles
/hook/server/endpoints/register. - The returned JSON contains
routes. -
methodandpathmatch the actual request. -
encryptResponseis set correctly when E2EE is enabled. - The endpoint response has correct content type and content length.
Check:
-
/upload/tellstarts thumbnail generation. -
/upload/sidecar/fetchwaits for thumbnail generation to complete. - Large media folders may need a longer timeout or a smaller initial sidecar.
- The thumbnail cache is working.
- The app is not being heavily CPU- or I/O-restricted in the background.
Before integrating Embedded Mode, you can use this checklist:
[ ] Hook server binds to 127.0.0.1 only
[ ] Hook server supports POST /events
[ ] Hook event payload can be parsed into event / timestamp / data
[ ] Hook handler returns {"ok": true} by default
[ ] ffl arguments include --hook
[ ] ffl arguments include --json as a fallback
[ ] Add --qr if the app needs QR output
[ ] App has a process manager for stop / cleanup
[ ] Intentional user cancellation is not reported as an error
[ ] Error events are mapped to categories and raw logs
[ ] If preview is supported, handle /hook/server/endpoints/register
[ ] If upload sidecar is supported, handle /upload/tell and /upload/sidecar/fetch
[ ] If Android content:// is supported, use VFS together with Hook
Use this rule of thumb:
| Scenario | Recommendation |
|---|---|
| Sharing a normal local file path | Hook is enough |
| Sharing a normal local folder path | Hook is enough |
| GUI wants to show progress, link, and errors | Hook |
MCP server wants to wrap ffl as a tool |
Hook |
Android content:// single file |
VFS + Hook |
Android content:// folder |
VFS + Hook |
| Multiple URIs that are not in the same folder | VFS virtual folder + Hook |
| Platform-native thumbnail generation | Hook endpoint routing |
| Upload preview sidecar | Hook sidecar flow |
In short:
- Hook is the control and event channel.
- VFS is the data source abstraction.
They can be used independently, or together.
In the Android app, both usually appear together. In an MCP server, Hook alone is usually enough.
Embedded Mode is not trying to turn ffl into a large server framework. The goal is to provide a clean IPC boundary that is just powerful enough:
ffl core:
- transfer
- tunnel
- upload / download
- link lifecycle
- encryption
- protocol handling
Host application:
- UI
- notification
- platform file access
- preview / thumbnail
- process lifecycle
- local policy
- error presentation
This gives several benefits:
- The
fflcore can stay small and stable. - The host application can use platform-native capabilities.
- The same core can be reused by CLI, Android, MCP, and GUI environments.
- New platforms do not need to fork the core; they only need Hook / VFS glue.
- Testing is easier, because Hooks can be simulated with JSONL files or a small HTTP server.
If you want to integrate Embedded Mode into your own project, a good path is:
🚀 Suggested path: Start with JSONL, then HTTP Hook, then subprocess lifecycle, then endpoint routing, and only add VFS when your file source requires it.
ffl ./sample.jpg --hook events.jsonlLook at the event format first.
Support:
POST /events
Print incoming events and return:
{"ok": true}Do not run the CLI manually. Start it from your program using ProcessBuilder, subprocess.Popen, or an equivalent API.
Handle link creation, progress, errors, and completion.
This gives your app a stable fallback and QR image output.
Start with /manifest, then add /thumb.
Examples include Android content://, multiple independent URIs, or app-managed data sources.
The core idea of Embedded Mode is simple:
✨ Summary: The host application starts
ffl;fflsends events back through Hook; when platform-specific behavior is needed, the host application can also respond tofflrequests through Hook endpoint routing.
This model has already been validated in two real environments:
-
MCP server: wraps
fflas a file-sharing tool that can be called by an AI Agent. -
Android app: starts
fflfrom a native service, updates UI through Hook, supportscontent://through VFS, and generates preview / thumbnail / sidecar data using native Android APIs.
If you are building a CLI workflow, plain ffl is enough.
If you are building an app, service, agent tool, or platform integration, Embedded Mode gives you a more stable, cleaner, and more maintainable integration model.