Android Zygisk module + Kotlin Compose manager app for storage redirection.
IMPORTANT Before using this project, make sure: - Your device is rooted. - Zygisk is enabled. - The module is installed and the device has been rebooted once.
users.{userId}) on multi-user devices.allowed_real_paths) and path rewrite rules (path_mappings).Apps -> select a user and app -> enable redirect.Apps and choose the target user/app.Enable Redirect.Allowed Paths.Path Mappings (request_path -> final_path).NOTE After saving, the target app process will be stopped automatically to apply changes on next launch.
Assume app com.example on user 0.
When redirect is enabled, the default redirect base is:
/storage/emulated/0/Android/data/com.example/sdcard
So by default, app access like:
/storage/emulated/0/DCIM/MyApp/a.jpg
is redirected to:
/storage/emulated/0/Android/data/com.example/sdcard/DCIM/MyApp/a.jpg
Then exception rules are applied on top of that default behavior:
| Priority | Rule | Behavior |
|---|---|---|
| 1 | path_mappings |
Longest matching request_path prefix rewrites to final_path. |
| 2 | allowed_real_paths exclusions |
Rules prefixed with ! are treated as not allowed and go back to fallback redirect. |
| 3 | allowed_real_paths |
Matched path is restored to the same real path instead of the default redirect path. |
| 4 | Fallback redirect | Other storage paths are moved under the default redirect base. |
How user-input relative paths are resolved:
0:path_mappings.request_path = "DCIM/MyApp" -> /storage/emulated/0/DCIM/MyApppath_mappings.final_path = "Pictures/MyApp" -> /storage/emulated/0/Pictures/MyAppallowed_real_paths = "Download/Public" -> /storage/emulated/0/Download/Public10, the same config becomes:/storage/emulated/10/DCIM/MyApp/storage/emulated/10/Pictures/MyApp/storage/emulated/10/Download/Publicpath_mappings example:
DCIM/MyApp -> Pictures/MyApp/storage/emulated/0/DCIM/MyApp/a.jpg/storage/emulated/0/Pictures/MyApp/a.jpgIf no rule matches:
/storage/emulated/0/Download/a.txt/storage/emulated/0/Android/data/com.example/sdcard/Download/a.txtIf path matches allowed_real_paths:
Download/Public/storage/emulated/0/Download/Public/a.txtIf an exclusion rule matches:
Download/*!Download/Private/storage/emulated/0/Download/Private/a.txt/storage/emulated/0/Android/data/com.example/sdcard/Download/Private/a.txtallowed_real_paths and path_mappings Are Both SetThey can be used together. Practical behavior:
allowed_real_paths restores listed directories to real paths, while path_mappings rewrites listed request directories to different final directories.DCIM, and map DCIM/MyApp -> Pictures/MyApp. Other files under DCIM stay in real DCIM, but DCIM/MyApp goes to mapped target.Download/Public, and map Download/Public -> Documents/Public. Final path follows mapping target Documents/Public.This matches implementation order: runtime routing evaluates mappings before allowed rules, and mount preparation restores allowed paths before applying mappings.
Config directory on device:
/data/adb/modules/storage.redirect.x/config/
├─ global.json
└─ apps/
└─ <package_name>.json
global.json{
"file_monitor_enabled": true,
"fuse_fixer_enabled": false
}
file_monitor_enabled: enables file operation monitoring for MediaProvider and redirected apps.fuse_fixer_enabled: enables the optional MediaProvider FUSE path compatibility hook.apps/<package>.json Template{
"users": {
"0": {
"enabled": true,
"allowed_real_paths": [
"Download/MyApp",
"Documents/MyApp",
"Pictures/*",
"!Pictures/Private"
],
"path_mappings": {
"DCIM/MyApp": "Pictures/MyApp"
}
}
}
}
path_mappings also accepts the compatibility array form:
{
"path_mappings": [
{
"request_path": "DCIM/MyApp",
"final_path": "Pictures/MyApp"
}
]
}
/storage/emulated/<userId>./.. or .. path segments.Android/data, Android/media, or Android/obb; the manager rejects Android/data.allowed_real_paths supports * and ? wildcards.allowed_real_paths supports ! exclusion prefix, for example !Download/Private.path_mappings only accepts plain relative paths; !, *, and ? are not supported there.request_path and final_path cannot be identical.config/apps are watched and effective config changes automatically stop affected packages.Reload Redirect, restart affected apps, or reboot the device.{
"users": {
"0": {
"enabled": true
}
}
}
{
"users": {
"0": {
"enabled": true,
"allowed_real_paths": ["Download/OwnerOnly"]
},
"10": {
"enabled": true,
"allowed_real_paths": ["Download/WorkProfile"],
"path_mappings": {
"Movies/Input": "Movies/WorkMapped"
}
}
}
}
Supported ABIs:
arm64-v8ax86_64Build Zygisk module:
PYTHONUTF8=1 python scripts/build.py build-zygisk
PYTHONUTF8=1 python scripts/build.py build-zygisk --debug
PYTHONUTF8=1 python scripts/build.py build-zygisk --abi arm64-v8a
PYTHONUTF8=1 python scripts/build.py build-zygisk --abi x86_64 --debug
The module zip is written to build/zygisk/ as:
Storage-Redirect-X_v<version>-<abi-or-zygisk>.zip
Build APK:
PYTHONUTF8=1 python scripts/build.py build-apk
PYTHONUTF8=1 python scripts/build.py build-apk --abi arm64-v8a
The release APK is moved to build/apk/ as:
Storage-Redirect-X_<version>_<abi-or-universal>.apk
Build all:
PYTHONUTF8=1 python scripts/build.py build-all
PYTHONUTF8=1 python scripts/build.py build-all --debug
Useful options:
--abi arm64-v8a|x86_64: build only one ABI.--debug: keep symbols, disable LTO, and skip strip for easier crash stack analysis.--ndk <path>: specify Android NDK path for module builds.-v: show verbose build logs.javac availabled8) and platform android-37arm64-v8a / x86_64>= 1.93.1) with Android targets installed$ claude mcp add Storage-redirection-X-Public \
-- python -m otcore.mcp_server <graph>