How to pull an APK file off an Android device with adb
The question comes up constantly, and the classic version is this Stack Overflow thread: how do I get the APK of an app that is already installed on my phone? The short answer is two commands. adb shell pm path <package> tells you where the APK lives on the device, and adb pull <that path> copies it to your computer. That is the whole trick, and it works without root on the devices I have used.
The longer answer is that on newer Android, one app is often several APK files, not one. If you only pull the first file you get a “base” APK that will not install on its own. This tutorial covers both cases, with the exact output you should see at each step.
This is for developers, QA people and operators who manage their own test phones and need a copy of an app they own or are authorised to test: your own build, a client’s build, a sideloaded internal tool. I will not cover getting around any app’s protections, and you should only pull APKs you have the right to copy. This is not legal advice, and app licences vary.
What you need
- a computer running Windows, macOS or Linux. I use Windows 10 with PowerShell, and the commands are the same elsewhere unless I say so.
- Android platform-tools, which contains
adb. Google distributes it free from the Android developer adb page. No account is needed. - an Android phone with developer options on and USB debugging enabled. If the toggle is greyed out, see fixing a greyed out USB debugging toggle on Android.
- a USB cable that carries data. Charge-only cables are the most common reason a phone never shows up.
- the app already installed on the phone. You cannot pull what is not there.
- cost: nothing, beyond the phone you already own.
Step by step
Step 1: confirm adb sees the device
Plug in the phone, accept the “Allow USB debugging?” prompt on its screen, then run:
adb devices
Expected output:
List of devices attached
R58M12ABCDE device
The serial will differ. The word that matters is device.
If it breaks: unauthorized means you have not accepted the prompt on the phone, so unplug, replug and look at the screen. An empty list usually means a charge-only cable, a missing USB driver on Windows, or the phone is set to “charging only” in its USB mode notification. If you have more than one device attached, add -s <serial> to every command below.
Step 2: find the package name
adb works with package names like com.example.app, not the label on the home screen. List third-party apps and filter:
adb shell pm list packages -3
On PowerShell, narrow it down with:
adb shell pm list packages -3 | Select-String "chrome"
On macOS or Linux use | grep chrome. Expected output looks like:
package:com.example.notes
package:com.example.scanner
The -3 flag limits the list to apps you installed, skipping system packages. Drop it if you want a system app.
If it breaks: if your app is not listed, it may be a system app (remove -3), or it may be installed only in a work profile or secondary user. In that case add --user <id>, and find ids with adb shell pm list users.
Step 3: ask the device where the APK is
This is the step that answers the Stack Overflow question:
adb shell pm path com.example.notes
Expected output for a simple, single-file app:
package:/data/app/~~Xk3l9fQ2example==/com.example.notes-Zm41example==/base.apk
The random-looking directory names are generated at install time and change if the app is reinstalled. Do not hard-code them in scripts. Always ask pm path fresh.
If it breaks: empty output with no error means the package name is wrong or not installed for the current user. Go back to step 2 and copy the name exactly.
Step 4: pull the file
Strip the package: prefix and give the path to adb pull:
adb pull /data/app/~~Xk3l9fQ2example==/com.example.notes-Zm41example==/base.apk notes.apk
Expected output:
/data/app/~~Xk3l9fQ2example==/com.example.notes-Zm41example==/base.apk: 1 file pulled, 0 skipped. 38.2 MB/s (14233104 bytes in 0.355s)
Your speed and byte count will differ. The destination name notes.apk is optional, but without it you get base.apk, and every app pulled that way has the same filename.
If it breaks: “remote object does not exist” nearly always means a typo in the path, often from copying the package: prefix or a trailing carriage return. “Permission denied” is rare for apps in /data/app, because those files are world-readable on the stock builds I have used, but some vendor builds are stricter. If you hit it, the clean fix is to build or export the APK from source, not to go hunting for workarounds.
Step 5: check whether the app is split
Look again at the pm path output. If it returned exactly one line ending in base.apk, you are done, and you can skip to step 7. If it returned several lines, the app is split:
package:/data/app/~~Xk3l9fQ2example==/com.example.notes-Zm41example==/base.apk
package:/data/app/~~Xk3l9fQ2example==/com.example.notes-Zm41example==/split_config.arm64_v8a.apk
package:/data/app/~~Xk3l9fQ2example==/com.example.notes-Zm41example==/split_config.xxhdpi.apk
package:/data/app/~~Xk3l9fQ2example==/com.example.notes-Zm41example==/split_config.en.apk
This happens because apps published as Android App Bundles are delivered as a base APK plus configuration splits for your CPU architecture, screen density and language. Google explains the format in its Android App Bundle documentation. The base APK alone usually will not install or run, because the native libraries and resources sit in the splits.
If it breaks: if you are unsure, count the lines. More than one line means split. Pulling only the first one is the most common mistake I see with this question.
Step 6: pull every split into one folder
On PowerShell:
mkdir notes-apks
adb shell pm path com.example.notes | ForEach-Object { $_.Trim() -replace '^package:','' } | ForEach-Object { adb pull $_ notes-apks/ }
On macOS or Linux:
mkdir notes-apks
adb shell pm path com.example.notes | tr -d '\r' | sed 's/^package://' | while read -r p; do adb pull "$p" notes-apks/; done
Expected result: one “1 file pulled” line per APK, and a folder containing base.apk plus each split_config.*.apk.
If it breaks: on some older adb and Windows combinations, adb shell output ends lines with \r\n, so the path carries an invisible carriage return and the pull fails with “does not exist”. The Trim() and tr -d '\r' pieces above are there for that reason. Updating platform-tools also helps.
Step 7: reinstall to prove the pull worked
A file that exists is not the same as a file that installs. Test it on a spare device or after uninstalling. For a single APK:
adb install notes.apk
For splits, install them together in one session:
adb install-multiple notes-apks/*.apk
On PowerShell, expand the files first: adb install-multiple (Get-ChildItem notes-apks\*.apk).FullName. Expected output in both cases is Success.
Note that the device only needs the splits that match it. If you pull from an arm64 phone and install on an x86 emulator, the architecture split will be wrong. Match the target to the source.
If it breaks: INSTALL_FAILED_VERSION_DOWNGRADE means a newer version is already installed, and downgrading an installed Android app with adb install -d covers that. On Xiaomi and Redmi phones, INSTALL_FAILED_USER_RESTRICTED is a separate setting and has its own fix in fixing install failed user restricted when adb installing on Xiaomi and Redmi phones.
Step 8: wrap it in a small script
Once you do this twice, script it. Here is the bash version I keep around:
#!/usr/bin/env bash
# usage: ./pullapk.sh com.example.notes [serial]
pkg="$1"
serial="${2:+-s $2}"
out="apks/$pkg"
mkdir -p "$out"
paths=$(adb $serial shell pm path "$pkg" | tr -d '\r' | sed 's/^package://')
[ -z "$paths" ] && { echo "not installed: $pkg"; exit 1; }
echo "$paths" | while read -r p; do
adb $serial pull "$p" "$out/"
done
adb $serial shell dumpsys package "$pkg" | grep -m1 versionName
ls -l "$out"
The versionName line is worth keeping. A folder of APKs without a version note becomes unreadable within a month. I also drop the output of dumpsys package into a text file next to the APKs when I archive a build.
If it breaks: if grep finds nothing, the package is not installed for that user, same as step 3.
Common pitfalls
- pulling only
base.apkfrom a split app. It copies fine and fails at install. Always count the lines frompm path. - hard-coding the install directory. The
~~random==part changes on reinstall and sometimes after an update, so resolve the path every run. - forgetting
-s <serial>with several phones attached. adb will refuse with “more than one device/emulator”, or worse, you pull from the wrong phone in a script that sets a default. Pass the serial every time. - trying
adb backupinstead. It is deprecated and many apps opt out of it, so it is the wrong tool for extracting an APK.pm pathplusadb pullis the supported route, and the adb documentation is the place to check current flags. - pulling an app you have no right to copy. A free app on the Play Store is still someone’s software. Keep this to your own builds, internal tools and apps you are authorised to test, and read the licence if in doubt.
Scaling this
At 10 phones, the script above is enough. Loop over adb devices serials, call pullapk.sh for each, and keep a spreadsheet of serial, package, versionName and date pulled. Your real problems at this size are cables and USB hubs, not adb.
At 100 phones, the device list itself becomes the hard part. A powered hub per 10 to 15 phones, a naming scheme for serials, and a rule that every pulled APK goes into a folder named by package and version. Host machines start to matter too, so read host limits for running many Android phones before you buy hubs. Hardware trouble hides in the adb output: devices flip between offline and device, and your script should retry once and log the failure, not stop the whole run. A tool from the open source device farm list can handle inventory so you are not maintaining it by hand.
At 1000, nobody is pulling APKs by hand from phones. You pull once from a reference device, store the splits in versioned artifact storage, and install from there. The phone becomes a target, not a source. If you own the app, skip the phone entirely and take the APK or bundle from your CI build, which is more reliable than extracting it from a device.
One practical note if you do not want to keep physical phones on a desk. cloudf.one rents real Android phones in Singapore on dedicated hardware, each with a persistent Singapore mobile IP, controlled from the browser. There is no self-service adb on it. adb and Appium reach a rented phone only through a WireGuard tunnel that we set up on request, by emailing [email protected]. So it suits you if you need a Singapore device to test an app you are authorised to test, and not if you need instant adb access. It is Singapore-only, so it is no help if you need a device in another country.
Where to go next
- downgrading an installed Android app with adb install -d: once you have a pulled APK, this is how you put an older version back for regression testing.
- fixing a greyed out USB debugging toggle on Android: if step 1 never gets going because the toggle will not switch on.
- best open source Android device farm tools: for when the folder of APKs and a shelf of phones starts to need real inventory.
The full index is at /blog/.
Written by Xavier Fok
disclosure: this article may contain affiliate links. if you buy through them we may earn a commission at no extra cost to you. verdicts are independent of payouts. last reviewed by Xavier Fok on 2026-10-05.