Blolabel
← Notes
analysis 8 min read

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.

Abstract technical illustration of identical keys cut from the same blank, each stamped with a different serial number, so a lock that reads the serial rejects every one of them

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.

"PART 3: XCUITest Automation Tutorial, Adding Accessibility Identifiers", @QAUnderground, 6 January 2021. Supports the standard practice this note qualifies: identifiers are the preferred locator because a developer assigns them deliberately, which is exactly what does not happen in the system Settings rows above.

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.

patternreplaced withreal examplewhy it changessource
[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}#uuid61932825-AFFA-4F26-... in a Settings follow-up rowidentifies one account recordphoneshell/memory.py
0x[0-9a-f]{4,}#ptr0x85a8cf0f0 on a connected headphone rowa heap address, new on every launchphoneshell/memory.py
(?:[0-9a-f]{2}:){5}[0-9a-f]{2}#mac38:C4:3A:26:96:EEidentifies one paired devicephoneshell/memory.py
\b\d{4,}\b#nrow indices, ids, timestampsordering and content, not structurephoneshell/memory.py
\b[0-9a-f]{12,}\b#hexbare hashes and tokenssame as above, without the 0xphoneshell/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 same row, hashed two ways raw identifier, as WebDriverAgent reports it com.apple.settings.connectedHeadphone.38:C4:3A:26:96:EE-0x85a8cf0f0 hash the raw string launch 1: a3f0c41d8b2e5577 launch 2: 71bd9e04cc136a2f every revisit is a new screen, so nothing is ever learned strip instance data, then hash ...connectedHeadphone.#mac-#ptr launch 1 and 2: 5c2e7a1f90b4d833 the screen has an identity, and the controls on it can accumulate history basis for the hash, per element type | normalised identifier or label | round(x/32), round(y/32) sorted, joined, sha1, first 16 chars The hash values above are illustrative of the two behaviours. The normalisation patterns, the 32 point grid and the 16 character truncation are the real ones, in phoneshell/memory.py.
Screen memory is worth building only if the key is stable. On iOS the key has to be repaired before it is used, because the system's own apps put instance data in it.

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.

False alarms from checking the pixels against the label Measured on three real screens, 46 elements in the scoped set 39% every element 0% six types whose label is the rendering 0 30% 55% of elements flagged wrongly A gate that fires on 39% of elements is not a gate. Scope decides whether the check is usable at all.
The check did not get smarter. It got narrower, and narrowness is what took it from noise to a usable safety property.

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

Sources

  1. phoneshell, phoneshell/memory.py: the identifier noise patterns and the fingerprint basis. https://github.com/Blomega/phoneshell/blob/main/phoneshell/memory.py
  2. 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
  3. 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
  4. 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
  5. Apple, UIAccessibilityIdentification accessibilityIdentifier, the documented purpose of the identifier. https://developer.apple.com/documentation/uikit/uiaccessibilityidentification/accessibilityidentifier
  6. QA Underground, PART 3: XCUITest Automation Tutorial, Adding Accessibility Identifiers, 6 January 2021. https://www.youtube.com/watch?v=JPMSIglp3OE