# Understand Appium 3 Drivers, Plugins, and Boundaries

> A global Appium install is not an environment: drivers, plugins, the device-side servers they ship and the platform toolchain all version separately. This guide covers what Appium 3 changed for setups (Node 20.19, mandatory insecure-feature prefixes, session discovery), why a project package.json with a lockfile beats APPIUM_HOME alone, why installed plugins stay inactive until named, a loopback-bound .appiumrc.json with scoped allow and deny lists, per-platform capability files, and a CI gate that parses appium driver list --installed --json and runs driver doctor.

- Author: [Shashank Rawlani](https://shashank.rawlani.com)

- Published: 2026-10-13T13:30:00.000Z

- Updated: 2026-10-14T11:09:52.936Z

- Category: Automation Tutorials

- Canonical source: https://automationtester.in/blog/automation-tutorials/appium-drivers-plugins-test-boundaries

- Tags: appium, mobile-testing, uiautomator2, xcuitest, ci-cd, test-environment, dependency-pinning

**Quick answer:** An Appium environment is five separately versioned things: the server, each driver, each plugin, the platform toolchain the drivers call (Xcode, the Android SDK, a JDK) and the capabilities your tests send. Pin the first three with exact versions in a project `package.json` and a lockfile, activate plugins by name, put the listen address and every insecure feature in `.appiumrc.json` with a driver prefix, keep capabilities per platform in version control, and make CI fail when `appium driver list --installed --json` disagrees with what the repository declares.

A mobile suite that passed on Friday fails on Monday on a CI agent that was rebuilt over the weekend. Nobody touched the tests. The agent's setup script ran `npm install -g appium` and `appium driver install uiautomator2`, both of which resolve to whatever is newest on the day they run, and the driver that came back carries a different on-device server from the one on every laptop in the team. Nothing in the repository records which versions produced the green run, so there is nothing to diff.

That is the default outcome of Appium's extension model, and it is a consequence of a design decision rather than a bug. The server knows how to speak WebDriver and how to load extensions. It does not know how to automate anything until a driver is installed, and the docs say so directly: without at least one driver, "Appium won't know how to automate anything." Everything that touches a device lives in packages the server does not bring with it.

**Appium 3 is the current major.** Versions checked on the npm registry on 2026-09-30: `appium` 3.8.0 (3.0.0 was published on 2025-08-18; the last Appium 2 release is 2.19.0), `appium-uiautomator2-driver` 8.7.0, `appium-xcuitest-driver` 12.13.3. Appium 3 requires Node.js in the range `^20.19.0 || ^22.12.0 || >=24.0.0` and npm 10 or newer. The registry's `beta` tag already points at a 4.0.0 pre-release, which is one more reason to install by exact version rather than by tag or range.

## What a bare Appium install leaves undecided

The Appium docs describe what sits between a test and an iPhone when the XCUITest driver is in play: your test code, the Appium client, the Selenium client, the network, the Appium server, the XCUITest driver, WebDriverAgent, Xcode, XCUITest, iOS and macOS. Eleven layers, and the server is only one of them.

Two of those layers hide a pin you already have. The XCUITest driver is split in two: a Node.js half that runs inside Appium, and WebDriverAgent, an Objective-C WebDriver server that the driver "builds and manages" on the device. UiAutomator2 has the same shape, with most commands proxied to an on-device UiAutomator2 server. Neither device-side component is something you install yourself. Each driver release depends on an exact version of it:

```
$ npm view appium-uiautomator2-driver@8.7.0 dependencies.appium-uiautomator2-server
10.6.6
$ npm view appium-xcuitest-driver@12.13.3 dependencies.appium-webdriveragent
16.12.11
```

So the driver version is also the version of the code running on the phone. On 2026-09-30 the newest `appium-webdriveragent` on npm was 16.13.5, yet XCUITest 12.13.3 ships 16.12.11. A team that "keeps WebDriverAgent current" separately from the driver is running a combination the driver was never released with.

The driver also decides which server major you can run. The UiAutomator2 README states that "since major version 5.0.0, this driver is only compatible with Appium 3", and the XCUITest compatibility table maps driver 10.0.0 and newer to Appium 3 and 4.0.0 through 9.10.5 to Appium 2. Unpinned, a driver install on an Appium 2 host is a request for a driver that no longer targets that host.

## The Appium 3 changes your setup will hit

The migration guide calls Appium 3 "a smaller upgrade with fewer breaking changes" than Appium 2 was. Three of them land on environment definition rather than test code.

The Node floor moved from 14.17.0 to 20.19.0. A CI image that has been quietly working on Node 18 stops starting the server.

Insecure feature names now need a scope prefix. Appium 2.13 introduced the prefix as optional; Appium 3 "makes the scope prefix mandatory, and will throw an error if features are specified without a scope." Started with an Appium 2 style flag, Appium 3.8.0 refuses to boot:

```
$ npx appium --allow-insecure=adb_shell
Error: The full feature name must include both the destination automation name or
the '*' wildcard to apply the feature to all installed drivers, and the feature name
split by a colon. Got 'adb_shell' instead
```

Session discovery moved behind a flag. `GET /sessions` is gone; its replacement, `GET /appium/sessions`, only answers when the server was started with the `session_discovery` feature, and server-scope features like that one "also require the wildcard prefix". Appium Inspector's attach-to-session needs version 2025.3.1 or later for the new endpoint.

The fourth change bites old clients rather than old setups: `POST /session` no longer accepts `desiredCapabilities` or `requiredCapabilities`, only the W3C `capabilities` object. If a homegrown HTTP helper still builds the JSON Wire payload, it fails at session creation.

## A project manifest beats a global install

The shape that produced the Monday failure is the one most setup guides still show, including Appium's own migration page:

```
# Every line resolves "newest" on the day it runs, into state outside the repo.
npm install -g appium
appium driver install uiautomator2
appium driver install xcuitest
appium plugin install images
```

Appium supports two ways out. The first is the extension CLI with exact versions and a dedicated `APPIUM_HOME`, the directory where "Appium manages extensions"; it defaults to `.appium` in your home directory, which is shared by every project on the machine.

```
export APPIUM_HOME="$PWD/.appium-home"
npx --yes appium@3.8.0 driver install uiautomator2@8.7.0
npx --yes appium@3.8.0 driver install xcuitest@12.13.3
npx --yes appium@3.8.0 plugin install images@5.0.1
```

That fixes the top-level versions and the location. It does not fix the tree beneath them. Install a plugin into an empty `APPIUM_HOME` with Appium 3.8.0 and list the directory: you get `package.json` and `node_modules`, and no `package-lock.json`. The drivers pin their direct dependencies exactly, but those dependencies use caret ranges of their own (`appium-adb` 16.0.5 depends on `@appium/support ^7.2.6`, for instance), so two installs a month apart can resolve different transitive code.

The second way closes that gap. When `APPIUM_HOME` is not set, Appium checks whether the current directory is inside an npm package that lists `appium` as a dependency, and if so loads drivers and plugins from that `package.json`. The extensions become ordinary dependencies, and `npm ci` reproduces the whole tree from the lockfile:

```
{
  "private": true,
  "engines": { "node": ">=22.12.0" },
  "devDependencies": {
    "appium": "3.8.0",
    "appium-uiautomator2-driver": "8.7.0",
    "appium-xcuitest-driver": "12.13.3",
    "@appium/images-plugin": "5.0.1"
  },
  "scripts": {
    "appium": "appium",
    "appium:check": "node ci/check-appium-stack.mjs"
  }
}
```

Start the server with `npx appium` from that directory and the log says where it looked: `The autodetected Appium home path: /path/to/your/repo`. If that line shows `~/.appium` instead, something set `APPIUM_HOME` in the environment and your manifest is being ignored.

The cost is that a test repository written in Java or Python gains a `package.json`, and Appium's docs recommend the project route "only if you are already using npm for your project". Take the trade anyway when the suite runs in CI. A lockfile you did not otherwise need is cheaper than a stack you cannot rebuild. If the repository really cannot carry one, use the pinned `APPIUM_HOME` form and accept that the drift gate further down is your only defence.

One more CLI behaviour matters here: `appium driver update` only moves minor and patch versions unless you pass `--unsafe`. A setup script that "updates drivers" on every run is still drift, just bounded drift.

## Installed plugins stay inactive until named

Drivers and plugins have opposite defaults, and the asymmetry catches people in both directions. The server reference says of `--use-drivers`: "By default, all installed drivers are activated." Of `--use-plugins`: "By default, no plugins are activated." An installed images plugin does nothing until the server is told to load it, and an installed driver you forgot about is live whether you wanted it or not.

Name both explicitly. The server log confirms the result, marking loaded plugins with `(ACTIVE)` under `Available plugins`, which is worth keeping in the CI artefacts next to the test report.

The official plugin list includes `relaxed-caps`, which exists to "relax Appium's requirement for vendor prefixes on capabilities". Installing it to make an old capability file work hides exactly the thing you want visible in review: which keys are W3C standard and which belong to a driver.

## Security settings that live in the repository

This is the setting most often got wrong in CI, usually with good intentions: a test needs one shell command, so the job starts the server with `--relaxed-security`, which "turns on all insecure features". The server reference adds that it should be used only "if all clients are in a trusted network and could not potentially break out of the session sandbox." A shared CI agent does not meet that bar.

The server has a second default worth changing at the same time. `--address` defaults to `0.0.0.0`, every interface on the host, while Appium's security guide assumes you "don't expose Appium's port(s) to the wider internet." A server that only needs to talk to test processes on the same agent should bind to loopback.

Appium reads a config file from the working directory upward, and `.appiumrc.json` is the recommended name. Every server argument becomes a kebab-case property under `server`, with list arguments written as arrays:

```
{
  "server": {
    "address": "127.0.0.1",
    "port": 4723,
    "use-drivers": ["uiautomator2", "xcuitest"],
    "use-plugins": ["images"],
    "relaxed-security": false,
    "allow-insecure": [
      "uiautomator2:chromedriver_autodownload",
      "xcuitest:shutdown_other_sims"
    ],
    "deny-insecure": [
      "*:adb_shell",
      "*:session_discovery"
    ]
  }
}
```

Each allowed feature names the driver it applies to, using the driver's automation name as the prefix, so `uiautomator2:chromedriver_autodownload` lets UiAutomator2 fetch a matching Chromedriver without granting anything to XCUITest. The deny list is a tripwire rather than a restatement of defaults: `--deny-insecure` is "applied after both" the allow list and relaxed security, so if someone later adds `--relaxed-security` to a CI command line, shell access through `mobile: shell` still stays off. The server echoes the result at startup under `Explicitly disabling insecure features`.

Command-line arguments take precedence over the config file, so the file is only the policy if nothing in the CI job overrides it. Grep the pipeline definitions for `--relaxed-security` and `--allow-insecure` as part of review, and take feature names from each driver's own security page rather than guessing them. If the same values also live in environment files for a device farm, the [Environment Variable Parser](/tools/env-parser) is a quick way to spot a duplicated or conflicting key before it reaches the job.

## Capabilities per platform, under review

Capabilities "cannot be changed during the lifecycle of the session", which makes them part of the environment definition, not a detail of the test. Keep one file per platform next to the manifest, and write the `appium:` vendor prefix explicitly even when your client would add it.

```
{
  "platformName": "Android",
  "appium:automationName": "UiAutomator2",
  "appium:avd": "ci_api_35",
  "appium:platformVersion": "15",
  "appium:app": "build/outputs/apk/debug/app-debug.apk",
  "appium:systemPort": 8210,
  "appium:uiautomator2ServerLaunchTimeout": 60000,
  "appium:newCommandTimeout": 120
}
```

```
{
  "platformName": "iOS",
  "appium:automationName": "XCUITest",
  "appium:deviceName": "iPhone 16",
  "appium:platformVersion": "18.4",
  "appium:app": "build/Debug-iphonesimulator/App.app",
  "appium:usePreinstalledWDA": true,
  "appium:prebuiltWDAPath": "wda/WebDriverAgentRunner-Runner.app",
  "appium:wdaLaunchTimeout": 120000
}
```

A few of these are chosen for reproducibility rather than convenience. UiAutomator2 treats `appium:deviceName` as "not used to select a device under test"; `appium:avd` for an emulator or `appium:udid` for hardware is what actually picks one, and the README asks for `udid` and an explicit `systemPort` whenever sessions run in parallel. On iOS, the prebuilt WebDriverAgent pair skips the `xcodebuild` step at session start. The driver can fetch the matching runner itself with `appium driver run xcuitest download-wda -- --kind=sim --platform=ios --outdir=wda`, and because the driver pins its WebDriverAgent version, the download matches the installed driver. That route requires iOS 17 or later with WebDriverAgent 13 or newer.

## A drift gate that fails the build

A lockfile says what should be installed. CI should prove what is. Declare the expected stack in one small file:

```
{
  "server": "3.8.0",
  "drivers": { "uiautomator2": "8.7.0", "xcuitest": "12.13.3" },
  "plugins": { "images": "5.0.1" }
}
```

Then compare it with what the server reports. `appium driver list --installed --json` returns an object keyed by driver name, with `version`, `installSpec` and `installPath` for each; `appium plugin list` takes the same flags.

```
// ci/check-appium-stack.mjs
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";

const expected = JSON.parse(readFileSync("appium-stack.json", "utf8"));
const appium = (...args) =>
  execFileSync("node_modules/.bin/appium", args, {
    encoding: "utf8",
    stdio: ["ignore", "pipe", "ignore"],
  });

const problems = [];

const server = appium("--version").trim();
if (server !== expected.server) {
  problems.push(`appium server ${server}, expected ${expected.server}`);
}

for (const kind of ["driver", "plugin"]) {
  const installed = JSON.parse(appium(kind, "list", "--installed", "--json"));
  const declared = expected[`${kind}s`];

  for (const [name, version] of Object.entries(declared)) {
    const actual = installed[name]?.version ?? "missing";
    if (actual !== version) problems.push(`${kind} ${name}: ${actual}, expected ${version}`);
  }
  for (const name of Object.keys(installed)) {
    if (!(name in declared)) problems.push(`${kind} ${name}: installed but not declared`);
  }
}

if (problems.length > 0) {
  console.error(`Appium stack drift:\n  ${problems.join("\n  ")}`);
  process.exit(1);
}
console.log("Appium stack matches appium-stack.json");
```

Run against a declared driver version that differs from the installed one, the script prints `driver uiautomator2: 8.7.0, expected 8.6.0` and exits 1. The script calls the project's own binary rather than `npx appium` on purpose. When the package is absent locally, `npx` offers to fetch it from the registry, and a CI job answering yes would check the newest Appium instead of the one the lockfile installed. A missing local binary should fail the job, and here it does, with `ENOENT`.

The extensions are half the environment. The other half is the toolchain each driver calls, which no npm file captures. Each official driver ships doctor checks that "validate whether the extension has its prerequisites configured correctly", and `appium driver doctor uiautomator2` separates required fixes from optional ones. With Appium 3.8.0 and UiAutomator2 8.7.0 it exits non-zero when a required check fails, such as an unset `ANDROID_HOME` or `JAVA_HOME`, so it works as a gate without parsing anything. Put it first in the job and record the toolchain beside it:

```
#!/usr/bin/env bash
set -euo pipefail

npm ci
npm run appium:check

mkdir -p artifacts
{
  node --version
  npx appium --version
  java -version 2>&1 | head -n 1
  "$ANDROID_HOME/platform-tools/adb" version | head -n 1
  if [[ "$(uname)" == "Darwin" ]]; then
    xcodebuild -version
    xcrun simctl list runtimes
  fi
} > artifacts/toolchain.txt

npx appium driver doctor uiautomator2
if [[ "$(uname)" == "Darwin" ]]; then
  npx appium driver doctor xcuitest
fi
```

The Xcode line is not decoration. XCUITest aims to support "the latest two (2) major Xcode/iOS/iPadOS/tvOS/watchOS versions", and its compatibility table lists full support for Xcode 16.0 and newer from driver 11.1.2, with Xcode 14.0 as the oldest likely working version. When a hosted macOS image changes its default Xcode, `toolchain.txt` from the last green run is the evidence that tells you what moved.

## What pinning costs

Every upgrade becomes a pull request that changes the lockfile, `appium-stack.json` and possibly the Xcode image together. Someone now has to watch the driver changelogs, because fixes no longer arrive by accident. `appium driver list --installed --updates` reports what is available for npm-installed extensions and is a reasonable weekly job.

The pin also does not reach the device. An emulator image, a simulator runtime or a physical phone's OS version is still chosen elsewhere; which of those combinations are worth paying for is its own decision. And none of this applies to exploratory sessions through the Appium MCP server, which is a different tool with a different job.

## Pin the stack before the next CI run

On a machine where the suite passes today, run `appium --version`, `appium driver list --installed --json` and `appium plugin list --installed --json`. Those outputs are the versions your green run actually used. Write them into a project `package.json` with exact versions, commit the lockfile, and add `appium-stack.json` with the same numbers.

Then move every server flag out of the CI script and into `.appiumrc.json`: loopback address, named drivers, named plugins, prefixed insecure features, and a deny list for shell access. Delete `--relaxed-security` wherever it appears and see which test fails; that test tells you the one feature you need to allow by name.

Evidence to keep with each CI run: the drift check output, the doctor output for each driver, `toolchain.txt`, and the server startup log showing the active plugins and disabled insecure features.

## Questions about pinning Appium extensions

### Does `appium setup` give a reproducible environment?

No. `appium setup mobile` installs a preset: UiAutomator2, XCUITest (on macOS hosts only) and Espresso, plus the images and inspector plugins, with no versions. It is a quick start for a laptop. `appium setup reset` is the useful one in this context: it uninstalls every extension and its manifest from the Appium home, which clears a home left inconsistent by a failed upgrade.

### Where does Appium record what is installed?

In `extensions.yaml` under `$APPIUM_HOME/node_modules/.cache/appium/`. Each entry keeps the resolved `version` and the `installSpec` you typed, so an entry reading `installSpec: universal-xml` beside `version: 3.0.1` tells you the version was whatever was newest that day. Treat it as a cache to inspect, not a file to commit.

### Do Java or Python test projects really need a `package.json`?

Only the process that starts the server does. The client talks HTTP to whatever server is running, so the manifest can live in a small `appium/` directory in the repository, with the CI job starting the server from there before the Maven, Gradle or pytest step runs.

## References

- [Appium docs: Migrating to Appium 3](https://appium.io/docs/en/latest/guides/migrating-2-to-3/): the Node 20.19.0 floor, the mandatory feature prefix with the wildcard alternative, session discovery moving to `GET /appium/sessions` behind `session_discovery`, and the removal of `desiredCapabilities` from `POST /session`.

- [Appium docs: Appium Server Security](https://appium.io/docs/en/latest/guides/security/): the precedence of `--relaxed-security`, `--allow-insecure` and `--deny-insecure`, the automation-name prefix syntax, and the assumption that the port is not exposed.

- [Appium docs: Server command-line arguments](https://appium.io/docs/en/latest/reference/cli/server/): the `0.0.0.0` default address, all drivers active by default, no plugins active by default, and the warning attached to relaxed security.

- [Appium docs: Insecure Features reference](https://appium.io/docs/en/latest/reference/cli/insecure-features/): `session_discovery` as a server-level feature that needs the wildcard prefix.

- [Appium docs: Managing Drivers and Plugins](https://appium.io/docs/en/latest/guides/managing-exts/): `APPIUM_HOME` and its default, the location of `extensions.yaml`, and project-mode loading from `package.json` when `APPIUM_HOME` is unset.

- [Appium docs: appium driver/plugin](https://appium.io/docs/en/latest/reference/cli/extensions/): `install` with a version modifier, `list --installed --json --updates`, `doctor`, `run`, and `update` staying within minor versions without `--unsafe`.

- [Appium docs: appium setup](https://appium.io/docs/en/latest/reference/cli/setup/): the unversioned presets and what `reset` removes.

- [Appium docs: The Appium Config File](https://appium.io/docs/en/latest/guides/config/): `.appiumrc.json` discovery, the kebab-case `server` property layout, and CLI arguments taking precedence over the file.

- [Appium docs: Session Capabilities](https://appium.io/docs/en/latest/guides/caps/): the `appium:` vendor prefix and capabilities being fixed for the life of a session.

- [Appium docs: Intro to Appium Drivers](https://appium.io/docs/en/latest/intro/drivers/): the eleven-layer XCUITest stack and the driver's split into a Node.js half and WebDriverAgent.

- [Appium docs: Appium Plugins](https://appium.io/docs/en/latest/ecosystem/plugins/): the official plugin list, including what `relaxed-caps` is for.

- [Appium docs: System Requirements](https://appium.io/docs/en/latest/quickstart/requirements/): the Node.js and npm ranges for the server and the pointer to per-driver doctor checks.

- [UiAutomator2 driver README at v8.7.0](https://github.com/appium/appium-uiautomator2-driver/blob/v8.7.0/README.md): Appium 3 only from driver 5.0.0, host prerequisites, the doctor command, the device-selection capabilities, and the `adb_shell` and `chromedriver_autodownload` feature names.

- [XCUITest driver: System Requirements](https://appium.github.io/appium-xcuitest-driver/latest/getting-started/system-requirements/): the two-major-versions support policy, the driver-to-Xcode table, and driver 10.0.0 and newer requiring Appium 3.

- [XCUITest driver: Insecure Features](https://appium.github.io/appium-xcuitest-driver/latest/reference/security-flags/): the driver's feature names, including `shutdown_other_sims` and `get_server_logs`.

- [XCUITest driver: Run Prebuilt WDA](https://appium.github.io/appium-xcuitest-driver/latest/guides/run-prebuilt-wda/): `download-wda`, the `usePreinstalledWDA` and `prebuiltWDAPath` capabilities, and the iOS 17 with WebDriverAgent 13 requirement.

- [XCUITest driver: Capabilities](https://appium.github.io/appium-xcuitest-driver/latest/reference/capabilities/): `appium:deviceName` for simulator selection, `appium:udid` for hardware and parallel runs, and `appium:wdaLaunchTimeout`.

- [npm Docs: npx](https://docs.npmjs.com/cli/v11/commands/npx): that `npx` installs a missing package into the npm cache after a prompt, which is why the drift gate calls the local binary.

- [npm registry: appium](https://www.npmjs.com/package/appium): the current 3.8.0 release, the 3.0.0 and 2.19.0 publish dates, and the 4.0.0 pre-release on the `beta` tag; the driver package pages record the exact `appium-uiautomator2-server` and `appium-webdriveragent` dependencies quoted above.

## Continue reading on AutomationTester.in

- [Mock or Integrate? Playwright Network Testing Boundaries](https://automationtester.in/blog/automation-tutorials/playwright-network-mocking-integration-boundaries)
- [Build Deterministic Python API Test Data Factories](https://automationtester.in/blog/automation-tutorials/python-api-test-data-factories-deterministic)
- [Make JUnit Failures Useful in CI](https://automationtester.in/blog/automation-tutorials/junit-failure-reports-useful-ci)
- [Design Playwright Fixtures for Parallel Test Isolation](https://automationtester.in/blog/automation-tutorials/playwright-fixtures-parallel-test-isolation)
- [Debug JUnit Parallel-Execution Flakes](https://automationtester.in/blog/automation-tutorials/debug-junit-parallel-execution-flakes)

Source: [Understand Appium 3 Drivers, Plugins, and Boundaries](https://automationtester.in/blog/automation-tutorials/appium-drivers-plugins-test-boundaries) by Shashank Rawlani.
