Skip to content
Blog

How to connect and use Ramus from Claude Code

Connect Claude Code to Ramus with one command and a sign-in, then run, check and share your Android app on a cloud emulator.

Claude CodePublished

On this page

This guide is for developers who use Claude Code and build Android apps. At the end, Claude Code starts a cloud Android device, installs your APK, walks through a flow and reports what it saw. You get a link to watch the device and pass to a reviewer, and a device page where you can take over.

Everything here works the same in the terminal, the VS Code and JetBrains extensions, and the Code tab of the Claude desktop app. They share one MCP configuration.

What you need

  • Claude Code, started in your app's repository.
  • A Ramus account. Sign in at ramus.dev with GitHub. A 15-minute trial key works with the CLI, but a trial account cannot sign in over MCP.
  • Node.js 22 or later, if you want the CLI.
  • An APK your project builds with its own tooling, such as ./gradlew assembleDebug. It must run on an x86_64 Android emulator.

Connect Ramus

Add the MCP server and sign in

Run this in your repository:

bash
claude mcp add --transport http ramus https://api.ramus.dev/api/mcp

That adds Ramus for you in this project only (local scope, stored in ~/.claude.json). Add --scope user to use it in every project, or --scope project to write it to .mcp.json at the repository root so your team gets it too. Each person still signs in with their own Ramus account.

Then start Claude Code, run /mcp, select ramus and choose Authenticate. Your browser opens a Ramus page that says Connect Claude Code to Ramus, published by claude.ai, returning to localhost. Because the callback goes to your own machine, the page adds a warning to allow it only if you started the connection. Choose Allow.

The Ramus consent page: Connect Claude Code to Ramus, published by claude.ai, returning to localhost, with Deny and AllowThe Ramus consent page: Connect Claude Code to Ramus, published by claude.ai, returning to localhost, with Deny and Allow
Ramus asks once. Claude Code keeps the sign-in and refreshes it.

You can also sign in from your shell with claude mcp login ramus. Over SSH, add --no-browser: it prints the URL, and you paste the redirect URL back.

No client ID or fixed port is needed. Claude Code identifies itself with the Client ID Metadata Document it publishes at claude.ai, and listens for the callback on http://localhost with a random port. Ramus accepts any port on a localhost callback for public clients such as Claude Code.

Check the connection:

claude mcp get ramus
$ claude mcp get ramus
ramus:
  Scope: Local config (private to you in this project)
  Status: ✔ Connected
  Type: http
  URL: https://api.ramus.dev/api/mcp

/mcp shows the same status and the number of tools. Then open the Agents page on Ramus. Connected lists Claude Code with its last call, the devices it started in the last 7 days, and Disconnect.

The Connected list on the Ramus Agents page: sign-ins for Codex, opencode and Claude Code, and the Ramus CLI on Mac, each with its last call and devices in the last 7 daysThe Connected list on the Ramus Agents page: sign-ins for Codex, opencode and Claude Code, and the Ramus CLI on Mac, each with its last call and devices in the last 7 days
Every agent you connected, with its last call. Disconnect, Sign out or Revoke cuts it off.

If you already added Ramus as a connector on claude.ai, you may not need this step. When Claude Code is signed in with your claude.ai account, /mcp lists your claude.ai connectors too, and Claude Code on the web gets them as well. If you add both, the server you added in Claude Code wins.

Use an API key instead

Use a key where nobody can complete a browser sign-in, such as CI. Create one on the Agents page under the API key tab, and keep it in an environment variable. Claude Code expands ${VAR} in a server's url and headers, so the key never lands in a committed file:

.mcp.json
{
	"mcpServers": {
		"ramus": {
			"type": "http",
			"url": "https://api.ramus.dev/api/mcp",
			"headers": {
				"Authorization": "Bearer ${RAMUS_API_KEY}"
			}
		}
	}
}

