codename-11/hermes-relay/android

Claude Code and agent skill by codename-11/hermes-relay

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Control an Android phone via Hermes-Relay — navigate apps, tap, type, swipe, and drive Uber, WhatsApp, Spotify, Maps, Settings, and more

Install

npx vskill@latest install codename-11/hermes-relay/android

SKILL.md

Android Phone Control

Drive a paired Android phone from Hermes using the android_* tool family. This skill is the playbook the LLM reads before touching those tools for the first time — architecture, pairing, the tool surface, bounded tool budgets, and per-app step-by-step procedures.

1. How it works

  1. The Hermes-Relay Android app runs on the phone and exposes an AccessibilityService + MediaProjection dispatcher.
  2. The phone connects outbound to the Hermes-Relay WSS relay (default :8767) as the bridge channel.
  3. Each android_* tool sends an HTTP request to the relay on loopback, which forwards it as a bridge.command envelope over the WSS socket to the phone. The phone executes the action and replies with bridge.response.
  4. Sensitive actions pass through Tier 5 safety rails on the phone (blocklist, destructive-verb confirmation, idle auto-disable). See section 9.
  5. sideload builds unlock additional capabilities behind a build-flavor flag (voice-to-bridge intents, direct SMS, direct dial). See section 10.

2. Setup — how the phone gets paired

Do not call `android_setup`. That is a fallback for teaching the host about an existing session token — it does NOT pair a new phone. For first-time setup or re-pairing, use one of the canonical flows:

InvocationWhen to use
/hermes-relay-pair (slash command)Any Hermes chat session. Invokes the hermes-relay-pair skill, which generates a pairing QR for the user to scan.
hermes-pair (shell shim)Terminal users on the host. Runs python -m plugin.pair. Same QR, no chat round-trip.

If the user has not paired yet, stop and hand off to /hermes-relay-pair. Do not attempt to drive tools against an unpaired phone — every call will return 503 bridge not connected.

To confirm the phone is reachable before starting a workflow, call android_ping once. A healthy response looks like {"status": "ok", "bridge": {...}}. Anything else means the phone is not connected — stop and tell the user.

3. Available tools

All of these live in plugin/tools/android_tool.py (except android_navigate in plugin/tools/android_navigate.py). Every tool returns a JSON string.

Screen reading
ToolPurpose
android_read_screen(include_bounds=False)Dump the accessibility tree of the current screen. Each node has nodeId, text, contentDescription, className, clickable, focusable, and optionally bounds. This is your primary "what's on screen" input — prefer it over screenshots when you only need to read text or find a tap target.
android_screenshot()Capture a JPEG of the phone screen. Returns a MEDIA:hermes-relay://<token> line that the phone fetches over the bearer-auth'd relay. Use this when you need vision (logos, layouts, CAPTCHAs, non-accessibility-reachable content) — not for text that android_read_screen already exposes.
Interaction
ToolPurpose
android_tap(x=None, y=None, node_id=None)Tap by accessibility node_id (preferred — stable across screens) or by (x, y) pixel coordinates (fallback).
android_tap_text(text, exact=False)Tap the first element whose visible text matches. Use when you can see the word on screen but don't have a node id from android_read_screen yet. exact=False does substring matching.
android_type(text, clear_first=False)Type into the currently focused input field. Set clear_first=True to blank it before typing. You still need a preceding tap to focus the field.
android_swipe(direction, distance="medium")Screen-level swipe. direction is one of up, down, left, right. distance is short, medium, or long.
android_scroll(direction, node_id=None)Scroll within a scrollable container. Pass node_id from android_read_screen to scroll a specific list; omit it to scroll the root.
android_press_key(key)Press a curated system key: back, home, recents, power, volume_up, volume_down, enter, delete, tab, escape, search, notifications. No raw KeyEvent injection.
android_open_app(package)Launch an app by package name. See section 7 for common package names.
android_wait(text=None, class_name=None, timeout_ms=5000)Poll every 500 ms for an element to appear. Cheaper than burning a retry budget on read_screen loops.
App state
ToolPurpose
android_current_app()Return the package and activity of the foreground app. Use to confirm a launch succeeded or that you didn't land on a permission modal.
android_get_apps()List all installed apps with package names and labels. Use when the user asks for "an app that does X" and you need to discover what's installed.
android_ping()Health check — is the phone connected to the relay? Call once at the start of a workflow, not on every step.
Orchestration
ToolPurpose
android_navigate(intent, max_iterations=5)Close-the-loop vision-driven navigation. Takes one screenshot per step, asks a vision model what to tap next, and dispatches to the direct tools above. Hard-capped at max_iterations (default 5, absolute ceiling 20). Returns a full trace of every step. See section 6 for when to reach for this instead of the direct tools.

There is also an android_setup(bridge_session_token) function in the same module — that is a fallback for reconfiguring the host after a re-install when you already have a session token. It is not how you pair a phone. See section 2.

4. Critical: do not loop

Hard rule: cap any single user request at 5–7 tool calls total. When you hit that budget, STOP and report the current state to the user. Do not retry. Do not keep tapping. Do not "try one more thing."

Why this exists:

  • Accessibility trees are noisy. The same visible "Send" button can have three different nodeIds across a single app depending on what's loaded above it. A naive retry loop burns minutes chasing stale node ids.
  • Phones have Tier 5 safety rails (section 9). If a tap is blocked, retrying it will just produce the same 403. Report the block — don't hammer the gate.
  • android_navigate is the correct escape hatch for "I don't know what to tap next." Its iteration cap is code-enforced. Your job as the outer agent is to bound the total number of invocations, not to write your own retry logic around it.

