Skip to content
Blog

How to connect and use Ramus from Goose

Add Ramus to Goose as a remote extension so it can run your APK on a cloud Android device, check a flow and send you a link to share.

GoosePublished

On this page

This guide is for developers who build Android apps with Goose, the open-source agent that started at Block and now lives in the Agentic AI Foundation. It covers the Goose CLI and Goose Desktop. At the end, Goose can start a cloud Android device, install your APK, check a flow and send you a link, so you or a reviewer can watch the device. You take over from your device page.

What you need

  • Goose 1.46 or later, CLI or Desktop. Earlier versions cannot finish the Ramus sign-in. goose --version shows the CLI's version and goose update upgrades it.
  • A model provider set up in Goose, with a model that can call tools.
  • 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

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

Add the extension from the CLI

Run goose configure, choose Add Extension, then Remote Extension (Streamable HTTP), and answer the prompts:

  1. Name: ramus.
  2. Endpoint URI: https://api.ramus.dev/api/mcp.
  3. Timeout: 300 seconds.
  4. Description: anything, such as Cloud Android devices.
  5. Custom headers: No.

Answer No to custom headers, because Goose signs in instead. The result is an entry like this one, which you can also write by hand:

~/.config/goose/config.yaml
extensions:
  ramus:
    enabled: true
    type: streamable_http
    name: ramus
    description: Cloud Android devices
    uri: https://api.ramus.dev/api/mcp
    timeout: 300

Keep the timeout at 300 seconds, the Goose default. android_start_session and android_install_app can wait up to 50 seconds for a device or an install.

Add the extension in Goose Desktop

Open the sidebar, click Extensions, then Add custom extension. Set Type to Streamable HTTP, Extension Name to ramus, Endpoint to https://api.ramus.dev/api/mcp and Timeout to 300, then click Add. Leave Request Headers empty.

Goose Desktop also installs extensions from a link. Opening this one on a machine with Goose Desktop adds the same extension:

goose://extension?url=https%3A%2F%2Fapi.ramus.dev%2Fapi%2Fmcp&type=streamable_http&id=ramus&name=ramus&description=Cloud%20Android%20devices&timeout=300

Sign in with Ramus

Start a session with goose session, or open a new chat in Goose Desktop. When Goose starts the extension, Ramus asks for a sign-in, and Goose opens your browser at Ramus's consent page, Connect goose to Ramus, which says the app is published by goose-docs.ai and runs on your own computer. Sign in with GitHub if Ramus asks and choose Allow.

If no browser opens, the Goose CLI prints the sign-in address. Goose waits five minutes for you to finish, then stores the tokens and refreshes them, so you sign in once.

The Ramus consent page: Connect goose to Ramus, published by goose-docs.ai, returning to 127.0.0.1, listing what it will be able to do, with Deny and AllowThe Ramus consent page: Connect goose to Ramus, published by goose-docs.ai, returning to 127.0.0.1, listing what it will be able to do, with Deny and Allow
Ramus asks you to approve Goose once.

Or use an API key

If you cannot sign in from a browser on this machine, use a key instead. Create one on the Agents page, tab API key. Goose expands ${VAR} in a header value from the variables the extension lists in env_keys, so the key stays out of config.yaml:

~/.config/goose/config.yaml
extensions:
  ramus:
    enabled: true
    type: streamable_http
    name: ramus
    description: Cloud Android devices
    uri: https://api.ramus.dev/api/mcp
    env_keys:
      - RAMUS_API_KEY
    headers:
      Authorization: Bearer ${RAMUS_API_KEY}
    timeout: 300

Goose looks for RAMUS_API_KEY in its own environment first, then in its secret store, the system keychain. For the CLI, export the variable in the shell you start Goose from. In Goose Desktop, add RAMUS_API_KEY under Environment Variables in the extension dialog, which saves it to the secret store, and Authorization with the value Bearer ${RAMUS_API_KEY} under Request Headers.

Check the connection

When you start goose session, a Ramus extension that fails to load shows up before your first prompt as a line starting ⚠ Failed to start extension 'ramus'. goose info -v lists your enabled extensions. In Goose Desktop, the Extensions page shows ramus switched on. Goose names each tool after its extension, such as ramus__android_snapshot, and lists them if you ask "Which Ramus tools do you have?"

Then open ramus.dev/agents. After Goose's first tool call, goose appears under Connected, with Sign-in (or your API key's name) and the time of its last call. Disconnect or Revoke 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 Developer extension, on by default, gives Goose a shell tool, so Goose can also 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:

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 Goose the Ramus loop. Goose loads 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

