Blolabel
← Notes
tutorial 7 min read

Five tools on one Mac refuse an iPhone's UDID in lower case, which is why Appium stopped rewriting it

Match loosely, pass through exactly. A measured table of which tools accept which spelling, and the twelve exact comparisons in our own harness that would misreport a phone sitting on the cable.

On 21 September 2026 we passed the lower-case form of our iPhone's UDID to five tools on one Mac, with the phone on the cable. All five refused it: xcodebuild, devicectl, pymobiledevice3, ideviceinfo, and our own phoneshell, which reported the phone as absent and then named that same phone as the one "present instead". Appium's XCUITest driver spent 18 and 19 September making its UDID matching case-insensitive, and the detail worth copying is what it did not do: it never rewrites the UDID.

Abstract technical illustration of one long hexadecimal identifier rendered twice in two weights, with five gates of which each admits only one form

Three releases in two days, all about letter case

appium-xcuitest-driver 12.12.5 shipped on 18 September with PR #2977 by Yash121l, fixing issue #2941. The driver decided whether appium:udid named a real device with an exact devices.includes(udid). An Apple TV paired through pair-appletv sits in the tunnel registry in upper case while Xcode lists the same device in lower case, so the Xcode form failed the match, fell through to the simulator lookup, and died with "Unknown device or simulator UDID". 12.12.6 followed on 19 September with PR #2981 by mykola-mokhnach, applying the same fix to the comparisons #2977 had left: the usbmux SerialNumber lookup, the simulator lookups, and IPC session-claim contention detection. The same day, appium-webdriveragent 16.12.9 (PR #1260) began resolving a real device's UDID to the case xcodebuild -showdestinations reports before building -destination id=, because xcodebuild's own match is case-sensitive.

The PR #2977 description gives the reason for the design: "I left the UDID value itself untouched on purpose: canonicalizing it to the registry's upper-case form would move the failure into the WDA xcodebuild destination." Match loosely, pass through exactly. The WDA change does the other half: when a string has to go to xcodebuild, look up xcodebuild's own spelling and use that one, falling back to the caller's string after a 15000 ms timeout.

What the tools on our Mac accept

Device: iPhone 17 Pro Max (iPhone18,2), iOS 26.6.2 (23G90), on USB. Its UDID starts 00008150- and ends 401C; we print only those ends here because the rest identifies a personal phone. Every tool reported it in upper case, digits and letters A to F. Each command was run twice, once with that string and once with the same string lower-cased.

tool (version)how it reports the UDIDcommandupper caselower casesource
xcodebuild (Xcode 26.6, 17F113)id:00008150-…401C in -showdestinations, upper-destination id=... -showBuildSettingsexit 0exit 64, "Unable to find a device matching the provided destination specifier", then lists the phone in upper caseour run, 21 Sep 2026
devicectl (518.33)hardwareProperties.udid, upperdevice info details --device ...osVersionNumber 26.6.2CoreDeviceError 1000, "The specified device was not found"our run, 21 Sep 2026
pymobiledevice3 (11.8.0)usbmux Identifier, upperlockdown info --udid ...ProductVersion 26.6.2"usbmux has no device matching udid"our run, 21 Sep 2026
libimobiledevice ideviceinfo (1.4.0)idevice_id -l, upperideviceinfo -u ... -k ProductVersion26.6.2"Device ... not found!"our run, 21 Sep 2026
phoneshell device_check (commit cac38a6)whatever usbmux or devicectl saysdevice_check(prefer=...)ok, "iOS 26.6.2 over USB""the configured phone is not connected. Present instead: [device name] (iPhone18,2)"our run, 21 Sep 2026
appium-xcuitest-driver 12.12.6varies by tooldevice discovery, usbmux, simulator, session claimmatchmatch, value kept as givenPR #2977, PR #2981

For an iPhone on this Mac the tools agree with each other: upper case everywhere, and nothing accepts anything else. The disagreement Appium fixed is between tools that report different cases for the same device, which in the reports is an Apple TV paired through pair-appletv. We have no Apple TV and did not test one. What the table does show is that a case-insensitive compare is safe and a case-normalising rewrite is not, since every consumer downstream insists on its own spelling.

Fix the comparison, not the string input from config, a person, or another tool 00008150-…401c (lower case) A. normalise rewrite to one case, then == comparison passes rewritten string reaches a tool with the other spelling: not found B. match loosely, pass through exactly compare lower(a) == lower(b) keep the matched record hand each tool the string it reported itself: always found Appium: 12.12.5 (18 Sep) and 12.12.6 (19 Sep) match loosely; WDA 16.12.9 (19 Sep) looks up xcodebuild's own spelling On our Mac, 4 of 4 Apple and open-source tools rejected the lower-case form; phoneshell made 5 of 5 phoneshell today: 12 exact comparisons, 0 case-insensitive, so it fails at the first one instead of later
Approach B is what the three Appium releases converge on. Tool results are from our runs on 21 September 2026; release dates from the npm registry.

Twelve places our code compares a UDID exactly

We grepped phoneshell for UDID comparisons at commit cac38a6. There are twelve, none of them case-insensitive:

filelineswhat the comparison decideswhat a case mismatch would do
phoneshell/device.py255, 258is the configured phone on usbmux, or live in devicectl"not connected", then lists the same phone as present (measured)
phoneshell/bringup.py82, 91which scanned phone is the configured oneno phone marked configured, sort order changes
phoneshell/bringup.py86dedupe the usbmux list against the devicectl listone phone listed twice, once per tool
phoneshell/bringup.py212is the configured phone among the live ones"A different iPhone is plugged in", about the same iPhone
phoneshell/bringup.py474is the resolved phone on the cabletakes the wifi path with the phone on USB
phoneshell/bringup.py482does the config need re-pinningrewrites the config to the tool's spelling, which is the harmless one
phoneshell/farm.py125, 132, 189primary phone, slot lookup, configured flagallocates and saves a second slot, with its own port pair, for the phone it already holds; the connect then fails at device_check
phoneshell/server.py612which farm member a connect request meantthe request reports failure and returns no member record
A lower-case UDID through phoneshell's farm connect: four exact comparisons, four misses server.py POST /api/farm/connect any string accepted farm.py slot_for != configured (line 125) new slot, saved to disk farm.py config_for port 8100 + slot for the lower-case id device.py device_check no exact match on usbmux or devicectl (255, 258) "the configured phone is not connected. Present instead: [device name] (iPhone18,2)" (the same phone, measured 21 Sep 2026) server.py line 612 member lookup: none 12 exact UDID comparisons in phoneshell, 0 case-insensitive. None fires while UDIDs come from usbmux or devicectl; most of them fire for a UDID a person typed. The fix is at the comparisons, never at the string. Only the device_check step was executed; the farm steps are read from the code, not reproduced.
Read from phoneshell/server.py, farm.py and device.py at commit cac38a6. The device_check message is from our run on 21 September 2026.

In normal use none of this fires, and we should say so plainly. Every UDID phoneshell stores comes from usbmux or devicectl, and line 482 pins the tool's own spelling into the config on first connect. The exposure is a UDID a person types into runtime/config.yaml or passes to the farm's connect endpoint, which takes any string in its JSON body. The measured symptom is a diagnosis that is wrong in an especially confusing way: it tells you the phone is missing while naming it. The rows after the first are read from the code, not reproduced.

What to change, and what to leave alone

Compare UDIDs with a case-folded key, keep the record the tool returned, and use that record's string for every call that follows. Do not lower-case on input and do not upper-case on input. The one place a rewrite is correct is the one WDA 16.12.9 chose: immediately before a call to a tool, replace the string with the spelling that tool itself listed, and fall back to the original if the lookup fails.

For a harness with one phone and one Mac, the cost of leaving it is a confusing error message. For a farm, where UDIDs arrive from scripts, CI variables, and people, the cost is phantom duplicates: our farm.py would allocate a second slot, with its own port pair off 8100, for a phone it already holds. That is the case worth fixing first.

Check it yourself

# your phone's UDID as each tool reports it
idevice_id -l
xcrun devicectl list devices --json-output /tmp/dc.json && grep -o '"udid" : "[^"]*"' /tmp/dc.json
pymobiledevice3 usbmux list | grep Identifier

# the same string, lower-cased, against each tool
U=$(idevice_id -l | head -1); L=$(echo $U | tr A-F a-f)
ideviceinfo -u $L -k ProductVersion
pymobiledevice3 lockdown info --udid $L
xcrun devicectl device info details --device $L
xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner \
  -destination "id=$L" -showBuildSettings; echo "exit $?"

# Appium's changes
gh pr view 2977 -R appium/appium-xcuitest-driver
gh pr view 2981 -R appium/appium-xcuitest-driver --json files
gh pr view 1260 -R appium/WebDriverAgent

# phoneshell's exact comparisons
grep -nE "udid *(==|!=|in )|== *(want|prefer|resolved)|prefer in" phoneshell/*.py

What would prove this wrong

The claim is that Apple's device tools accept only the spelling they report, so a UDID must be matched case-insensitively and passed through unchanged. It breaks if any of the four tools starts accepting the other case: that would make normalising safe and the pass-through rule unnecessary. The cheapest place to see it first is xcodebuild. We will rerun the table above against Xcode 27.1 when it leaves beta, and against the iPhone 14 Pro on the same Mac, and publish the rows by 30 November 2026. If a single tool accepts the lower-case form, this note gets a correction at the top.

Sources

  1. Yash121l, fix: match explicit udid capability against connected devices case-insensitively, appium-xcuitest-driver PR #2977, fixes #2941, released as 12.12.5 on 18 September 2026.
  2. mykola-mokhnach, fix: match udid capabilities case-insensitively across remaining device/simulator lookups, PR #2981, released as 12.12.6 on 19 September 2026.
  3. mykola-mokhnach, fix: resolve real device udid to xcodebuild's own case before building '-destination id=', appium/WebDriverAgent PR #1260, released as v16.12.9 on 19 September 2026.
  4. npm registry, appium-xcuitest-driver and appium-webdriveragent publish times, read 21 September 2026.
  5. Blolabel measurement, 21 September 2026: xcodebuild (Xcode 26.6, 17F113), devicectl 518.33, pymobiledevice3 11.8.0, libimobiledevice ideviceinfo 1.4.0, against an iPhone 17 Pro Max (iPhone18,2) on iOS 26.6.2 (23G90) over USB.
  6. Blomega/phoneshell at commit cac38a6: phoneshell/device.py, bringup.py, farm.py, server.py. github.com/Blomega/phoneshell