libunbound.so. The Kotlin side hooks Discord’s React Native runtime and Android UI, and the native library runs the same C++ core as the iOS loader: settings, addon scanning, bundle updates and signature checks, the crash guard, the toolbox, and hot reload.
Install
There are two ways to run the module. Both use the same module APK, signed with the same key on every release.- Rooted (Vector)
- Not rooted (LSPatch)
- Install
app.unbound.android.xposed.apkfrom the latestdiscord-<version>-androidrelease. - In Vector, or another Xposed framework such as LSPosed, enable the module. Its scope is
com.discord. - Force stop Discord and reopen it.
arm64-v8a or x86_64 device.
What happens at startup
When Discord’s process starts, the module:- Loads
libunbound.so. If the module APK’s library folder doesn’t have it, the loader extracts it once tocode_cache/unboundin Discord’s data directory and reuses that copy until the module is updated. - Opens the log file, creates the
Unboundfolder, and readssettings.json. - Reads the Hermes bytecode version of Discord’s own bundle, which picks the right bundle build.
- Hooks
ReactInstance’s bundle loading. If no bundle loader method can be found, the loader showsThis version of Discord is incompatible with this version of Unbound.and Discord runs without Unbound. - Blocks Sentry and AppsFlyer, then hooks fonts, theme colors, the recovery shortcut, the toolbox gestures, and the chat UI.
- Reloads
settings.json. Ifloader.enabledisfalse, it logsLoader is disabled. Aborting.and stops here, so Discord runs with no Unbound code. - Scans the addon folders again.
- Starts preparing Unbound’s bundle in the background: the cached copy if it has one, then an update.
- Installs a module capture so Unbound can reach Discord’s Metro modules, and a gate that holds Discord’s first screen for up to 1.5 seconds so Unbound can run before it.
- Lets Discord’s bundle run, then runs Unbound’s bundle as soon as it is ready and React Native’s modules are available.
loader.devtools is true, and finally a preload script that defines these globals:
If no bundle is ready, the loader shows
Failed to load Unbound's bundle. Please report this to the developers. and Discord runs without Unbound.
The loader never starts plugins itself. It reads them from disk and hands them to Unbound’s JavaScript through
UNBOUND_PLUGINS, and the bundle starts and stops them.Storage
All of Unbound’s files are in anUnbound folder inside Discord’s private data directory:
<data dir> is /data/data/com.discord (or the same path under /data/user/0). It is private to Discord, so browsing it needs root. The toolbox’s Copy App Folder Path copies the full path.
Addons
Addons
The loader skips an entry, logs why, and keeps scanning when:
- it is a file rather than a folder,
- it has no
manifest.json, or the manifest isn’t a JSON object, - a plugin’s
mainis missing, points outside its folder, or can’t be read, - a theme has no valid
bundle.json, or - a plugin’s signature is invalid. See signed plugins.
Settings
Settings
settings.json holds every Unbound setting, including the loader configuration flags and everything in the settings reference.The loader watches the file and applies outside edits without a restart. If it isn’t valid JSON, the loader copies it to settings.json.corrupt-<timestamp> and starts from {}.Bundle
Bundle
The active bundle is
unbound.bundle (Hermes bytecode) or unbound.js, depending on the build the loader downloaded. Next to it are files the updater manages: a .trust record of how the bundle was verified, and while an update is pending, .candidate files. Don’t edit them by hand. Use the toolbox’s bundle actions instead.Crash guard and logs
Crash guard and logs
Bundle updates
The bundle source isloader.update.url. When it is empty, release builds use https://builds.unbound.rip/. On the Android emulator, a running dev server at http://10.0.2.2:3000/ takes precedence over the default. A URL that ends in .bundle or .js names the file directly. Any other URL is treated as a folder.
On each launch the loader:
- Fetches
manifest.jsonfrom the source folder. It lists the Hermes bytecode versions the bundle was built for. If Discord’s bytecode version is listed, the loader downloadsunbound.<version>.bundle. Otherwise, or when no manifest can be read, it downloadsunbound.js. - Fetches
manifest.json.sigand checks it against the signing key built into the loader. It refuses the update when the signature doesn’t match or when the manifest:- is missing its build time, revision, channel, or hashes,
- is on a different channel than the active bundle, or was built before it,
- needs a different native bridge version than this loader has, or
- lists no SHA-512 hash for the chosen file.
- Downloads the file as a candidate. It sends the stored ETag as
If-None-Matchunlessloader.update.forceistrue, so an unchanged bundle costs a304. - Checks the candidate’s SHA-512 against the signed manifest.
- Runs the candidate. Only once it has run is it promoted to the active bundle, and its ETag is saved to
loader.update.etag. If it fails to run, or the app dies while running it, the candidate is discarded on the next launch and the previous bundle is used.
loader.update.hmr set to true, the loader waits for a fresh download first and falls back to the cached copy only if the download fails.
The signature check is skipped when loader.update.unsigned is true, when the URL’s host is localhost, *.localhost, *.local, ::1, or a private IPv4 address, or when the loader was built without a signing key.
Signed plugins
A plugin manifest can carry asignature object with version set to 1 and an ed25519 signature over the plugin’s id, version, the SHA-512 of its code, and any bytecode digests. The loader adds a trust field to each manifest it hands to Unbound:
A plugin whose signature is present but doesn’t match is not loaded at all.
A plugin can also ship precompiled Hermes bytecode next to its
main file as <name>.<bytecode version>.hbc, with its SHA-512 under the manifest’s bytecode key. The loader only returns that file when its version matches Discord’s runtime and its digest matches.
Crash guard and freeze watchdog
The loader records which addon is starting or running. When Discord exits while an addon was starting or running, the next launch counts it as a crash. After two launches in a row end in the same addon, Unbound disables it, and the loader showsDisabled <name> because Discord crashed while starting it. on the following launch. The message says running it when the addon was running, and Disabled <name> because Discord froze in it and had to be closed. after a freeze.
A watchdog pings Discord’s JavaScript thread every second. When the thread hasn’t answered for 6 seconds while an addon’s code is on it, the loader writes addon-frozen.json and logs which addon froze Discord. If Discord is then closed while frozen, that counts as a crash of that addon. If the thread recovers, the loader logs how long the freeze lasted.
Toolbox and recovery
Open the toolbox by shaking the device or by holding three fingers on the screen. It has these actions:
The two gesture switches are stored in
settings.json as toolbox.shake and toolbox.three-finger. Turning one off turns the other back on, so you can’t lock yourself out.
The loader also adds a long-press shortcut to Discord’s launcher icon that toggles recovery mode and shows the new state. If Unbound had already loaded, Discord restarts. Use it when Discord crashes before you can open the toolbox. Recovery mode skips the applied theme, and Unbound starts addons as empty no-ops. See Troubleshooting for more.
Themes and fonts
The loader applies the theme whose id is in thethemes store’s applied key, unless recovery mode is on. It replaces Discord’s color resources with the theme’s raw colors and overrides Discord’s semantic color getters with its semantic colors. On Discord builds whose theme classes are obfuscated, semantic colors are applied through the matching color resources instead. Changing the applied theme takes effect without a restart.
Fonts in Fonts replace the font states named in font-states, through React Native’s font manager, or where Android creates typefaces when that manager is obfuscated.
Hot reload
Withloader.update.hmr set to true, the loader connects to the /__hot endpoint on the bundle source’s origin. When the dev server reports a bundle with a new ETag, the loader restarts Discord to load it. Turning the key on or off applies right away.
Logs
The loader writes to Android’s log under theUnbound tag and to Logs/unbound.log. That file rotates at 1 MB, and the two previous files are kept. To copy the log files from a rooted device or emulator, run: