Automated mobile app UI spider — crawls and documents screens, elements, and navigation flows. Works on any installed app, no source code access required.
# Install
pip install -e .
# Crawl an app (requires Android emulator with ADB)
export ANTHROPIC_API_KEY=your-key
nativeappspider crawl com.example.app
# Generate report from existing crawl
nativeappspider report output/com.example.app_20240101_120000/- Python 3.12+
- Android emulator or device connected via ADB
ANTHROPIC_API_KEYenvironment variable set- Default model:
claude-sonnet-4-6(configurable via--model,--analysis-model,--decision-model)
You need ADB and an Android emulator. Two options:
Download Android Studio. It includes the emulator, AVD manager, and ADB. After install:
- Open Android Studio → Tools → Device Manager
- Create a new virtual device (e.g. Pixel 7, API 34)
- Launch the emulator (DNS works automatically when launched with a GUI)
- ADB is at
~/Library/Android/sdk/platform-tools/adb— add it to your PATH:export PATH="$HOME/Library/Android/sdk/platform-tools:$PATH"
Lighter setup, no IDE needed. Requires Homebrew:
# Install Android command-line tools
brew install --cask android-commandlinetools
# Install platform-tools (ADB), emulator, and a system image
sdkmanager "platform-tools" "emulator" "platforms;android-34" \
"system-images;android-34;google_apis;arm64-v8a"
# Create an emulator (AVD)
avdmanager create avd -n appspider_test \
-k "system-images;android-34;google_apis;arm64-v8a"
# Launch the emulator (headless, with DNS configured)
emulator -avd appspider_test -no-audio -no-window -dns-server 8.8.8.8,8.8.4.4Important: The -dns-server flag is required for apps that load network content (maps, API data, etc.). Without it, the headless emulator has IP connectivity but no DNS resolution, so network requests will fail silently.
If you need a specific screen resolution (e.g. for accurate tap coordinates):
adb shell wm size 1080x1920
adb shell wm density 420To set GPS location (e.g. Bristol, UK — useful for location-based apps):
adb emu geo fix -2.5879 51.4545adb devices # Should list your emulator (e.g. "emulator-5554 device")
adb shell wm size # Should return display dimensions
adb shell ping google.com # Should resolve and get responses (verifies DNS)# Basic crawl
nativeappspider crawl com.example.app
# Limit scope
--max-screens 20 --max-actions 50
# Target a specific device
--serial emulator-5554
# Fresh start (clears app data so it launches from the initial screen)
--fresh
# Skip specific flows (e.g. avoid registration/login during exploration)
--avoid registration --avoid login --avoid "sign up"
# Auto-dismiss dialogs (consent banners, cookie popups, onboarding overlays)
--dismiss consent --dismiss cookie --dismiss onboarding
# Navigate to a specific screen first, then explore from there
--focus map
# Combine focus, avoid, and dismiss for targeted crawling
--focus settings --avoid login --dismiss consent
# Use a cheaper model for navigation decisions to reduce costs
--decision-model claude-haiku-4-5
# Use a specific model for both analysis and decisions
--model claude-haiku-4-5
# Resume a previous crawl with a higher budget
--continue output/com.example.app_20240101_120000/ --max-screens 30
# Use a YAML config file instead of CLI flags
nativeappspider crawl --config examples/zapmap.yaml
# Verbose logging
nativeappspider -v crawl com.example.app
# Regenerate report from previous crawl data
nativeappspider report output/com.example.app_20240101_120000/Instead of passing many CLI flags, use a YAML config file:
# examples/myapp.yaml
package: com.example.app
max_screens: 10
max_actions: 60
fresh: true
focus: settings menu
scroll_discovery: false
avoid:
- registration
- login
dismiss:
- consent
- privacy
- cookie
# Optional: use a cheaper model for navigation decisions
decision_model: claude-haiku-4-5Run with: nativeappspider crawl --config examples/myapp.yaml
The crawler includes several heuristics to improve efficiency and avoid getting stuck:
- Force-stop before launch — the app is force-stopped before each crawl so it always starts from its main activity, even without
--fresh. App data is preserved. - Breadth-first from focus screen — after reaching the
--focustarget, the crawler systematically tries each untried element on that screen before going deep into any one path. - Screen name deduplication — if Claude identifies a screen with the same name as one already recorded (e.g. a form with different fields focused), it's treated as a revisit instead of consuming a new screen slot.
- Text field filtering — text input fields (EditText, TextInputEditText, etc.) are excluded from navigation decisions. They're still documented in the screen analysis, but the crawler won't waste actions tapping into form fields.
- Toxic screen detection — screens that repeatedly cause the app to leave (e.g. photo pickers that open a system activity) are automatically skipped after 2 relaunches.
- Auto-dismiss app dialogs — screens matching
--dismisskeywords are dismissed automatically by tapping accept/close buttons, preventing the crawler from getting stuck on consent banners or cookie popups that don't respond to the back button. - Ad region masking — known ad SDK elements are masked in screenshots before perceptual hashing, so rotating ads don't cause the same screen to be treated as new.
Each crawl produces a timestamped directory:
output/com.example.app_20240101_120000/
├── screenshots/ # PNG screenshot of each unique screen
├── screens.json # Screen names, descriptions, elements, activity names
├── transitions.json # Navigation edges between screens
├── flow.mmd # Mermaid diagram of the navigation graph
└── report.html # Self-contained visual report (open in browser)
# Install dependencies
uv sync
# Run tests (71 unit tests, ~2s, no device needed)
uv run pytest tests/unit/ -v
# Lint
uv run ruff check src/