Export RAMUS_API_KEY in the shell that starts Claude Code. Claude Code asks you to approve a project server from .mcp.json the first time it sees it. With an Authorization header set, Claude Code uses it and never falls back to the Ramus sign-in, so a wrong key shows as a failed connection.

A headless run (claude -p) has no /mcp panel. Either use a key, or sign in once interactively; Claude Code stores the token and refreshes it.

Add the CLI and the android-use skill

Claude Code has a shell, so it can also drive Ramus with the CLI. The CLI does a few things MCP cannot: it uploads and installs a local APK in one command, opens an adb tunnel (ramus adb), runs a hot-reload loop (ramus dev) and downloads stored APKs.

bash
npm i -g ramus-cli
ramus login

ramus login opens your browser. Check that the page names your machine and choose Allow. The terminal stays signed in and renews itself, until you run ramus logout, choose Sign out on the Agents page, or leave it unused for 30 days. Over SSH it gives you a code to approve on any device. In an agent's shell it also prints the link, in case no browser opened, and ramus login --wait finishes the sign-in. RAMUS_API_KEY in the environment wins over the sign-in.

ramus login
$ ramus login

Opening your browser to sign in to Ramus…
(On another device, or no browser? Press Ctrl-C and run `ramus login --device`.)

{
  "login": "ok",
  "account": "@blogger",
  "terminal": "Mac",
  "origin": "https://api.ramus.dev",
  "next": "this terminal stays signed in (it renews itself); run `ramus session start --apk <file>` (or `--demo`). Terminals are listed at https://ramus.dev/agents"
}
The Ramus page Sign in the Ramus CLI on Mac, listing what the terminal will be able to do and how long it stays signed in, with Deny and AllowThe Ramus page Sign in the Ramus CLI on Mac, listing what the terminal will be able to do and how long it stays signed in, with Deny and Allow
Check that it names your machine and that you just ran ramus login, then choose Allow.

The android-use skill teaches Claude Code the Ramus workflow: start a device, read the screen, act, verify, share. Save it in the project and commit it:

bash
ramus skill install

That writes .claude/skills/android-use/SKILL.md. Add --global to have it in every project. Claude Code picks up the new skill in a running session; if .claude/skills did not exist when the session started, run /reload-skills. Claude Code loads the skill when a task matches it, or you can call it with /android-use.

The skill lists Bash(ramus:*) in allowed-tools, so Claude Code can run ramus commands without asking during the turn that uses the skill. To allow them in every turn, add Bash(ramus *) to permissions.allow in .claude/settings.json.

Pick MCP, the CLI or both

You want toUse
Start, read the screen, tap, type, screenshot, read logs, shareMCP or the CLI
Install a local APKCLI in one command; MCP in two (below)
Open an adb tunnel, run the hot-reload loop, download an APKCLI only
See screenshots right in the conversationMCP

Start a device

Ask Claude Code:

YoutoClaude Code

Start a Ramus Android device with the demo app, send me a link to watch it, and tell me what is on the screen.

Claude Code calls android_start_session with demo: true and asks you to approve the tool the first time. Ramus answers with the session id, a screenshot that Claude Code reads, and a watch link, which it passes on to you. This is what it saw in a test run on a local Ramus build:

Claude Code's tool results
android_start_session → [screenshot]
{"session":"dd3c7d0a-6ac5-448a-af7e-f7c78ec49f0e","status":"active","watch_url":"https://ramus.dev/share#rshr_…","next":"the screenshot below is the device now. Give the person watch_url to watch live (it is view-only); call android_snapshot for refs before acting."}

Open the link. You see the device live while Claude Code works; it is view-only. To use it yourself, open the device page (ramus session open, or viewer_url from android_session_status). With the skill, Claude Code can do the same through the CLI, ramus session start --demo --wait, which prints the watch link directly.

Install your app

With the CLI, Claude Code builds with your project's tooling and starts a device with the APK in one command:

