{"title":"Understand Appium 3 Drivers, Plugins, and Boundaries","excerpt":"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.","canonicalUrl":"https://automationtester.in/blog/automation-tutorials/appium-drivers-plugins-test-boundaries","category":{"name":"Automation Tutorials","slug":"automation-tutorials"},"tags":["appium","mobile-testing","uiautomator2","xcuitest","ci-cd","test-environment","dependency-pinning"],"author":{"name":"Shashank Rawlani","url":"https://shashank.rawlani.com","linkedin":"https://www.linkedin.com/in/shashankrawlani/"},"datePublished":"2026-10-13T13:30:00.000Z","dateModified":"2026-10-14T11:09:52.936Z","coverImage":{"url":"https://ik.imagekit.io/automationtester/automationtester-in/blog/covers/v3/appium-drivers-plugins-test-boundaries.webp","alt":"Dark technical illustration read left to right. Near the left, five orange pills float at uneven heights, each linked by a faint dashed line to a dashed orange outline of itself sitting at a different height. An orange curve runs from each pill into one row of a tall green panel in the middle, where every row has a ringed green dot pierced by a short vertical green bar, followed by a green pill. Five green curves leave the panel and fan into a dashed green boundary on the right that holds a stack of five green layered panels, each a little narrower than the one above, each with a pill and a small dot. A dashed orange path climbs out of the top of the boundary and ends at a crossed-out orange ring; a green curve leaves the middle panel of the stack, crosses the boundary and ends in an arrowhead at a large ringed green dot. A grey rail enters from the left edge and stops at a small grey ringed dot. Grey rules and faint pills run along the top and bottom edges."},"contentHtml":"<div class=\"callout callout-info\"><strong>Quick answer:</strong> 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 <code class=\"language-text\">package.json</code> and a lockfile, activate plugins by name, put the listen address and every insecure feature in <code class=\"language-text\">.appiumrc.json</code> with a driver prefix, keep capabilities per platform in version control, and make CI fail when <code class=\"language-text\">appium driver list --installed --json</code> disagrees with what the repository declares.</div>\n\n<p>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 <code class=\"language-text\">npm install -g appium</code> and <code class=\"language-text\">appium driver install uiautomator2</code>, 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.</p>\n\n<p>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.</p>\n\n<div class=\"callout callout-warning\"><strong>Appium 3 is the current major.</strong> Versions checked on the npm registry on 2026-09-30: <code class=\"language-text\">appium</code> 3.8.0 (3.0.0 was published on 2025-08-18; the last Appium 2 release is 2.19.0), <code class=\"language-text\">appium-uiautomator2-driver</code> 8.7.0, <code class=\"language-text\">appium-xcuitest-driver</code> 12.13.3. Appium 3 requires Node.js in the range <code class=\"language-text\">^20.19.0 || ^22.12.0 || &gt;=24.0.0</code> and npm 10 or newer. The registry's <code class=\"language-text\">beta</code> 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.</div>\n\n<h2 id=\"what-a-bare-appium-install-leaves-undecided\">What a bare Appium install leaves undecided</h2>\n\n<p>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.</p>\n\n<p>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:</p>\n\n<pre class=\"language-bash\"><code class=\"language-bash\">$ npm view appium-uiautomator2-driver@8.7.0 dependencies.appium-uiautomator2-server\n10.6.6\n$ npm view appium-xcuitest-driver@12.13.3 dependencies.appium-webdriveragent\n16.12.11</code></pre>\n\n<p>So the driver version is also the version of the code running on the phone. On 2026-09-30 the newest <code class=\"language-text\">appium-webdriveragent</code> 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.</p>\n\n<p>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.</p>\n\n<h2 id=\"the-appium-3-changes-your-setup-will-hit\">The Appium 3 changes your setup will hit</h2>\n\n<p>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.</p>\n\n<p>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.</p>\n\n<p>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:</p>\n\n<pre class=\"language-text\"><code class=\"language-text\">$ npx appium --allow-insecure=adb_shell\nError: The full feature name must include both the destination automation name or\nthe '*' wildcard to apply the feature to all installed drivers, and the feature name\nsplit by a colon. Got 'adb_shell' instead</code></pre>\n\n<p>Session discovery moved behind a flag. <code class=\"language-text\">GET /sessions</code> is gone; its replacement, <code class=\"language-text\">GET /appium/sessions</code>, only answers when the server was started with the <code class=\"language-text\">session_discovery</code> 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.</p>\n\n<p>The fourth change bites old clients rather than old setups: <code class=\"language-text\">POST /session</code> no longer accepts <code class=\"language-text\">desiredCapabilities</code> or <code class=\"language-text\">requiredCapabilities</code>, only the W3C <code class=\"language-text\">capabilities</code> object. If a homegrown HTTP helper still builds the JSON Wire payload, it fails at session creation.</p>\n\n<h2 id=\"a-project-manifest-beats-a-global-install\">A project manifest beats a global install</h2>\n\n<p>The shape that produced the Monday failure is the one most setup guides still show, including Appium's own migration page:</p>\n\n<pre class=\"language-bash\"><code class=\"language-bash\"># Every line resolves \"newest\" on the day it runs, into state outside the repo.\nnpm install -g appium\nappium driver install uiautomator2\nappium driver install xcuitest\nappium plugin install images</code></pre>\n\n<p>Appium supports two ways out. The first is the extension CLI with exact versions and a dedicated <code class=\"language-text\">APPIUM_HOME</code>, the directory where \"Appium manages extensions\"; it defaults to <code class=\"language-text\">.appium</code> in your home directory, which is shared by every project on the machine.</p>\n\n<pre class=\"language-bash\"><code class=\"language-bash\">export APPIUM_HOME=\"$PWD/.appium-home\"\nnpx --yes appium@3.8.0 driver install uiautomator2@8.7.0\nnpx --yes appium@3.8.0 driver install xcuitest@12.13.3\nnpx --yes appium@3.8.0 plugin install images@5.0.1</code></pre>\n\n<p>That fixes the top-level versions and the location. It does not fix the tree beneath them. Install a plugin into an empty <code class=\"language-text\">APPIUM_HOME</code> with Appium 3.8.0 and list the directory: you get <code class=\"language-text\">package.json</code> and <code class=\"language-text\">node_modules</code>, and no <code class=\"language-text\">package-lock.json</code>. The drivers pin their direct dependencies exactly, but those dependencies use caret ranges of their own (<code class=\"language-text\">appium-adb</code> 16.0.5 depends on <code class=\"language-text\">@appium/support ^7.2.6</code>, for instance), so two installs a month apart can resolve different transitive code.</p>\n\n<p>The second way closes that gap. When <code class=\"language-text\">APPIUM_HOME</code> is not set, Appium checks whether the current directory is inside an npm package that lists <code class=\"language-text\">appium</code> as a dependency, and if so loads drivers and plugins from that <code class=\"language-text\">package.json</code>. The extensions become ordinary dependencies, and <code class=\"language-text\">npm ci</code> reproduces the whole tree from the lockfile:</p>\n\n<pre class=\"language-json\"><code class=\"language-json\">{\n  \"private\": true,\n  \"engines\": { \"node\": \"&gt;=22.12.0\" },\n  \"devDependencies\": {\n    \"appium\": \"3.8.0\",\n    \"appium-uiautomator2-driver\": \"8.7.0\",\n    \"appium-xcuitest-driver\": \"12.13.3\",\n    \"@appium/images-plugin\": \"5.0.1\"\n  },\n  \"scripts\": {\n    \"appium\": \"appium\",\n    \"appium:check\": \"node ci/check-appium-stack.mjs\"\n  }\n}</code></pre>\n\n<p>Start the server with <code class=\"language-text\">npx appium</code> from that directory and the log says where it looked: <code class=\"language-text\">The autodetected Appium home path: /path/to/your/repo</code>. If that line shows <code class=\"language-text\">~/.appium</code> instead, something set <code class=\"language-text\">APPIUM_HOME</code> in the environment and your manifest is being ignored.</p>\n\n<p>The cost is that a test repository written in Java or Python gains a <code class=\"language-text\">package.json</code>, 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 <code class=\"language-text\">APPIUM_HOME</code> form and accept that the drift gate further down is your only defence.</p>\n\n<p>One more CLI behaviour matters here: <code class=\"language-text\">appium driver update</code> only moves minor and patch versions unless you pass <code class=\"language-text\">--unsafe</code>. A setup script that \"updates drivers\" on every run is still drift, just bounded drift.</p>\n\n<h2 id=\"installed-plugins-stay-inactive-until-named\">Installed plugins stay inactive until named</h2>\n\n<p>Drivers and plugins have opposite defaults, and the asymmetry catches people in both directions. The server reference says of <code class=\"language-text\">--use-drivers</code>: \"By default, all installed drivers are activated.\" Of <code class=\"language-text\">--use-plugins</code>: \"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.</p>\n\n<p>Name both explicitly. The server log confirms the result, marking loaded plugins with <code class=\"language-text\">(ACTIVE)</code> under <code class=\"language-text\">Available plugins</code>, which is worth keeping in the CI artefacts next to the test report.</p>\n\n<p>The official plugin list includes <code class=\"language-text\">relaxed-caps</code>, 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.</p>\n\n<h2 id=\"security-settings-that-live-in-the-repository\">Security settings that live in the repository</h2>\n\n<p>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 <code class=\"language-text\">--relaxed-security</code>, 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.</p>\n\n<p>The server has a second default worth changing at the same time. <code class=\"language-text\">--address</code> defaults to <code class=\"language-text\">0.0.0.0</code>, 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.</p>\n\n<p>Appium reads a config file from the working directory upward, and <code class=\"language-text\">.appiumrc.json</code> is the recommended name. Every server argument becomes a kebab-case property under <code class=\"language-text\">server</code>, with list arguments written as arrays:</p>\n\n<pre class=\"language-json\"><code class=\"language-json\">{\n  \"server\": {\n    \"address\": \"127.0.0.1\",\n    \"port\": 4723,\n    \"use-drivers\": [\"uiautomator2\", \"xcuitest\"],\n    \"use-plugins\": [\"images\"],\n    \"relaxed-security\": false,\n    \"allow-insecure\": [\n      \"uiautomator2:chromedriver_autodownload\",\n      \"xcuitest:shutdown_other_sims\"\n    ],\n    \"deny-insecure\": [\n      \"*:adb_shell\",\n      \"*:session_discovery\"\n    ]\n  }\n}</code></pre>\n\n<p>Each allowed feature names the driver it applies to, using the driver's automation name as the prefix, so <code class=\"language-text\">uiautomator2:chromedriver_autodownload</code> lets UiAutomator2 fetch a matching Chromedriver without granting anything to XCUITest. The deny list is a tripwire rather than a restatement of defaults: <code class=\"language-text\">--deny-insecure</code> is \"applied after both\" the allow list and relaxed security, so if someone later adds <code class=\"language-text\">--relaxed-security</code> to a CI command line, shell access through <code class=\"language-text\">mobile: shell</code> still stays off. The server echoes the result at startup under <code class=\"language-text\">Explicitly disabling insecure features</code>.</p>\n\n<p>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 <code class=\"language-text\">--relaxed-security</code> and <code class=\"language-text\">--allow-insecure</code> 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 <a href=\"/tools/env-parser\">Environment Variable Parser</a> is a quick way to spot a duplicated or conflicting key before it reaches the job.</p>\n\n<h2 id=\"capabilities-per-platform-under-review\">Capabilities per platform, under review</h2>\n\n<p>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 <code class=\"language-text\">appium:</code> vendor prefix explicitly even when your client would add it.</p>\n\n<pre class=\"language-json\"><code class=\"language-json\">{\n  \"platformName\": \"Android\",\n  \"appium:automationName\": \"UiAutomator2\",\n  \"appium:avd\": \"ci_api_35\",\n  \"appium:platformVersion\": \"15\",\n  \"appium:app\": \"build/outputs/apk/debug/app-debug.apk\",\n  \"appium:systemPort\": 8210,\n  \"appium:uiautomator2ServerLaunchTimeout\": 60000,\n  \"appium:newCommandTimeout\": 120\n}</code></pre>\n\n<pre class=\"language-json\"><code class=\"language-json\">{\n  \"platformName\": \"iOS\",\n  \"appium:automationName\": \"XCUITest\",\n  \"appium:deviceName\": \"iPhone 16\",\n  \"appium:platformVersion\": \"18.4\",\n  \"appium:app\": \"build/Debug-iphonesimulator/App.app\",\n  \"appium:usePreinstalledWDA\": true,\n  \"appium:prebuiltWDAPath\": \"wda/WebDriverAgentRunner-Runner.app\",\n  \"appium:wdaLaunchTimeout\": 120000\n}</code></pre>\n\n<p>A few of these are chosen for reproducibility rather than convenience. UiAutomator2 treats <code class=\"language-text\">appium:deviceName</code> as \"not used to select a device under test\"; <code class=\"language-text\">appium:avd</code> for an emulator or <code class=\"language-text\">appium:udid</code> for hardware is what actually picks one, and the README asks for <code class=\"language-text\">udid</code> and an explicit <code class=\"language-text\">systemPort</code> whenever sessions run in parallel. On iOS, the prebuilt WebDriverAgent pair skips the <code class=\"language-text\">xcodebuild</code> step at session start. The driver can fetch the matching runner itself with <code class=\"language-text\">appium driver run xcuitest download-wda -- --kind=sim --platform=ios --outdir=wda</code>, 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.</p>\n\n<h2 id=\"a-drift-gate-that-fails-the-build\">A drift gate that fails the build</h2>\n\n<p>A lockfile says what should be installed. CI should prove what is. Declare the expected stack in one small file:</p>\n\n<pre class=\"language-json\"><code class=\"language-json\">{\n  \"server\": \"3.8.0\",\n  \"drivers\": { \"uiautomator2\": \"8.7.0\", \"xcuitest\": \"12.13.3\" },\n  \"plugins\": { \"images\": \"5.0.1\" }\n}</code></pre>\n\n<p>Then compare it with what the server reports. <code class=\"language-text\">appium driver list --installed --json</code> returns an object keyed by driver name, with <code class=\"language-text\">version</code>, <code class=\"language-text\">installSpec</code> and <code class=\"language-text\">installPath</code> for each; <code class=\"language-text\">appium plugin list</code> takes the same flags.</p>\n\n<pre class=\"language-javascript\"><code class=\"language-javascript\">// ci/check-appium-stack.mjs\nimport { execFileSync } from \"node:child_process\";\nimport { readFileSync } from \"node:fs\";\n\nconst expected = JSON.parse(readFileSync(\"appium-stack.json\", \"utf8\"));\nconst appium = (...args) =&gt;\n  execFileSync(\"node_modules/.bin/appium\", args, {\n    encoding: \"utf8\",\n    stdio: [\"ignore\", \"pipe\", \"ignore\"],\n  });\n\nconst problems = [];\n\nconst server = appium(\"--version\").trim();\nif (server !== expected.server) {\n  problems.push(`appium server ${server}, expected ${expected.server}`);\n}\n\nfor (const kind of [\"driver\", \"plugin\"]) {\n  const installed = JSON.parse(appium(kind, \"list\", \"--installed\", \"--json\"));\n  const declared = expected[`${kind}s`];\n\n  for (const [name, version] of Object.entries(declared)) {\n    const actual = installed[name]?.version ?? \"missing\";\n    if (actual !== version) problems.push(`${kind} ${name}: ${actual}, expected ${version}`);\n  }\n  for (const name of Object.keys(installed)) {\n    if (!(name in declared)) problems.push(`${kind} ${name}: installed but not declared`);\n  }\n}\n\nif (problems.length &gt; 0) {\n  console.error(`Appium stack drift:\\n  ${problems.join(\"\\n  \")}`);\n  process.exit(1);\n}\nconsole.log(\"Appium stack matches appium-stack.json\");</code></pre>\n\n<p>Run against a declared driver version that differs from the installed one, the script prints <code class=\"language-text\">driver uiautomator2: 8.7.0, expected 8.6.0</code> and exits 1. The script calls the project's own binary rather than <code class=\"language-text\">npx appium</code> on purpose. When the package is absent locally, <code class=\"language-text\">npx</code> 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 <code class=\"language-text\">ENOENT</code>.</p>\n\n<p>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 <code class=\"language-text\">appium driver doctor uiautomator2</code> 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 <code class=\"language-text\">ANDROID_HOME</code> or <code class=\"language-text\">JAVA_HOME</code>, so it works as a gate without parsing anything. Put it first in the job and record the toolchain beside it:</p>\n\n<pre class=\"language-bash\"><code class=\"language-bash\">#!/usr/bin/env bash\nset -euo pipefail\n\nnpm ci\nnpm run appium:check\n\nmkdir -p artifacts\n{\n  node --version\n  npx appium --version\n  java -version 2&gt;&amp;1 | head -n 1\n  \"$ANDROID_HOME/platform-tools/adb\" version | head -n 1\n  if [[ \"$(uname)\" == \"Darwin\" ]]; then\n    xcodebuild -version\n    xcrun simctl list runtimes\n  fi\n} &gt; artifacts/toolchain.txt\n\nnpx appium driver doctor uiautomator2\nif [[ \"$(uname)\" == \"Darwin\" ]]; then\n  npx appium driver doctor xcuitest\nfi</code></pre>\n\n<p>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, <code class=\"language-text\">toolchain.txt</code> from the last green run is the evidence that tells you what moved.</p>\n\n<h2 id=\"what-pinning-costs\">What pinning costs</h2>\n\n<p>Every upgrade becomes a pull request that changes the lockfile, <code class=\"language-text\">appium-stack.json</code> and possibly the Xcode image together. Someone now has to watch the driver changelogs, because fixes no longer arrive by accident. <code class=\"language-text\">appium driver list --installed --updates</code> reports what is available for npm-installed extensions and is a reasonable weekly job.</p>\n\n<p>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.</p>\n\n<h2 id=\"pin-the-stack-before-the-next-ci-run\">Pin the stack before the next CI run</h2>\n\n<p>On a machine where the suite passes today, run <code class=\"language-text\">appium --version</code>, <code class=\"language-text\">appium driver list --installed --json</code> and <code class=\"language-text\">appium plugin list --installed --json</code>. Those outputs are the versions your green run actually used. Write them into a project <code class=\"language-text\">package.json</code> with exact versions, commit the lockfile, and add <code class=\"language-text\">appium-stack.json</code> with the same numbers.</p>\n\n<p>Then move every server flag out of the CI script and into <code class=\"language-text\">.appiumrc.json</code>: loopback address, named drivers, named plugins, prefixed insecure features, and a deny list for shell access. Delete <code class=\"language-text\">--relaxed-security</code> wherever it appears and see which test fails; that test tells you the one feature you need to allow by name.</p>\n\n<p>Evidence to keep with each CI run: the drift check output, the doctor output for each driver, <code class=\"language-text\">toolchain.txt</code>, and the server startup log showing the active plugins and disabled insecure features.</p>\n\n<h2 id=\"appium-extension-questions\">Questions about pinning Appium extensions</h2>\n\n<h3 id=\"does-appium-setup-give-a-reproducible-environment\">Does <code class=\"language-text\">appium setup</code> give a reproducible environment?</h3>\n\n<p>No. <code class=\"language-text\">appium setup mobile</code> 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. <code class=\"language-text\">appium setup reset</code> 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.</p>\n\n<h3 id=\"where-does-appium-record-installed-extensions\">Where does Appium record what is installed?</h3>\n\n<p>In <code class=\"language-text\">extensions.yaml</code> under <code class=\"language-text\">$APPIUM_HOME/node_modules/.cache/appium/</code>. Each entry keeps the resolved <code class=\"language-text\">version</code> and the <code class=\"language-text\">installSpec</code> you typed, so an entry reading <code class=\"language-text\">installSpec: universal-xml</code> beside <code class=\"language-text\">version: 3.0.1</code> tells you the version was whatever was newest that day. Treat it as a cache to inspect, not a file to commit.</p>\n\n<h3 id=\"do-java-or-python-clients-need-the-package-json\">Do Java or Python test projects really need a <code class=\"language-text\">package.json</code>?</h3>\n\n<p>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 <code class=\"language-text\">appium/</code> directory in the repository, with the CI job starting the server from there before the Maven, Gradle or pytest step runs.</p>\n\n<h2 id=\"appium-stack-references\">References</h2>\n\n<ul>\n<li><a href=\"https://appium.io/docs/en/latest/guides/migrating-2-to-3/\">Appium docs: Migrating to Appium 3</a>: the Node 20.19.0 floor, the mandatory feature prefix with the wildcard alternative, session discovery moving to <code class=\"language-text\">GET /appium/sessions</code> behind <code class=\"language-text\">session_discovery</code>, and the removal of <code class=\"language-text\">desiredCapabilities</code> from <code class=\"language-text\">POST /session</code>.</li>\n<li><a href=\"https://appium.io/docs/en/latest/guides/security/\">Appium docs: Appium Server Security</a>: the precedence of <code class=\"language-text\">--relaxed-security</code>, <code class=\"language-text\">--allow-insecure</code> and <code class=\"language-text\">--deny-insecure</code>, the automation-name prefix syntax, and the assumption that the port is not exposed.</li>\n<li><a href=\"https://appium.io/docs/en/latest/reference/cli/server/\">Appium docs: Server command-line arguments</a>: the <code class=\"language-text\">0.0.0.0</code> default address, all drivers active by default, no plugins active by default, and the warning attached to relaxed security.</li>\n<li><a href=\"https://appium.io/docs/en/latest/reference/cli/insecure-features/\">Appium docs: Insecure Features reference</a>: <code class=\"language-text\">session_discovery</code> as a server-level feature that needs the wildcard prefix.</li>\n<li><a href=\"https://appium.io/docs/en/latest/guides/managing-exts/\">Appium docs: Managing Drivers and Plugins</a>: <code class=\"language-text\">APPIUM_HOME</code> and its default, the location of <code class=\"language-text\">extensions.yaml</code>, and project-mode loading from <code class=\"language-text\">package.json</code> when <code class=\"language-text\">APPIUM_HOME</code> is unset.</li>\n<li><a href=\"https://appium.io/docs/en/latest/reference/cli/extensions/\">Appium docs: appium driver/plugin</a>: <code class=\"language-text\">install</code> with a version modifier, <code class=\"language-text\">list --installed --json --updates</code>, <code class=\"language-text\">doctor</code>, <code class=\"language-text\">run</code>, and <code class=\"language-text\">update</code> staying within minor versions without <code class=\"language-text\">--unsafe</code>.</li>\n<li><a href=\"https://appium.io/docs/en/latest/reference/cli/setup/\">Appium docs: appium setup</a>: the unversioned presets and what <code class=\"language-text\">reset</code> removes.</li>\n<li><a href=\"https://appium.io/docs/en/latest/guides/config/\">Appium docs: The Appium Config File</a>: <code class=\"language-text\">.appiumrc.json</code> discovery, the kebab-case <code class=\"language-text\">server</code> property layout, and CLI arguments taking precedence over the file.</li>\n<li><a href=\"https://appium.io/docs/en/latest/guides/caps/\">Appium docs: Session Capabilities</a>: the <code class=\"language-text\">appium:</code> vendor prefix and capabilities being fixed for the life of a session.</li>\n<li><a href=\"https://appium.io/docs/en/latest/intro/drivers/\">Appium docs: Intro to Appium Drivers</a>: the eleven-layer XCUITest stack and the driver's split into a Node.js half and WebDriverAgent.</li>\n<li><a href=\"https://appium.io/docs/en/latest/ecosystem/plugins/\">Appium docs: Appium Plugins</a>: the official plugin list, including what <code class=\"language-text\">relaxed-caps</code> is for.</li>\n<li><a href=\"https://appium.io/docs/en/latest/quickstart/requirements/\">Appium docs: System Requirements</a>: the Node.js and npm ranges for the server and the pointer to per-driver doctor checks.</li>\n<li><a href=\"https://github.com/appium/appium-uiautomator2-driver/blob/v8.7.0/README.md\">UiAutomator2 driver README at v8.7.0</a>: Appium 3 only from driver 5.0.0, host prerequisites, the doctor command, the device-selection capabilities, and the <code class=\"language-text\">adb_shell</code> and <code class=\"language-text\">chromedriver_autodownload</code> feature names.</li>\n<li><a href=\"https://appium.github.io/appium-xcuitest-driver/latest/getting-started/system-requirements/\">XCUITest driver: System Requirements</a>: the two-major-versions support policy, the driver-to-Xcode table, and driver 10.0.0 and newer requiring Appium 3.</li>\n<li><a href=\"https://appium.github.io/appium-xcuitest-driver/latest/reference/security-flags/\">XCUITest driver: Insecure Features</a>: the driver's feature names, including <code class=\"language-text\">shutdown_other_sims</code> and <code class=\"language-text\">get_server_logs</code>.</li>\n<li><a href=\"https://appium.github.io/appium-xcuitest-driver/latest/guides/run-prebuilt-wda/\">XCUITest driver: Run Prebuilt WDA</a>: <code class=\"language-text\">download-wda</code>, the <code class=\"language-text\">usePreinstalledWDA</code> and <code class=\"language-text\">prebuiltWDAPath</code> capabilities, and the iOS 17 with WebDriverAgent 13 requirement.</li>\n<li><a href=\"https://appium.github.io/appium-xcuitest-driver/latest/reference/capabilities/\">XCUITest driver: Capabilities</a>: <code class=\"language-text\">appium:deviceName</code> for simulator selection, <code class=\"language-text\">appium:udid</code> for hardware and parallel runs, and <code class=\"language-text\">appium:wdaLaunchTimeout</code>.</li>\n<li><a href=\"https://docs.npmjs.com/cli/v11/commands/npx\">npm Docs: npx</a>: that <code class=\"language-text\">npx</code> installs a missing package into the npm cache after a prompt, which is why the drift gate calls the local binary.</li>\n<li><a href=\"https://www.npmjs.com/package/appium\">npm registry: appium</a>: 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 <code class=\"language-text\">beta</code> tag; the driver package pages record the exact <code class=\"language-text\">appium-uiautomator2-server</code> and <code class=\"language-text\">appium-webdriveragent</code> dependencies quoted above.</li>\n</ul>\n"}