<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Tutorial on ErrorZap</title><link>https://errorzap.com/tags/tutorial/</link><description>Recent content in Tutorial on ErrorZap</description><image><title>ErrorZap</title><url>https://errorzap.com/og.png</url><link>https://errorzap.com/og.png</link></image><generator>Hugo</generator><language>en-US</language><copyright>ErrorZap</copyright><lastBuildDate>Sat, 26 Sep 2026 20:30:00 -0600</lastBuildDate><atom:link href="https://errorzap.com/tags/tutorial/index.xml" rel="self" type="application/rss+xml"/><item><title>I Handed My AI the Keys to My Android Phones</title><link>https://errorzap.com/posts/letting-my-ai-drive-my-android-phones/</link><pubDate>Sat, 26 Sep 2026 20:30:00 -0600</pubDate><guid>https://errorzap.com/posts/letting-my-ai-drive-my-android-phones/</guid><description>How Claude and I set up remote ADB over Tailscale that survives every reboot, so my AI can fix settings, clean up notifications and push app updates to my phones from anywhere. Plus the traps we fell into on the way.</description><content:encoded><![CDATA[<p>I have two phones, a family phone I help look after, and an AI assistant that lives on my PC. For months, every phone fix went the same way. Claude would tell me what to tap, I&rsquo;d tap it, I&rsquo;d read the screen back, and we&rsquo;d do it again.</p>
<p>That&rsquo;s backwards. The AI is the one that knows the fix. It should be doing the tapping.</p>
<p>So we set it up so it can. Claude can now reach any of my phones from anywhere, over my private network, after every reboot, and change them for me. It fixes settings, silences noisy notification channels, installs new app builds and takes screenshots. This is how we built it, including the parts that fought back.</p>
<p>The scripts are on GitHub: <strong><a href="https://github.com/pueblokc/android-remote-adb-tailscale">pueblokc/android-remote-adb-tailscale</a></strong>.</p>
<h2 id="the-idea-in-one-paragraph">The idea in one paragraph</h2>
<p>Android already ships a remote-control protocol: <strong>ADB</strong>, the Android Debug Bridge. With a cable, <code>adb shell</code> gets you a command line on the phone and <code>adb install</code> pushes apps. Android 11 and newer can also do it over Wi-Fi (&ldquo;Wireless debugging&rdquo;). Put the phone on <strong><a href="https://tailscale.com/">Tailscale</a></strong>, a private mesh VPN, and &ldquo;over Wi-Fi&rdquo; becomes &ldquo;from anywhere, and only for my own devices.&rdquo; Then the AI on my PC runs <code>adb connect &lt;phone&gt;:5555</code> and gets to work.</p>
<p>That&rsquo;s the whole plan. Android had several objections.</p>
<h2 id="problem-1-the-door-keeps-moving-and-it-locks-behind-you">Problem 1: the door keeps moving, and it locks behind you</h2>
<p><img alt="The host holds a cable in a dark alley, confused, while a power-button gremlin yanks the plug out next to a door whose number spins like a slot machine." loading="lazy" src="/posts/letting-my-ai-drive-my-android-phones/the-problem.webp"></p>
<p>Wireless debugging has two habits that make it useless for remote work:</p>
<ol>
<li><strong>It listens on a random port</strong>, a new one each time it turns on. You can&rsquo;t <code>adb connect</code> to a port you don&rsquo;t know.</li>
<li><strong>It turns itself off at every reboot.</strong> A phone update, a crash, or a dead battery, and your remote access is gone until someone walks over and flips the switch.</li>
</ol>
<p>The classic workaround is <code>adb tcpip 5555</code>. That tells the phone&rsquo;s ADB daemon to listen on fixed port 5555 on every network interface, Tailscale included. It works right up until the next reboot, then it&rsquo;s gone too.</p>
<p>So the job is really: <strong>at every boot, put all of this back without a human.</strong></p>
<h2 id="the-fix-a-three-part-relay-that-runs-every-boot">The fix: a three-part relay that runs every boot</h2>
<p><img alt="A cutaway of a phone: a small robot with a checklist flips a lever, a ball rolls to a terminal, and a gate opens onto a glowing teal tunnel." loading="lazy" src="/posts/letting-my-ai-drive-my-android-phones/boot-chain.webp"></p>
<p>No single app could do the whole job, so the work is split three ways:</p>
<table>
	<thead>
			<tr>
					<th>Step</th>
					<th>Who does it</th>
					<th>What it does</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>1</td>
					<td><strong>Tasker</strong> (boot profile)</td>
					<td>Sets <code>adb_wifi_enabled = 1</code>, which turns Wireless debugging back on</td>
			</tr>
			<tr>
					<td>2</td>
					<td><strong>Termux:Boot</strong> running <code>kc-adb-boot.sh</code></td>
					<td>Finds Wireless debugging&rsquo;s random port, connects to <em>itself</em> on <code>127.0.0.1</code>, and runs <code>adb tcpip 5555</code></td>
			</tr>
			<tr>
					<td>3</td>
					<td><strong>Tailscale</strong> as Always-on VPN, plus a small keeper script</td>
					<td>Keeps the phone on the tailnet so <code>&lt;tailscale-ip&gt;:5555</code> is reachable from my PC</td>
			</tr>
	</tbody>
</table>
<p>Step 2 is the fun part. The phone runs its own ADB client inside Termux, uses it to talk to its own Wireless-debugging port, and uses that connection to open 5555. The boot script finds the random port through mDNS (<code>adb mdns services</code>). If that comes up empty, it falls back to an <code>nmap</code> scan of ports 30000–49999 on localhost. It retries for about seven minutes, then logs to <code>/sdcard/Download/kc-adb-boot.log</code> so you can see what happened.</p>
<h3 id="why-tasker-and-not-just-termux">Why Tasker and not just Termux?</h3>
<p>This is the first trap we hit. Termux can be granted <code>WRITE_SECURE_SETTINGS</code>, and it <em>still</em> can&rsquo;t flip the Wireless-debugging switch. <code>settings put global adb_wifi_enabled 1</code> from Termux dies with <strong>&ldquo;Failed transaction&rdquo;</strong>. Tasker&rsquo;s <em>Custom Setting</em> action does the same write and it works. So Tasker turns the switch on, and Termux does everything else. (The boot script still tries it; you&rsquo;ll see that &ldquo;Failed transaction&rdquo; line in the log, and it&rsquo;s harmless.)</p>
<h3 id="why-a-tailscale-keeper">Why a Tailscale keeper?</h3>
<p>The second trap was on the TCL. Its vendor memory manager kills Tailscale in the background, and Android&rsquo;s Always-on VPN doesn&rsquo;t bring it back. <code>kc-tailscale-keeper.sh</code> pings Tailscale&rsquo;s MagicDNS address (<code>100.100.100.100</code>) every 60 seconds. If there&rsquo;s no answer, it sends Tailscale&rsquo;s own <code>CONNECT_VPN</code> broadcast. That&rsquo;s cheap, and it works.</p>
<h3 id="the-one-thing-we-deliberately-didn-automate">The one thing we deliberately <em>didn&rsquo;t</em> automate</h3>
<p><strong>Android runs none of this until the phone has been unlocked once after a reboot.</strong> Before that first unlock, app storage is still encrypted and Termux can&rsquo;t even start. That&rsquo;s Android&rsquo;s security model doing its job, and I&rsquo;m not going to fight it. After a reboot, someone unlocks the phone once. About a minute later, it&rsquo;s back on the network.</p>
<h2 id="setting-it-up-the-short-version">Setting it up (the short version)</h2>
<p>You need USB debugging on once, plus Tailscale, Termux and Termux:Boot (<strong>from the same source</strong>, F-Droid or GitHub, because they must share a signing key) and Tasker. The <a href="https://github.com/pueblokc/android-remote-adb-tailscale">README</a> has every command. The outline:</p>
<p><strong>1. Grant permissions from the PC</strong> (reversible). Run <code>host/01-grant-permissions.sh &lt;serial&gt;</code>. It grants Termux and Tasker <code>WRITE_SECURE_SETTINGS</code>, whitelists Termux, Termux:Boot, Tailscale and Tasker from battery optimization, and turns off Android&rsquo;s &ldquo;phantom process&rdquo; killer, which otherwise murders long-running Termux jobs.</p>
<p><strong>2. Push the scripts and open port 5555.</strong> <code>host/02-push-scripts.sh &lt;serial&gt;</code>.</p>
<p><strong>3. In Termux, run three scripts in order:</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>bash /sdcard/Download/kc-upg.sh            <span style="color:#6272a4"># full pkg upgrade, ends in UPGDONE</span>
</span></span><span style="display:flex;"><span>bash /sdcard/Download/kc-adb-setup.sh      <span style="color:#6272a4"># adb + nmap, installs boot script; tap &#34;Always allow&#34;</span>
</span></span><span style="display:flex;"><span>bash /sdcard/Download/kc-keeper-install.sh <span style="color:#6272a4"># starts the Tailscale keeper</span>
</span></span></code></pre></div><p>Don&rsquo;t skip <code>kc-upg.sh</code>. One phone had an older Termux with a half-upgraded package set, and <code>adb</code> refused to run with <code>CANNOT LINK EXECUTABLE ... __hash_memory</code>. A full <code>pkg upgrade</code> fixed it.</p>
<p><strong>4. Build the Tasker profile:</strong> Profiles → + → Event → System → <strong>Device Boot</strong>. New task, add a <strong>Custom Setting</strong> action: Type <code>Global</code>, Name <code>adb_wifi_enabled</code>, Value <code>1</code>. Press ▶ to test it, then ✓ to apply. Accepting Tasker&rsquo;s terms is a human tap. The AI filled in everything else through the UI.</p>
<p><strong>5. Prove it.</strong> <code>host/03-reboot-test.sh &lt;serial&gt; &lt;tailscale-ip&gt;</code> turns Wireless debugging <em>off</em>, reboots, and times how long until 5555 answers. On the family Pixel 6 it was back about <strong>60 seconds after unlock</strong>, and mDNS found the port on the first try.</p>
<h2 id="teaching-the-ai-to-have-gentle-hands">Teaching the AI to have gentle hands</h2>
<p><img alt="A padded robot glove gently taps a phone while a smiling microphone character stays on screen; behind them a clumsy giant robot fist is crossed out. The host holds a sign reading GENTLE HANDS ONLY." loading="lazy" src="/posts/letting-my-ai-drive-my-android-phones/gentle-hands.webp"></p>
<p>Getting in was half the work. The other half was learning what <em>not</em> to do once we were in.</p>
<ul>
<li><strong>Don&rsquo;t use <code>uiautomator dump</code> to read the screen.</strong> It&rsquo;s the standard way to get a phone&rsquo;s UI as XML, and it quietly shuts down every accessibility service while it runs. My dictation app (Wispr Flow) lives on accessibility. After a few dumps in a row its bubble went invisible and untouchable until the app was force-stopped. <a href="/posts/wispr-flow-android-bubble-invisible-not-touchable/">That whole saga is here.</a> We replaced it with a small helper that asks Android <em>not</em> to suppress accessibility services (<code>FLAG_DONT_SUPPRESS_ACCESSIBILITY_SERVICES</code>) and returns the same XML. It made zero disconnects in five-dump bursts. Plain ADB (<code>shell</code>, <code>dumpsys</code>, <code>screencap</code>, <code>input</code>) was never the problem.</li>
<li><strong>Don&rsquo;t launch apps with <code>monkey</code>.</strong> It&rsquo;s the lazy one-liner for &ldquo;open this app,&rdquo; and when it exits it turns auto-rotate back on. Use <code>am start -n</code> with the activity from <code>cmd package resolve-activity</code> instead.</li>
<li><strong><code>adb shell input text</code> goes through the phone&rsquo;s shell.</strong> A <code>|</code>, <code>;</code> or <code>&gt;</code> in the text runs on the phone instead of getting typed. Push a script file and type <code>bash /sdcard/Download/x.sh</code> instead.</li>
<li><strong>On Windows Git Bash, <code>/sdcard/...</code> gets rewritten</strong> to <code>C:/Program Files/Git/sdcard/...</code>. Set <code>MSYS_NO_PATHCONV=1</code>. This one cost us two wait loops that sat there until they timed out.</li>
<li><strong>The AI never types passwords, PINs or one-time codes.</strong> It fills in the username and hands me the phone. Terms-of-service checkboxes are a human tap too.</li>
<li><strong>If someone is using the phone, wait.</strong> Check what&rsquo;s on screen (<code>dumpsys window | grep mCurrentFocus</code>) before taking over. That matters most on a phone that isn&rsquo;t yours.</li>
</ul>
<h2 id="what-its-actually-good-for">What it&rsquo;s actually good for</h2>
<p><img alt="Two phones lie side by side on a dark workbench while a small mechanic robot feeds glowing boxes into both from a conveyor belt; the host holds a tablet showing two green checkmarks." loading="lazy" src="/posts/letting-my-ai-drive-my-android-phones/updates.webp"></p>
<p>This is where it pays off. Some things Claude has done on the phones since:</p>
<ul>
<li><strong>Hunted down a &ldquo;random&rdquo; sound.</strong> A family phone kept playing a <em>&ldquo;You&rsquo;ve got mail!&rdquo;</em> sound at odd times. The AI dumped every notification channel (<code>dumpsys notification --noredact</code>) and matched the custom sound file&rsquo;s media ID. The culprit was one chat group&rsquo;s channel, which is why it seemed random: it only played when that group got a message. It swapped the sound, then set nine mail channels and a few chatty apps to silent. It kept a before-and-after record so every change can be undone.</li>
<li><strong>Pushes app updates.</strong> When I build a new version of one of my own apps, the new build goes onto both phones over ADB in the same step, and it gets checked on both.</li>
<li><strong>Keeps my two phones identical.</strong> Any setting changed on one gets made on the other, then read back on both. Auto-rotate off, screen timeout, notification rules.</li>
<li><strong>Upgrades the plumbing.</strong> Termux package upgrades run through the same pushed-script trick, with logs I can read later.</li>
<li><strong>Sees what I see.</strong> <code>adb exec-out screencap -p</code> gives it a screenshot any time, and that&rsquo;s always safe.</li>
</ul>
<h2 id="security-because-this-is-full-control-of-a-phone">Security, because this is full control of a phone</h2>
<p>ADB is root-adjacent power over the device, so:</p>
<ul>
<li><strong>Never port-forward 5555 on your router.</strong> Tailscale is the only way in. The port is reachable only inside my private tailnet.</li>
<li><strong>Lock it down with Tailscale ACLs</strong> so only the admin PC can reach the phones on 5555.</li>
<li><strong>ADB still checks keys.</strong> Even on 5555, only computers whose ADB key the phone has approved can connect. Keep <code>~/.android/adbkey</code> private.</li>
<li><strong>Keep secrets human.</strong> The AI can do anything I can on the phone, so the things that prove <em>I&rsquo;m me</em> (passwords, PINs, 2FA codes) stay with me.</li>
</ul>
<h2 id="the-takeaway">The takeaway</h2>
<p>The hard part wasn&rsquo;t &ldquo;give the AI access.&rdquo; It was making access <strong>boring</strong>: it survives reboots, it doesn&rsquo;t break other apps, and it doesn&rsquo;t need me to walk over and toggle a switch. Once it&rsquo;s boring, the AI stops being a phone-support hotline that reads me instructions and starts closing tickets on its own.</p>
<p>Now when something on a phone annoys me, I describe it, and Claude fixes it.</p>
<p><strong>Scripts and full setup:</strong> <a href="https://github.com/pueblokc/android-remote-adb-tailscale">github.com/pueblokc/android-remote-adb-tailscale</a></p>
]]></content:encoded></item></channel></rss>