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.
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 UDID | command | upper case | lower case | source |
|---|---|---|---|---|---|
| xcodebuild (Xcode 26.6, 17F113) | id:00008150-…401C in -showdestinations, upper | -destination id=... -showBuildSettings | exit 0 | exit 64, "Unable to find a device matching the provided destination specifier", then lists the phone in upper case | our run, 21 Sep 2026 |
| devicectl (518.33) | hardwareProperties.udid, upper | device info details --device ... | osVersionNumber 26.6.2 | CoreDeviceError 1000, "The specified device was not found" | our run, 21 Sep 2026 |
| pymobiledevice3 (11.8.0) | usbmux Identifier, upper | lockdown 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, upper | ideviceinfo -u ... -k ProductVersion | 26.6.2 | "Device ... not found!" | our run, 21 Sep 2026 |
phoneshell device_check (commit cac38a6) | whatever usbmux or devicectl says | device_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.6 | varies by tool | device discovery, usbmux, simulator, session claim | match | match, value kept as given | PR #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.
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:
| file | lines | what the comparison decides | what a case mismatch would do |
|---|---|---|---|
phoneshell/device.py | 255, 258 | is the configured phone on usbmux, or live in devicectl | "not connected", then lists the same phone as present (measured) |
phoneshell/bringup.py | 82, 91 | which scanned phone is the configured one | no phone marked configured, sort order changes |
phoneshell/bringup.py | 86 | dedupe the usbmux list against the devicectl list | one phone listed twice, once per tool |
phoneshell/bringup.py | 212 | is the configured phone among the live ones | "A different iPhone is plugged in", about the same iPhone |
phoneshell/bringup.py | 474 | is the resolved phone on the cable | takes the wifi path with the phone on USB |
phoneshell/bringup.py | 482 | does the config need re-pinning | rewrites the config to the tool's spelling, which is the harmless one |
phoneshell/farm.py | 125, 132, 189 | primary phone, slot lookup, configured flag | allocates 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.py | 612 | which farm member a connect request meant | the request reports failure and returns no member record |
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
- 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.
- mykola-mokhnach, fix: match udid capabilities case-insensitively across remaining device/simulator lookups, PR #2981, released as 12.12.6 on 19 September 2026.
- 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.
- npm registry, appium-xcuitest-driver and appium-webdriveragent publish times, read 21 September 2026.
- 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.
- Blomega/phoneshell at commit cac38a6:
phoneshell/device.py,bringup.py,farm.py,server.py. github.com/Blomega/phoneshell