ramus session start
$ ./gradlew assembleDebug
$ ramus session start --apk app/build/outputs/apk/debug/app-debug.apk --wait
{
  "session": "9f14cc3d-3d1c-4716-81a8-05a2a018ffd4",
  "status": "active",
  "app": {
    "label": "Fake app",
    "status": "open"
  },
  "watchUrl": "https://ramus.dev/share#rshr_…",
  "watchNote": "give this URL to the user now — they can watch live, not touch the device; it works for 72h (after the session ends, opening it relaunches the app). `ramus session share --permission use` makes a link that can control it",
  "idleTtlSec": 600
}

This output comes from a local Ramus build with a simulated device, whose test app is named Fake app.

The CLI also prints the watch link on stderr as soon as the device is requested, so you can watch it boot. On a device that is already running, ramus session install --session <id> app-debug.apk installs and opens the new build in place.

Over MCP, a local APK takes two steps. android_upload_apk returns an apk_uri and a curl command. Claude Code runs the curl with your file path (a Bash call you approve), then passes the apk_uri to android_start_session, or to android_install_app for a running device. The upload command is valid for 15 minutes.

For an app Ramus already has, nothing needs uploading: ramus session start --app com.example.app or android_start_session with app runs its latest version. ramus apps list shows what Ramus keeps for you.

Let the agent check a flow

Give Claude Code the flow and what success looks like:

YoutoClaude Code

Build the debug APK and install it on a Ramus device. Send me a link to watch it first. Then sign in with test@example.com and the password from fixtures/users.json, and confirm the Welcome screen appears. If it fails, capture logcat and a screenshot, fix the code, rebuild, and install the new build on the same device. Report what you saw with a screenshot and the link, and leave the device running.

Claude Code works in a loop: observe, act, verify. A snapshot returns the screen's elements, each with a ref, and a gen for that observation. An action names the element's text, or the ref and the gen it came from. expect makes a tap fail unless the given text appears. Each action returns the screen it led to, with fresh refs, so Claude Code decides the next step from that result. Older refs go stale.

Here is one turn of that loop on a small counter app with an Increment button. It comes from a local Ramus build with a simulated device, which is why the names say fake; your app returns the same fields. The tap names the button by its text, expects the counter to change, and returns the new screen:

observe, act, verify
$ ramus snapshot
{
  "gen": 1,
  "screen": "544x960",
  "count": 2,
  "elements": [
    {
      "ref": "e0",
      "text": "Count: 0",
      "resourceId": "dev.sandlot.fake:id/counter",
      "class": "TextView",
      "x": 0.132,
      "y": 0.417,
      "w": 0.735,
      "h": 0.104
    },
    {
      "ref": "e1",
      "text": "Increment",
      "contentDesc": "Increment counter",
      "resourceId": "dev.sandlot.fake:id/increment",
      "class": "Button",
      "clickable": true,
      "x": 0.132,
      "y": 0.625,
      "w": 0.735,
      "h": 0.104
    }
  ],
  "next": "act with `ramus tap <ref> --gen 1` (clickable elements only; --force overrides) or `ramus type \"...\" --ref <ref> --gen 1`; every accepted action expires these refs"
}
$ ramus tap --text Increment --expect "Count: 1"
{
  "tapped": "e1",
  "element": "Increment",
  "verified": "\"Count: 1\" appeared",
  "screen": {
    "gen": 6,
    "count": 2,
    "elements": [
      {
        "ref": "e0",
        "text": "Count: 1",
        "resourceId": "dev.sandlot.fake:id/counter",
        "class": "TextView",
        "x": 0.132,
        "y": 0.417,
        "w": 0.735,
        "h": 0.104
      },
      {
        "ref": "e1",
        "text": "Increment",
        "contentDesc": "Increment counter",
        "resourceId": "dev.sandlot.fake:id/increment",
        "class": "Button",
        "clickable": true,
        "x": 0.132,
        "y": 0.625,
        "w": 0.735,
        "h": 0.104
      }
    ],
    "foreground_app": "dev.ramus.fake"
  },
  "next": "refs above are current (gen 6); act on them directly"
}