Goose picks the skill when a task matches it. In the CLI, /skills android-use loads it by name. Goose also reads skills from .claude/skills/, so a copy your team saved for Claude Code works too.

For context that is always on, add one line to AGENTS.md. Goose reads AGENTS.md and .goosehints from your working directory up to the repository root:

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

MCP or CLI

Both reach the same devices on the same account. This guide follows MCP: Goose calls typed tools and gets screenshots back as images. Use the CLI for adb and ramus dev. If both are set up, tell Goose which one to use.

Goose's docs say it works best with fewer than 25 tools enabled in total, and Ramus brings 24. If you only use the CLI, switch the Ramus extension off. If you use MCP, load only the tools the test loop needs by adding available_tools to the ramus entry:

~/.config/goose/config.yaml
    available_tools:
      - android_start_session
      - android_session_status
      - android_list_sessions
      - android_upload_apk
      - android_install_app
      - android_snapshot
      - android_screenshot
      - android_tap
      - android_type
      - android_swipe
      - android_press
      - android_wait_for
      - android_logcat
      - android_app_launch
      - android_share_session
      - android_list_feedback
      - android_get_feedback
      - android_resolve_feedback
      - android_end_session

Share the setup as a recipe

A Goose recipe bundles instructions, a prompt and extensions in one file your team can run. When a recipe lists extensions, it gets only those, so include developer for the build:

android-check.yaml
version: "1.0.0"
title: Check an Android flow on Ramus
description: Build the debug APK, run it on a Ramus device and check one flow.
instructions: |
  Test Android changes on Ramus (https://ramus.dev/agents.md). Build the APK
  with the project's own tooling. Send the watch link as soon as you have it.
  On failure, take a screenshot and read logcat before changing anything.
prompt: |
  Build the debug APK, install it on a Ramus device and check this flow: {{ flow }}
parameters:
  - key: flow
    input_type: string
    requirement: required
    description: The flow to check, in plain words
extensions:
  - type: platform
    name: developer
  - type: streamable_http
    name: ramus
    description: Cloud Android devices
    uri: https://api.ramus.dev/api/mcp
    timeout: 300

Run it with goose run --recipe android-check.yaml --params flow="sign in and reach Welcome" --interactive.

Start a device

Goose runs in Autonomous mode by default, so it calls Ramus tools without asking. Start with the demo app:

YoutoGoose

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

Goose calls android_start_session with demo: true. 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. 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 Goose works.

Install your app

To run your own build, Goose 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. Goose runs the curl command with its shell tool and 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.
YoutoGoose

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

With the CLI the same step is one command. To start a new device from the APK:

ramus session start
$ 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.

To install a new build on a running device, use ramus session install. Pass the session id when the device was started over MCP, because the CLI only remembers devices it started itself:

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

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; Goose uploads the file itself.

Let the agent check a flow

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

YoutoGoose

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.

Goose 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 Goose 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.

When a step fails, Goose 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.

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

To see each step before it runs, switch the session to Manual Approval with /mode approve, or Smart Approval with /mode smart_approve. Then set each Ramus tool to Always Allow, Ask Before or Never Allow under goose configure, goose settings, Tool Permission. Reads such as android_snapshot and android_logcat are safe to allow. Keep android_start_session, android_share_session and android_end_session on Ask Before: they use minutes or hand out access.

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:

YoutoGoose

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

Goose 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 Goose 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:

YoutoGoose

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

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

YoutoGoose

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

  • Failed to start extension 'ramus'. Check the uri and type: streamable_http. Goose reads config.yaml when a session starts, so start a new session after you edit it.
  • The sign-in ends in "Auth required". Update to Goose 1.46 or later. If the browser cannot reach 127.0.0.1 on the machine running Goose, for example over SSH, use an API key instead.
  • The browser opens a Ramus sign-in although you set an API key. Ramus refused the header and Goose fell back to signing in. Check that env_keys lists RAMUS_API_KEY and that the variable is set where Goose runs; without that entry, ${RAMUS_API_KEY} is sent as written. Goose Desktop started from the Dock does not see your shell profile, so use its Environment Variables field.
  • Goose cannot find ramus from its shell tool. Goose Desktop may not load your shell startup files, so a CLI installed through a Node version manager can be missing from its PATH. Use the full path, or npx ramus-cli.
  • Goose does not use the Ramus tools. Check that the extension is on for this session (the puzzle button at the bottom of Goose Desktop), name Ramus in your prompt, and check available_tools if you set it.
  • stale_ref. Goose 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: Goose 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: Goose opens the consent page again, or check your key on the Agents page. A 403 with insufficient_scope means the token lacks a permission the tool needs: approve every permission when Goose asks again.

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.