Skip to content
Blog

How to connect and use Ramus from Zed

Connect Ramus to the Zed agent so it can run your APK on a cloud Android device, check a flow and send you a link to watch or share.

ZedPublished

On this page

This guide is for developers who build Android apps and use the agent panel in Zed. At the end, the Zed agent can start a cloud Android device, install your APK, tap through a flow and fix what breaks. It sends you a link, so you or a reviewer can watch the device. You take over from your device page.

What you need

  • Zed, with the Zed Agent in the agent panel and a model that can call tools and read images.
  • A Ramus account, which you sign in to with GitHub. An anonymous trial from ramus trial cannot sign in to the MCP server.
  • An Android project you can build locally, such as a Gradle app that produces app/build/outputs/apk/debug/app-debug.apk.
  • Node.js 22 or later, if you also want the ramus CLI.

Connect Ramus

Zed calls MCP servers context servers. Ramus is a remote one at https://api.ramus.dev/api/mcp, and Zed talks to it over Streamable HTTP. Zed signs in to it with your Ramus account, so there is no key to store.

Add the server

Open Zed's settings (cmd-,), go to AI, then MCP Servers. The command agent: open settings gets you there too. Click Add Server, choose Add Remote Server and fill in:

  • Server Name: ramus
  • URL: https://api.ramus.dev/api/mcp

Leave Headers empty and save. Zed writes the entry to your settings file. You can also add it by hand: run zed: open settings file and add:

~/.config/zed/settings.json
{
  "context_servers": {
    "ramus": {
      "url": "https://api.ramus.dev/api/mcp"
    }
  }
}

An entry without a header holds no secret, so your team can share it in .zed/settings.json. Each person signs in with their own account. Older guides also add "source": "custom"; current Zed does not need it.

Sign in with Ramus

The ramus row says Authenticate to connect this server. Click Authenticate. Your browser opens Ramus's consent page, Connect Zed to Ramus. Sign in with GitHub if asked, and choose Allow.

The Ramus consent page: Connect Zed to Ramus, with what it will be able to do, Deny and AllowThe Ramus consent page: Connect Zed to Ramus, with what it will be able to do, Deny and Allow
Ramus asks once. Zed keeps the sign-in and refreshes it.

Zed identifies itself with a client metadata document at https://zed.dev/oauth/client-metadata.json and receives the sign-in on http://127.0.0.1:<port>/callback, which Ramus accepts.

Or use an API key

Use a key where nobody can complete a browser sign-in. Open the Agents page, choose the API key tab, name the key zed and create it. Copy the key; the page shows it only once. A key works for your whole account until it expires or you revoke it there. Add it to the server as a header:

~/.config/zed/settings.json
{
  "context_servers": {
    "ramus": {
      "url": "https://api.ramus.dev/api/mcp",
      "headers": { "Authorization": "Bearer rms_your_key_here" }
    }
  }
}

Zed sends header values exactly as written and does not expand environment variables in them, so the key sits in the file as plain text. Keep this entry in your user settings, never in a .zed/settings.json you commit.

Check the connection

In Settings → AI → MCP Servers, the dot next to ramus is green. In the agent panel, the default Write profile includes every MCP server's tools; Ask and Minimal leave them out. In a custom profile, turn the Ramus tools on under Configure MCP Tools.

Then open ramus.dev/agents. After the agent's first tool call, Zed appears under Connected, with the time of its last call. Disconnect on that row cuts it off.

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.

Add the CLI and the skill

The Zed agent also has a terminal tool, so it can run the ramus 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) and runs the hot-reload loop (ramus dev). Install it and sign in yourself, in Zed's terminal panel:

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 status shows who you are signed in as. RAMUS_API_KEY in the environment wins over the sign-in.

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 the agent the Ramus loop. Zed loads Agent Skills from .agents/skills/ in your project and from ~/.agents/skills/:

bash
mkdir -p .agents/skills/android-use
curl -fsSL https://ramus.dev/skill.md -o .agents/skills/android-use/SKILL.md

Zed loads project skills only in a trusted project. The agent picks the skill when a task involves testing an Android app, or you load it by typing / in the message editor and choosing android-use.

For context that is always on, add one line to your project instructions:

Test Android changes on Ramus: https://ramus.dev/agents.md

Zed reads only the first of these files that your project has: .rules, .cursorrules, .windsurfrules, .clinerules, .github/copilot-instructions.md, AGENT.md, AGENTS.md, CLAUDE.md, GEMINI.md. Put the line in that file. If your project has a .rules file, a line added to AGENTS.md is never read.

