Tailscale deploy adb
Skill andrew-malitchuk/tailscale-deploy-adb/skills/tailscale-deploy-adb
Claude Code skill - deploy Android debug builds to a physical device over Tailscale + Wireless ADB, no USB required
npx -y skills add andrew-malitchuk/tailscale-deploy-adb --skill tailscale-deploy-adbAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Deploy Android debug build to a physical device over Tailscale + Wireless ADB. Use when user wants to install a fresh APK on their phone remotely (from gym, cafe, anywhere outside home Wi-Fi). Triggers: "tailscale deploy", "deploy adb", "install on phone", "deploy to phone", "deploy debug build", "push apk to device", "wireless deploy".
SKILL.md
14.4 KB, as published. Nobody here has run it
Deploys an Android debug build to a physical device via Tailscale + Wireless ADB. Reads per-project config from .tailscale-deploy-adb.json in the project root (current working directory).
Reference files (load on demand):
references/tailscale-resolve.md— Tailscale peer resolution, MagicDNS, troubleshooting offline peersreferences/adb-troubleshoot.md— Wireless ADB port fixation, commonadb connectfailures, cheat-listreferences/config-schema.md— full config schema, examples, how to findapplication_idandmain_activity
Step 0 — Check Tailscale
Before anything else, verify Tailscale is running on the Mac:
tailscale status >/dev/null 2>&1 && echo "running" || echo "stopped"
- running → proceed to Determine mode.
- stopped → Stop immediately. Tell user:
❌ Tailscale is not running on this Mac. Open the Tailscale app in the menu bar (or run
open -a Tailscale), connect, then retry.
Determine mode
Inspect the user's message for these keywords:
| Keyword | Mode |
|---|---|
dry-run, test connection, check connection, connection only | dry-run |
debug, attach debugger, debug mode, deploy with debugger, debug deploy | debug |
| Everything else | deploy |
dry-run: execute Steps 1–4 only. Skip Step 5. Jump to Step 8 (connection report).
debug: same as deploy but passes --debug-launch to deploy.sh — app launches suspended, waiting for JDWP debugger from Android Studio.
deploy: execute all Steps 1–8 with normal launch.
Step 1 — Validate environment
Run:
for cmd in tailscale adb jq; do
command -v "$cmd" >/dev/null 2>&1 && echo "OK: $cmd" || echo "MISSING: $cmd"
done
[ -f ./gradlew ] && echo "OK: gradlew" || echo "MISSING: ./gradlew"
If any line starts with MISSING:, stop and tell the user (all at once):
MISSING: tailscale→ not in PATH. macOS fix:sudo ln -s /Applications/Tailscale.app/Contents/MacOS/Tailscale /usr/local/bin/tailscaleMISSING: adb→ install platform-tools:brew install --cask android-platform-toolsMISSING: jq→brew install jqMISSING: ./gradlew→ "Run this skill from the Android project root (wheregradlewlives)"
Do not proceed until all tools are available.
Step 1.5 — Enable ADB TCP mode via USB
Android's Wireless Debugging (Developer Options) only works over WiFi. To deploy over any network — including cellular — the ADB daemon must be switched to TCP mode via USB. This must be done once per device boot.
Run:
adb devices
If the output shows a USB-connected device (state = device):
- Automatically run:
adb tcpip 5555 - Tell user:
✅ ADB TCP mode enabled on port 5555. Disconnect the USB cable now and proceed.
- Proceed to Step 2.
If no USB device is found (empty list, or only wireless/offline entries):
Display this mandatory notice and ask for confirmation via AskUserQuestion (single question):
⚠️ USB prerequisite required
Wireless ADB without WiFi requires TCP mode to be enabled first. This is a one-time step per device boot.
Steps (do this now):
- Connect your phone to this Mac via USB cable
- Confirm "Allow USB debugging" on the phone if prompted
- Run in terminal:
adb tcpip 5555- Disconnect USB — ADB now listens on port 5555 over any network
After phone reboot: this step must be repeated before deploying.
Question options:
- label
Done — ran adb tcpip 5555 via USB, descriptionready to continue - label
Skip — I'm on WiFi right now, descriptionwill use Wireless Debugging instead (WiFi only) - label
Cancel, descriptionstop here
If user selects Done: proceed to Step 2.
If user selects Skip: proceed to Step 2 (WiFi path — Wireless Debugging in Developer Options must be ON).
If user selects Cancel: stop gracefully with message: Deploy cancelled. Run the skill again when ready.
Step 2 — Load or create config
Use the Read tool to read .tailscale-deploy-adb.json from the current directory.
If the file exists, extract these fields:
| Field | Type | Default |
|---|---|---|
device_hostname | string | required |
application_id | string | required |
main_activity | string | required |
variant | string | "Debug" |
adb_port | integer | 5555 |
launch_after_install | boolean | true |
logcat_seconds | integer | 10 |
If the file does not exist, follow Steps 2a → 2b → 2c below.
Step 2a — Auto-detect values from the project
Run all three commands (suppress errors — detection may find nothing):
# Android peers in Tailscale (hostname|online-status)
tailscale status --json 2>/dev/null \
| jq -r '(.Peer // {}) | to_entries[] | .value | select(.OS == "android") | "\(.HostName)|\(if .Online then "online" else "offline" end)"' \
2>/dev/null || true
# Application IDs from Gradle build files
grep -rh "applicationId" --include="*.gradle" --include="*.gradle.kts" . 2>/dev/null \
| grep -v "//\|test\|Test" \
| grep -oE '"[a-z][a-zA-Z0-9._]+"' \
| tr -d '"' | sort -u | head -4 || true
# Launcher Activity from AndroidManifest
grep -rh "android:name" --include="AndroidManifest.xml" . 2>/dev/null \
| sed 's/.*android:name="\([^"]*\)".*/\1/' \
| grep -v "^\." | grep -iE "Activity|activity" | head -3 || true
Step 2b — Present choices via AskUserQuestion
Call AskUserQuestion with exactly 4 questions in a single invocation:
Q1 — Device hostname (header: "Device")
Build options from command 1 output. Each detected peer becomes one option (label = hostname, description = "online" or "offline in Tailscale"). If no peers detected: use label my-phone, description enter your Tailscale hostname. Always 2–4 options max (the tool appends "Other" for free-text input automatically).
Q2 — Application ID (header: "App ID")
Build options from command 2 output. For each detected base ID: add it as one option (description = "from build.gradle"). If it does not already end in .debug, also add <base-id>.debug as a second option (description = "debug variant via applicationIdSuffix"). If nothing detected: use label com.example.myapp.debug, description enter your debug package name.
Q3 — Main Activity (header: "Activity")
Build options from command 3 output. Each detected class becomes one option (description = "from AndroidManifest.xml"). If nothing detected: use label com.example.myapp.MainActivity, description enter your launcher Activity class.
Q4 — ADB port (header: "ADB Port")
- label
5555, descriptiondefault after running adb tcpip 5555 via USB (Recommended) - label
Custom, descriptionenter your port — visible in Developer Options → Wireless Debugging on the device
If user selects "Custom" or types "Other", ask them to provide the exact port number.
Always ask this question explicitly — Android randomizes the port after each pairing session unless fixed via adb tcpip 5555. See references/adb-troubleshoot.md.
Step 2c — Handle optional settings
After the 4-question call, use these defaults for remaining fields:
variant="Debug", launch_after_install=true, logcat_seconds=10
To change these later: edit .tailscale-deploy-adb.json directly, or delete the file and re-run the skill.
Step 2d — Write config file
Write .tailscale-deploy-adb.json via Write tool (pretty-printed, 2-space indent) with all collected values. Tell user:
✅ Config saved:
.tailscale-deploy-adb.json
Step 3 — Resolve device IP via Tailscale
Run (replace DEVICE_HOSTNAME with the value from config):
bash ~/.claude/skills/tailscale-deploy-adb/scripts/resolve-device.sh "DEVICE_HOSTNAME"
Interpret exit codes:
- 0 → capture stdout as
DEVICE_IP. Proceed. - 1 → peer not found. Stop. Tell user: verify hostname with
tailscale status. Loadreferences/tailscale-resolve.md. - 2 → peer offline. Stop. Tell user:
❌ Device
DEVICE_HOSTNAMEis offline in Tailscale. Check: Is Tailscale active on the phone? Is the screen on? Details:references/tailscale-resolve.md - 3 → Tailscale not running on Mac. Stop. Tell user to open Tailscale app.
Step 4 — Verify ADB connectivity (pre-flight)
Run (replace placeholders with actual values):
bash ~/.claude/skills/tailscale-deploy-adb/scripts/deploy.sh \
--ip "DEVICE_IP" \
--port "ADB_PORT" \
--connect-only
Interpret exit codes:
- 0 → ADB connected. The device is now also visible in Android Studio's device selector (both tools share the same ADB server). In
dry-runmode → jump to Step 8. Indeploymode → proceed to Step 5. - 1 → Stop. Tell user:
❌
adb connect DEVICE_IP:ADB_PORTfailed after 3 attempts. Most likely cause: ADB TCP mode not enabled. Connect phone via USB and runadb tcpip 5555, then retry. Also check: Is Tailscale active on the phone? Is the screen on? Full checklist:references/adb-troubleshoot.md
Step 5 — Build, install, launch, and logcat
(Skip in dry-run mode)
Construct the command from config values (replace all uppercase placeholders; set LAUNCH_AFTER_INSTALL to the string "true" or "false").
deploy mode:
bash ~/.claude/skills/tailscale-deploy-adb/scripts/deploy.sh \
--ip "DEVICE_IP" \
--port "ADB_PORT" \
--variant "VARIANT" \
--app-id "APPLICATION_ID" \
--activity "MAIN_ACTIVITY" \
--launch-app "LAUNCH_AFTER_INSTALL" \
--logcat-seconds "LOGCAT_SECONDS"
debug mode — add --debug-launch flag (starts app suspended, waiting for JDWP debugger):
bash ~/.claude/skills/tailscale-deploy-adb/scripts/deploy.sh \
--ip "DEVICE_IP" \
--port "ADB_PORT" \
--variant "VARIANT" \
--app-id "APPLICATION_ID" \
--activity "MAIN_ACTIVITY" \
--launch-app "true" \
--debug-launch \
--logcat-seconds "LOGCAT_SECONDS"
Capture all output. Interpret exit codes:
- 0 → success. Proceed to Step 8.
- 1 → ADB dropped during install (phone may have slept). Stop. Tell user to wake phone and retry.
- 2 → Gradle build failed. Script printed last 50 build-log lines to stderr. Stop. Show those lines to the user.
- 3 → APK install failed. Stop. Tell user to run
adb devicesand check forunauthorizedstate.
Step 6 — Launch app
Handled internally by deploy.sh when --launch-app true is passed. No separate action needed.
Step 7 — Logcat tail
Handled internally by deploy.sh when --logcat-seconds N > 0 is passed. No separate action needed.
Step 8 — Final report
dry-run success:
✅ Connection successful ──────────────────────────────────────── Device:
DEVICE_HOSTNAME→DEVICE_IP:ADB_PORTTailscale: online ✓ ADB state:device✓ ──────────────────────────────────────── Ready to deploy. Run withoutdry-runto build and install the APK.
deploy success (parse deploy.sh stdout for summary values):
✅ Deploy complete ──────────────────────────────────────── Device:
DEVICE_HOSTNAME(DEVICE_IP:ADB_PORT) APK:VARIANT›APPLICATION_IDLaunched: yes / no Logcat: N critical line(s) (E/ FATAL) ────────────────────────────────────────
If there were critical logcat lines, show them below the summary box.
debug success — app is suspended, waiting for debugger:
🐛 Deploy complete — app waiting for debugger ──────────────────────────────────────── Device:
DEVICE_HOSTNAME(DEVICE_IP:ADB_PORT) APK:VARIANT›APPLICATION_IDState: suspended (waiting for JDWP debugger) ──────────────────────────────────────── How to attach in Android Studio:
- Run → Attach Debugger to Android Process
- Select process
APPLICATION_ID- Click OK — app will resume
Error handling
- Tailscale offline: stop at Step 3, tell user what to check. Never retry automatically — device state is external.
- adb connect failure: 3 retries are inside
deploy.sh. If all fail → showreferences/adb-troubleshoot.mdcheat-list. - Gradle build failure: show last 50 build log lines immediately. Never attempt launch.
- pidof race:
deploy.shretries once after 1s. If pidof still returns empty (app may have crashed), logcat is skipped with a warning. - jq missing: caught in Step 1. Scripts never execute without
jq.
Notes
- Always invoke from the Android project root (where
gradlewand.tailscale-deploy-adb.jsonlive). - Android Studio: after
adb connectsucceeds, the device appears automatically in AS's device selector — both tools share the same ADB server daemon.deploy.shcallsadb start-serverbefore connecting to ensure the daemon is alive. - Wireless Debugging prerequisite: must be enabled on the device before connecting. Settings → Developer Options → Wireless Debugging → ON. This toggle can reset after OS updates.
- Wireless ADB port: Android randomizes the port after each pairing session. Fix it permanently (until reboot) with
adb tcpip 5555over USB. If you use a non-5555 port, record it inadb_portin the config. Details inreferences/adb-troubleshoot.md. - ADB serial for wireless is
<ip>:<port>. Alladb -scalls in scripts include the port. application_idfor debug builds often has a.debugsuffix (applicationIdSuffixinbuild.gradle). Confirm viabuild/outputs/apk/debug/output-metadata.jsonoradb shell pm list packages | grep your-app.