iOS Settings ships accessibility identifiers containing a MAC address and a raw pointer
The identifier is the most resilient locator iOS offers, right up to the point where the system's own apps put instance data inside one. Five normalisation patterns before hashing is the difference between an agent that remembers a screen and one that meets it for the first time on every launch.
iOS Settings ships accessibility identifiers with instance data inside them. Two real
examples from a physical iPhone 17 Pro Max on iOS 26.6:
com.apple.settings.followUpGroupAsItem...account.61932825-AFFA-4F26-... carries a UUID,
and com.apple.settings.connectedHeadphone.38:C4:3A:26:96:EE-0x85a8cf0f0 carries a MAC
address and a raw pointer. Both are stable within one app launch and different on the next. Any
screen fingerprint hashed from raw identifiers therefore treats every revisit as a screen it has
never seen, and an agent's memory of the phone never accumulates.
Why the best locator on iOS is also the least stable one
The received advice is correct as far as it goes: the accessibility identifier is the most resilient locator XCUITest offers, because it does not move when the label is translated or the layout changes. Apple's own documentation treats it as the stable handle. Every real-device testing guide says to use it in preference to visible text.
That advice assumes the identifier was written by a person naming a control. The two above were
written by code naming an instance: this particular linked account, this particular paired
headphone. The pointer is the clearest tell. 0x85a8cf0f0 is a heap address, and a heap
address survives exactly as long as the process that allocated it.
What gets stripped before hashing, and why
A fingerprint is meant to answer one question: have I been on this screen before? It has to be insensitive to content (a restaurant list with different restaurants is the same screen) and sensitive to structure (the list and the checkout sheet are not). Identifiers are the strongest structural signal available, so they are kept, with the volatile pieces replaced by tokens rather than dropped.
| pattern | replaced with | real example | why it changes | source |
|---|---|---|---|---|
[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} | #uuid | 61932825-AFFA-4F26-... in a Settings follow-up row | identifies one account record | phoneshell/memory.py |
0x[0-9a-f]{4,} | #ptr | 0x85a8cf0f0 on a connected headphone row | a heap address, new on every launch | phoneshell/memory.py |
(?:[0-9a-f]{2}:){5}[0-9a-f]{2} | #mac | 38:C4:3A:26:96:EE | identifies one paired device | phoneshell/memory.py |
\b\d{4,}\b | #n | row indices, ids, timestamps | ordering and content, not structure | phoneshell/memory.py |
\b[0-9a-f]{12,}\b | #hex | bare hashes and tokens | same as above, without the 0x | phoneshell/memory.py |
| numbers with units, clock times, relative dates | # | 14:19, 2.4 km, 40%, "just now" | content that changes while the screen does not | _VOLATILE, same file |
The fingerprint is then the bundle id, plus one line per structural element giving its type, its normalised identifier (falling back to a normalised label), and its position rounded to a 32 point grid, sorted and hashed to 16 hex characters. The coarse grid is deliberate: a row that shifts by a few points because a banner appeared above it is still the same row.
The second half: a label is not what is drawn
The same assumption fails in the other direction at action time. A model that reads "Confirm booking" in a serialised tree will tap that element even when the pixels under it now say something else, because the structural claim is what it was given. "Do GUI Agents Believe Their Eyes?" (arXiv:2607.04334, 5 July 2026) measures this directly: on outdated snapshots, models follow the outdated structure on up to 0.88 of probes while image-only accuracy stays near ceiling, and a single mis-sourced belief at the first step has a self-recovery rate of at most 0.03.
So before any tap, our harness crops the element's rectangle out of the current frame and asks macOS Vision what those pixels actually read. The first version of that check ran on everything and was unusable: a 39% false-alarm rate, because icons, images, text fields, webviews and containers all carry labels describing something other than their own rendering. An app name printed under an icon, a caption rather than a value, a label for a whole page.
Scoping the check to the six element types whose label is the text drawn on them
(Button, StaticText, Link, MenuItem,
Tab, SegmentedControl) took that to zero false alarms over 46
elements, and it still catches a relabelled button, which is the case the gate exists for.
Matching then has to be strict about which words agree. Any-token overlap matched "Place order" against a Cancel button, because both contain "order": the shared word is the one that does not matter and the differing word is the one that does. The rule is 60% coverage of the claimed label's tokens, with two concessions that are stated rather than hidden: a label longer than four tokens passes on two solid hits, because the crop clips long labels, and a crop containing only glyph-sized text comes back inconclusive rather than failing, because a bare chevron has nothing to verify against.
What this changes if you build on the tree
Do not hash identifiers raw. If you keep any per-screen or per-control state on iOS, normalise UUIDs, MAC addresses, pointers, long digit runs and long hex runs out first, or your memory will look like it is working in a single session and silently reset itself between app launches, which is the worst failure shape available: no error, no warning, just an agent that never gets better at a screen it has seen forty times.
Treat the identifier as a name with unknown provenance rather than a key. It may be a developer's
deliberate login_email_input, or it may be a description of one row of live data. The
two are indistinguishable without looking at the string, which is why the normalisation is a list of
patterns rather than a policy.
And when you verify an element before acting on it, choose the types you verify. The 39% number is what a well-intentioned global check costs: it does not fail loudly, it just makes the gate useless, and a gate that is usually wrong gets turned off.
Check it yourself
Everything here is one accessibility tree dump and forty lines of Python. No agent required.
git clone https://github.com/Blomega/phoneshell && cd phoneshell
bin/phoneshell up
# open Settings on the phone, then dump the tree and look for instance data
curl -s localhost:8100/source?format=json > /tmp/tree.json
grep -oE '"name":"[^"]*(0x[0-9a-f]{4,}|([0-9A-F]{2}:){5}[0-9A-F]{2}|[0-9A-F]{8}-[0-9A-F]{4})[^"]*"' /tmp/tree.json | sort -u
# what the normaliser does to them
python3 - <<'PY'
from phoneshell.memory import _norm_id
for s in ["com.apple.settings.connectedHeadphone.38:C4:3A:26:96:EE-0x85a8cf0f0",
"com.apple.settings.followUpGroupAsItem.group.account.61932825-AFFA-4F26-9E30-000000000000"]:
print(_norm_id(s))
PY
# the scoped label check and the 60% coverage rule
grep -n "LABEL_IS_RENDERED" -A 6 phoneshell/agent/verify.py
grep -n "def label_matches" -A 35 phoneshell/perception/ocr.py
To reproduce the launch instability, dump the tree, force-quit Settings, reopen it, dump again, and diff the identifiers of the connected-device rows. The prefix is stable and the tail is not.
What would prove this wrong
- Dump Settings twice across a force-quit on any iOS 26 device and find the pointer suffix unchanged. That would mean the value is not an address, or is allocated deterministically, and our reading of it is wrong. We predict it changes, on iOS 26.x, through 30 June 2027.
- Run the unscoped pixel-versus-label check on your own screens and get a false-alarm rate well under 39%. Ours is three screens, which is a small sample; if the number is really 5% on a typical app, the scoping is over-cautious and the gate could cover more element types.
- The claim we are least sure of: that 60% token coverage is the right threshold rather than merely a working one. It was chosen to separate "Place order" from "Cancel order" and has not been swept. A measured precision and recall curve over a labelled set would beat our reasoning.
Sources
- phoneshell, phoneshell/memory.py: the identifier noise patterns and the fingerprint basis. https://github.com/Blomega/phoneshell/blob/main/phoneshell/memory.py
- phoneshell, phoneshell/agent/verify.py and phoneshell/perception/ocr.py: the scoped consistency gate and the 60% token coverage rule. https://github.com/Blomega/phoneshell/blob/main/phoneshell/agent/verify.py
- phoneshell, FINDINGS.md section 3, measured on a physical iPhone 17 Pro Max running iOS 26.6. https://github.com/Blomega/phoneshell/blob/main/FINDINGS.md
- Zhang et al., Do GUI Agents Believe Their Eyes? Diagnosing State-Belief Reliance on Pixels versus Structure, arXiv:2607.04334, 5 July 2026. https://arxiv.org/abs/2607.04334
- Apple, UIAccessibilityIdentification accessibilityIdentifier, the documented purpose of the identifier. https://developer.apple.com/documentation/uikit/uiaccessibilityidentification/accessibilityidentifier
- QA Underground, PART 3: XCUITest Automation Tutorial, Adding Accessibility Identifiers, 6 January 2021. https://www.youtube.com/watch?v=JPMSIglp3OE