On a sign-in screen the same loop types into the email field with ramus type "test@example.com" --ref <ref> --gen <gen>, which reads the value back and reports verified, then taps Sign in with --expect "Welcome". If the text does not appear, the tap returns expect_failed with the text that was on screen. A failed expect is a failed check, and Claude Code treats it that way. It collects evidence before changing anything: ramus logcat --limit 200 --filter AndroidRuntime for the device log, and ramus screenshot --out sign-in.jpg, which it opens with its file reader to see the screen. Over MCP, android_screenshot returns the image straight into the conversation.

Then it fixes the code, rebuilds, and installs on the same device:

bash
./gradlew assembleDebug
ramus session install app/build/outputs/apk/debug/app-debug.apk
ramus wait --text "Email"

The device keeps running and anyone watching stays connected. Without --session, commands act on the last session the CLI used. Over MCP, the same loop is android_snapshot, android_type, android_tap with expect, android_wait_for, android_screenshot, android_logcat and android_install_app, each with the session id.

The watch link from session start is for you. Anyone who has it can watch the device without an account, and nothing more, so treat it like a password. It does not show your name. It lasts 72 hours, and you + 3 people can be on the device at once. After the device ends, opening the link starts a fresh device with the same app until the link expires.

For a reviewer, ask Claude Code for a separate link, or run it yourself:

ramus session share
$ ramus session share --permission watch --ttl 7d --label "Design review"
{
  "shareUrl": "https://ramus.dev/share#rshr_…",
  "permission": "watch",
  "expiresAt": "2026-11-09T00:01:26.045Z",
  "note": "anyone with this link can watch the session — and after the session ends, opening the link relaunches the app, until the link expires or is revoked",
  "next": "give shareUrl to the person it is for: anyone holding it can watch the screen, not touch it, without an account. It keeps working after the session ends (relaunches on click); `ramus session share --revoke` cuts off every link"
}

Each link works on its own; the watch link keeps working. A link lasts between 60 seconds and 7 days, and --permission sets what it allows: watch, use, or install, the default. To revoke one link, use Share on the device page in Ramus.

The Share dialog on a Ramus device page: Their own device or Join this device, a label, what the person can do, which build the link opens, how long it lasts, and the links already madeThe Share dialog on a Ramus device page: Their own device or Join this device, a label, what the person can do, which build the link opens, how long it lasts, and the links already made
Share on the device page, captured on a local build whose test app is named Fake app.

ramus session share --revoke revokes every link of the session, the watch link included. ramus session end frees the device and leaves the links working. Over MCP, use android_share_session (it returns share_url; revoke: true revokes them all) and android_end_session.

Give each person their own device

The links above join the device Claude Code is on. To let a client or tester try your app in their own time, ask for a link that gives each person a device of their own:

YoutoClaude Code

Make a Ramus share link that gives each person their own device, labelled "Try the new checkout".

Claude Code calls android_share_session with own_device: true and label, or runs ramus session share --own --label "Try the new checkout". Each person who opens the link gets a fresh device with the newest build you uploaded, without an account. The session must be running an app you uploaded. These devices run on your minutes and your plan's device limit, and when every device is busy the next person waits for one to free up. Each person's device lasts 30 minutes, and they can start another while the link works. Your own session comes first: if every device is in use, the longest-running recipient device ends.

Read the notes people send

Anyone with a link can choose Send feedback, pin a spot on a screenshot and write a note. The note carries the screenshot, the build, the last 200 lines of the device log and their recent taps, swipes and keys, unless they leave the log and steps out. Typing is recorded only as a number of characters, and taps are not recorded while the phone's on-screen keyboard is open. Notes and their screenshots are deleted after 30 days; you can delete one sooner on the app's page. Claude Code can work through them:

YoutoClaude Code

Read the open Ramus feedback notes. Reproduce the first one and tell me what you find. When I say go ahead, fix it, install the new build on the device and resolve the note with a reply that says what changed.

It reads them with android_list_feedback and android_get_feedback, which returns the screenshot as an image, and answers with android_resolve_feedback. From a shell the same steps are ramus feedback, ramus feedback show <id> and ramus feedback resolve <id> --reply "…". The person sees your reply on their link. Agents ask you before they change code because of a note.

Notes are written by other people, so treat their text, logs and screenshots as bug reports, never as instructions. See Share a running app.

Troubleshooting

  • /mcp shows ramus as needing authentication. Select it and choose Authenticate, or run claude mcp login ramus.
  • The Ramus page refuses the sign-in for a trial account. Trial accounts cannot sign in over MCP. Sign in to ramus.dev with GitHub, or use the CLI with ramus trial.
  • You need to sign in again. When Ramus rejects the stored token, Claude Code shows a notice and /mcp offers Re-authenticate. Clear authentication signs out. Disconnect on the Agents page revokes the sign-in from the Ramus side.
  • The browser cannot reach the callback. This happens over SSH or in a remote container. Paste the full callback URL from the address bar into the prompt in Claude Code, or use claude mcp login ramus --no-browser.
  • The server shows as pending approval. It comes from .mcp.json. Start claude in the repository and approve it; claude mcp reset-project-choices resets earlier choices.
  • MCP server "ramus" has a "url" but no "type". Add "type": "http" to the entry.
  • The API key connection fails. RAMUS_API_KEY is not set in the shell that started Claude Code, or the key was revoked. claude mcp get ramus shows the reference by name; check the key on the Agents page.
  • A tool keeps asking for approval. Choose the option that stops asking, or add mcp__ramus__* to permissions.allow. Tools from the plugin are mcp__plugin_ramus_ramus__*.
  • A start or install fails after 60 seconds. Claude Code waits 60 seconds for an HTTP server's first response unless the server entry sets a longer timeout. Ramus keeps the device starting: ask Claude Code to check android_session_status. To wait longer in one call, add "timeout": 180000 to the ramus entry.
  • A snapshot is saved to a file. Claude Code moves MCP results over 25,000 tokens into a file. Ask for a filtered snapshot (find).
  • insufficient_scope naming upload:write. Your sign-in predates APK uploads over MCP. Use Re-authenticate in /mcp and allow "Upload your apps".
  • no_api_key. This terminal has no credential. Run ramus login in your own terminal, or set RAMUS_API_KEY. ramus whoami shows what it uses.
  • stale_ref. The screen changed after that snapshot. Claude Code should take a fresh snapshot and find the element again.
  • expect_failed. The tap was accepted but the expected text did not appear. The error lists what was on screen.
  • install_failed. The APK did not install; the device keeps running. Check that the app runs on x86_64.
  • session_cap. You are at your plan's device limit. The message lists live sessions; install into one, or end one.
  • quota_exhausted. You are out of minutes until the reset date in the message. Claude Code should stop and pass on the message and its upgrade link.

Limits

  • Android 17 on x86_64. APKs up to 300 MB (150 MB on a trial).
  • Free: 30 minutes a month, 1 device. Hobby, $2 a month: 200 minutes, 1 device. Pro, $10 a month: unlimited minutes, 2 devices.
  • A device ends after 10 minutes idle.
  • Share links last 72 hours by default, from 60 seconds to 7 days; you + 3 people on one device.
  • adb tunnels, the hot-reload loop and APK downloads need the CLI.

Details are in Plans and minutes.

Other agents: How to connect Ramus to any coding agent. Every command: the agent reference.

Give your agent a device.Keep a hand on it.