MCP or CLI

Both reach the same devices on the same account. This guide follows MCP, which fits Zed well: each call shows as a tool card, Zed asks before it runs, and screenshots come back to the model as images. Use the CLI for adb and ramus dev. If both are set up, tell the agent which one to use.

Zed can also host external agents over the Agent Client Protocol, such as Claude Agent, Codex, Gemini CLI and OpenCode. They read their own MCP configuration and instruction files, and Zed may also forward its MCP servers to them. If the Ramus tools do not show up in one of those threads, set Ramus up for that agent: see the guides for Claude Code, Codex, Gemini CLI and OpenCode.

Start a device

Open a new thread in the agent panel and ask:

YoutoZed

Start a Ramus Android device with the demo app and send me a link to watch it.

The agent calls android_start_session with demo: true. Zed shows the tool card and asks you to allow it. The tool waits until the device is live, then returns the session id, a watch_url for you, the minutes left this month on plans with a monthly allowance, and a screenshot of the device. From a test run on a local Ramus build:

android_start_session
{
  "session": "95fc232d-4417-4b52-94ec-d8e6bee7f386",
  "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 watch link to see the device live in your browser. You can tap and type on it yourself while the agent works.

Install your app

To run your own build, the agent puts the APK on Ramus first. Over MCP that is two tool calls and one command:

  1. android_upload_apk with the file name returns an apk_uri and a ready curl command, valid for 15 minutes.
  2. The agent runs the curl command in its terminal with the APK's path. No output means the upload worked.
  3. It passes the apk_uri to android_install_app to install on the device that is already running, or to android_start_session to start a new one. android_install_app installs in place and opens the app. The device and your watch link stay connected.
YoutoZed

Build the debug APK with ./gradlew assembleDebug, upload it to Ramus and install it on the device that is running.

The upload step returns this; the agent replaces <APK_PATH> with the APK's path and runs the command:

android_upload_apk result
{
  "apk_uri": "s3://…/uploads/apk/a67766fa-ca73-459a-a1f1-0b25ac907259/app-debug.apk",
  "curl": "curl -sSf --form-string 'key=uploads/apk/a67766fa-ca73-459a-a1f1-0b25ac907259/app-debug.apk' --form-string 'Content-Type=application/vnd.android.package-archive' … -F 'file=@<APK_PATH>;type=application/vnd.android.package-archive' 'https://…/'",
  "max_mb": 300,
  "expires_in_minutes": 15,
  "next": "replace <APK_PATH> with the APK file path and run curl (no output means success), then call android_start_session or android_install_app with apk_uri"
}

With the CLI the same step is one command. Pass the session id, because the CLI only remembers devices it started itself:

bash
ramus session install --session <id> app/build/outputs/apk/debug/app-debug.apk

ramus session start --apk <file> --wait starts a new device from an APK instead. If Ramus already has the app, android_start_session with app set to its package name skips the upload. Chat-only clients such as ChatGPT take an APK attached to the message; in Zed the agent uploads the file itself.

Let the agent check a flow

Give the agent a concrete flow and tell it what evidence to bring back:

YoutoZed

Use the Ramus device. Test sign-in: tap "Sign in", enter reviewer@example.com, tap Continue, and check that the Welcome screen appears. If it fails, take a screenshot and read logcat, fix the bug, rebuild, and install the new APK on the same device. Repeat until the flow passes. Report what you saw with a screenshot, and leave the device running for me.

The agent works in a loop of observe, act and verify:

  • android_snapshot returns the screen as elements, each with a ref, and a generation number, gen.
  • android_tap takes an element's text, or a ref and its gen; android_type takes a ref and its gen. With expect: "Welcome", a tap fails with expect_failed when that text does not appear, even though the tap landed.
  • Each action returns the screen it led to, with fresh refs, so the agent decides the next step from that. An older ref fails with stale_ref instead of tapping the wrong thing.
  • android_wait_for waits for text to appear or go, or for the screen to settle.

From a local Ramus build with a simulated device, on a small counter app: a tap on Increment that expected "Count: 1", which returns the new screen:

android_tap
{
  "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"
}

When a step fails, the agent collects evidence first: android_screenshot returns the screen as an image, and android_logcat returns the latest log lines, optionally filtered by the app's package name. Then it fixes the code, rebuilds, uploads the new APK and calls android_install_app on the same session, while you keep watching through the same link. After a crash, android_app_launch brings the app back.

The watch_url from android_start_session is your link. Anyone who opens it can watch the device, and nothing more, without a Ramus account. It does not show your name. Do not post it publicly. For a reviewer, ask for a separate link:

YoutoZed

Make a Ramus share link for the reviewer that lasts 7 days.

The agent calls android_share_session with ttl_minutes: 10080 and gets back a share_url and its expires_at. A device can have several links; a new one leaves the others working. Links last 72 hours by default, up to 7 days, and you

  • 3 people can watch at once. After the session ends, opening a link starts the app again on a fresh device, without the old app state, until the link expires or is revoked.

android_share_session with revoke: true revokes every link of the session, your watch link included. To revoke a single link, use the Share panel on the device page at ramus.dev. For a link where the reviewer cannot tap or type, ask for a watch-only link; the agent passes permission: "watch".

When everyone is done, android_end_session frees the device. Ending does not revoke links, and a session also ends after 10 minutes with no activity. The CLI equivalents are ramus session share --ttl 7d --permission watch, ramus session share --revoke and ramus session end.

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.

Give each person their own device

The links above join the device the agent 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:

YoutoZed

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

The agent 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. The agent can work through them:

YoutoZed

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

  • The ramus dot stays red. Hover it to read the error and check that the URL ends in /api/mcp. A server in .zed/settings.json starts only after you trust the project; untrusted projects open in Restricted Mode.
  • Zed still says Authentication Required after you allowed it. Zed waits two minutes for the browser; click Authenticate again and finish sooner. Ramus refuses sign-in for trial accounts.
  • 401 with an API key. The header must read Bearer and then the key itself. Zed does not expand $VAR or ${VAR} in headers.
  • android_start_session times out. Without a timeout, Zed stops waiting after 60 seconds while the device keeps starting. Ramus waits 50 seconds by default, so this needs a slow start or a longer timeout_sec. Add "timeout": 180, and ask the agent to call android_list_sessions to find the device.
  • The agent does not use the Ramus tools. Use the Write profile, or a custom profile with the Ramus tools on, and name Ramus in your prompt. A warning icon next to the model selector means the model cannot use some tools.
  • Zed asks before every Ramus call. That is the default. Choose the Always option on a tool card, or allow the tools you trust in settings. Leave android_start_session, android_share_session and android_end_session on confirm: they use minutes or hand out access.
~/.config/zed/settings.json
{
  "agent": {
    "tool_permissions": {
      "tools": {
        "mcp:ramus:android_snapshot": { "default": "allow" },
        "mcp:ramus:android_screenshot": { "default": "allow" },
        "mcp:ramus:android_tap": { "default": "allow" },
        "mcp:ramus:android_type": { "default": "allow" },
        "mcp:ramus:android_wait_for": { "default": "allow" },
        "mcp:ramus:android_logcat": { "default": "allow" }
      }
    }
  }
}
  • ramus fails with unreachable in the agent's terminal. Zed's sandbox blocked the network. Approve network access, or running the command outside the sandbox. ramus status in your own terminal rules out a sign-in problem.
  • stale_ref. The agent used a ref from an older snapshot. It should snapshot again and find the element again, never reuse or guess refs.
  • expect_failed. The action ran, but the expected text did not appear (or did not go). The message lists what was on screen. Treat it as a test failure.
  • session_cap. You are at your plan's device limit. The message lists the live session ids: install into one with android_install_app, or end one.
  • quota_exhausted. The account is out of minutes until the reset date in the message. This is final: the agent should stop and pass the message on.
  • install_failed. The APK did not install; the message carries Android's reason and the session keeps running. An APK whose native code is ARM only cannot install on these x86_64 devices.
  • 401, or 403 insufficient_scope. A 401 means no valid sign-in or key: use Log Out and Authenticate, or check the key. A 403 with insufficient_scope means the token lacks a permission the tool needs: authenticate again and approve every permission.

Limits

These come from the current Ramus product policy:

  • Devices run Android 17 on x86_64. Input is single-finger.
  • A session ends after 10 minutes with no activity.
  • You + 3 people can watch a device at once. Share links last 72 hours by default and up to 7 days.
  • APKs can be up to 300 MB.
  • Plans, as minutes a month and devices at once: Free 30 and 1; Hobby, $2 a month, 200 and 1; Pro, $10 a month, unlimited and 2. See Plans and minutes.
  • The adb tunnel and ramus dev need the CLI and Android platform-tools on your machine.

For other tools, see How to connect Ramus to any coding agent. The full command and tool reference is at ramus.dev/agents.md.

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