STOP-and-report pattern. When you cannot make progress:

  1. Call android_read_screen once.
  2. Summarize what you see (2–3 sentences).
  3. Ask the user what they want you to do next. Do not guess.

Acceptable tool budgets for common workflows:

WorkflowBudget
Launch an app and read what's on screen2–3 tools (open_app → wait → read_screen)
Send a WhatsApp message to a known contact5–6 tools
Book an Uber to a known destination6–7 tools
Find an unknown settinghand off to android_navigate with max_iterations=5
"Scroll until you see X"3 swipes max, then stop and report

If you realize mid-workflow that you need more than 7 calls, stop, explain the situation to the user, and ask whether to continue.

5. Workflow pattern

The canonical shape of every bounded Android workflow:

open_app(package)
→ wait(text="<landmark>", timeout_ms=5000)
→ read_screen()
→ 1–3 targeted actions (tap_text / tap / type / swipe)
→ read_screen() # verify the action landed
→ STOP and report

Key discipline:

  • Verify before you chain. Never assume a tap "worked" — re-read the screen and confirm the next landmark is present before sending another action.
  • Prefer `tap_text` over `tap(x, y)`. Coordinates go stale the instant the layout shifts (keyboard pops, ad loads, notification arrives). nodeId from a fresh read_screen is second-best; raw coordinates are last resort.
  • Never `open_app` twice in a row. If the first didn't land, check android_current_app and report.
  • Never chain more than 3 actions without a `read_screen`. UI state drifts faster than you expect.

6. When to use android_navigate vs direct tools

Use the direct tools (tap_text, type, swipe, etc.) when you know the exact step sequence — for example, the app playbooks in section 8. You are in control of the budget, you can inspect intermediate state, and every step is cheap.

Use `android_navigate(intent, max_iterations=N)` when you need vision in the loop: the user's goal is clear but the path is not, the layout is unfamiliar, or the target element is visible in pixels but not in the accessibility tree (icon-only buttons, canvas-rendered UI, custom views). android_navigate takes one screenshot per step, lets a vision model pick the next action, and dispatches to the same direct tools under the hood. The iteration cap is enforced in code (default 5, max 20).

Rules of thumb:

  • Known app, known steps → direct tools. (Uber, WhatsApp, Spotify, Maps — see section 8.)
  • Unknown app or UI → `android_navigate`. Let vision decide.
  • Mixed → direct tools to get into the right screen, then `android_navigate` for the last-mile decision. Don't burn vision budget on open_app → wait that you can script.
  • Never wrap `android_navigate` in your own retry loop. If it fails, read the trace, summarize for the user, and stop.

7. Common package names

AppPackage
Uber (rider)com.ubercab
WhatsAppcom.whatsapp
Spotifycom.spotify.music
Google Mapscom.google.android.apps.maps
Chromecom.android.chrome
Gmailcom.google.android.gm
Instagramcom.instagram.android
X (formerly Twitter)com.twitter.android
Tindercom.tinder
Settingscom.android.settings

If the user asks about an app not on this list, call android_get_apps and match on the human-readable label.

8. App playbooks

Each playbook is a minimal bounded sequence. Follow them; do not improvise extra steps.

Uber — book a ride to a known destination
  1. android_open_app("com.ubercab")
  2. android_wait(text="Where to", timeout_ms=8000) — Uber's home search field.
  3. android_tap_text("Where to") — opens the destination picker.
  4. android_type("<destination>") — type the address into the focused field.
  5. android_wait(text="<destination-first-word>", timeout_ms=4000) — wait for autocomplete.
  6. android_tap_text("<first matching result>") — select the result.
  7. android_read_screen() — verify the ride-type picker appeared. STOP and report the quoted fare + ETA. Do not auto-confirm the ride — requesting a ride is a destructive action that spends money. Let the user confirm manually, or let the Tier 5 destructive-verb gate (section 9) prompt them.

Pitfalls:

  • Uber sometimes wraps the destination field in a non-clickable container. If tap_text("Where to") does nothing, re-read the screen and look for a clickable ancestor node.
  • If a login or onboarding splash appears, stop and report — do not try to log in for the user.
  • The "Confirm" / "Request" button is on the destructive-verb list by default (see section 9). Expect a 403 if you try to tap it without user confirmation.
WhatsApp — send a message to a contact
  1. android_open_app("com.whatsapp")
  2. android_wait(text="Chats", timeout_ms=5000) — landing tab.
  3. android_tap_text("Search") or tap the magnifying glass icon.
  4. android_type("<contact name>") — narrows the chat list.
  5. android_tap_text("<contact name>") — opens the chat.
  6. android_tap_text("Message") — focus the input field. (Label varies by locale; fall back to read_screen and find the EditText node.)
  7. android_type("<message body>")
  8. STOP and report. Do not auto-tap Send — it is on the destructive-verb list. Either let the user tap Send, or invoke the Tier 5 confirmation modal by calling android_tap_text("Send") and letting the phone prompt.

Pitfalls:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
197
Repository forks
44
7-day trend score
26

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/codename-11/hermes-relay/android/badge)](https://verified-skill.com/skills/codename-11/hermes-relay/android)

Something wrong with this skill or its page? Tell us.