Blolabel
← Notes
registry 8 min read

Fourteen overlay shapes, and the ordered moves that close each one

A sheet is recognised by starting part-way down the screen, not by being large, which is why the iOS share sheet at y=478 was invisible to a size-based detector. The reflex lives in the harness because instructing a model to ignore popups has been measured and does not work.

Almost everything an app puts in front of a phone agent is one of fourteen shapes, and each shape has a known way out. This is the catalogue our harness works through, published as data: what each overlay looks like in the accessibility tree, the ordered moves that close it, and the two things that must never be tapped. The reason it lives in the harness rather than in the prompt is that telling a model to ignore popups does not work, and there is a measurement for that.

Abstract technical illustration of fourteen translucent panels stacked at different depths and offsets in front of a single surface, each with a distinct exit path traced through it

The catalogue

The ordering inside each row matters: it runs from the move that is most certain and least destructive to the one most likely to have a side effect. Tapping a labelled "Not now" beats guessing at a backdrop, which beats a back swipe that might also leave the screen you wanted.

#shapehow it shows up in the treefirst movenever
1bottom sheet with a grabbera container starting part-way down and reaching the bottom, with a small wide element roughly 30 to 70 pt by 3 to 8 pt centred at its topdrag down from the grabber, about 60% of screen height
2full-screen modala container covering more than 85% of the screen that was not there a moment agotap the X: xmark, xmark.circle.fill, close, btn_close, ic_close
3interstitial adverta full-screen WebView or Image with no useful labelswait 2 to 4 seconds for the close control to exist, then look again
4action sheettype Sheet, or a bottom-anchored group whose last button is Canceltap Cancel
5system alerttype Alert, or GET /alert/text returns somethingread it and decidenever auto-accept a permission, a payment, or anything agreeing to terms
6rating or review promptan Alert containing rate, review, enjoying or feedbacktap Not Now
7onboarding tour or coach marka small overlay with a highlighted cut-out and the rest dimmedtap Skip
8paywall or upsella full-screen modal containing prices, "per month", "free trial", "upgrade"tap the X, which may take a second or two to appearnever tap Subscribe or Start free trial
9cookie or consent bannera WebView containing cookie, consent, privacy or GDPRtap Reject all or Only necessarydo not agree to terms on the user's behalf
10update nagan Alert or sheet containing update, version or what's newtap Laterdo not tap Update: it leaves the app for the App Store
11notification bannera transient element near the top, above the app's own contentswipe it up, or wait 4 seconds
12toast or snackbara small element near the bottom with no buttonswait 2 to 3 seconds, it leaves on its ownacting on it usually means tapping through it
13keyboard covering the contentKey elements present, or the bottom third is a keyboardscroll the target above the keyboard
14popover with an arrowtype Popover, or a small panel with the rest dimmedtap anywhere outside it

Source: phoneshell/agent/playbook.py, which is the file the harness reads and also the text handed to the model when the reflex fails. Fourteen entries, each with a tell, an ordered moves list, and an optional never.

A sheet is recognised by where it starts, not by how big it is

The detector that looks for the largest container picks the app's own full-screen window every time. That is how the iOS share sheet stayed invisible to ours: it is a Popover whose top edge sits at y=478, well inside a window that starts at zero and is larger than it. The rule that works is geometric and boring. A sheet starts part-way down the screen and reaches the bottom. Size is not the signal, offset is.

Largest container, versus the one that starts part-way down the screen app window y = 0, full height share sheet type Popover y = 478 pick the largest container selects the app window the sheet is never detected, and the agent reads the screen as "stuck" pick by vertical offset starts part-way down, reaches the bottom finds the Popover at y = 478 two conditions on the search run it on the RAW tree: the element filter drops unlabelled containers as scaffolding, and a sheet is one the grabber bar is decorative and usually absent from the tree, so aim the drag at the sheet's top edge plus a few points, where it physically sits tapping the grabber does nothing at all: iOS sheets dismiss on a drag phoneshell FINDINGS.md sections 4 and 5. Measured on a physical iPhone 17 Pro Max running iOS 26.6.
The share sheet is the case that makes the rule obvious: a small overlay inside a large window, which every size-based heuristic ranks last.

Three related gesture facts fall out of the same geometry, and each was a real bug. A drag through the middle of the screen pans the map underneath instead of scrolling the results sheet on top of it, which reads to an agent as "scrolling does nothing", so the drag has to start inside the container that actually scrolls, found in the raw tree. A page flip needs momentum, because a drag that ends stationary snaps back to the page it started on. And a top-edge gesture must start at exactly y=0: from there the top-left swipe opens Notification Centre and the top-right opens Control Centre, repeatably. Whether y=1 also works is untested.

Why the reflex sits in code rather than in the prompt

One alternative is to describe the popups in the system prompt and let the model deal with them. That has been measured, and it does not work. "Attacking Vision-Language Computer Agents via Pop-ups" (arXiv:2411.02391) puts adversarial popups into OSWorld and VisualWebArena and reports an 86% attack success rate, meaning the frequency with which agents click the popup, along with a 47% drop in task success. The defenses tested are the ones you would reach for first: instructing the agent to ignore popups in the system prompt, and adding an advertisement notice. Neither reduced the attack success rate by more than 25%.

That is an adversarial setting, and ordinary commercial popups are not adversarial. The relevant part transfers anyway: a model asked to reason about an overlay in the same context where it is reasoning about the task will attend to the overlay, and the overlay is designed by someone whose job is to capture attention. A deterministic dismissal in the harness does not have attention to capture.

"Appium Tutorial 24: Handling Alerts/Popups in iOS App", @sdetpavan, 31 May 2019. Supports the claim that the established mobile automation answer to an overlay is a driver-level routine, the alert API or an auto-accept capability, rather than instructions to the thing being tested. Row 5 of the table above is where we depart from it: auto-accepting a permission or a payment is not ours to do.

The cost argument is the same shape. An agent that has to reason about a promo sheet burns its budget narrating it, and turns are the expensive unit on a phone, not milliseconds. Fourteen ordered lists of moves cost nothing per step and remove that reasoning entirely.

Where the decision is made observe screen match the 14 tells type, geometry, keywords apply moves in order certain first, destructive last model sees a clean screen never-tap gate, applied to every move Subscribe, Start free trial, Update, any permission, any terms no shape matches hand the whole cheatsheet to the model as text why not simply tell the model to ignore popups Adversarial popups in OSWorld and VisualWebArena: 86% attack success rate, task success down 47%. Prompt-level defenses, including "ignore pop-ups" and an advertisement notice, cut that by no more than 25%. Zhang et al., arXiv:2411.02391. Catalogue: phoneshell/agent/playbook.py, 14 entries.
The gate is the part that cannot be delegated. Everything above it is a lookup; the never-tap list is a policy about whose money and whose consent are in play.

What to take from this if you are building one

Write your own version of this table before you write dismissal logic, because the table is the part that survives. Our moves reference our gesture layer, but the tells are properties of iOS: a Popover is a Popover, an action sheet ends in Cancel, a paywall contains the word "month".

Keep the never-tap list separate from the moves. It is the only part with a consequence that cannot be undone by observing again, and it is the part a model should not be allowed to override with reasoning, however good the reasoning sounds.

Last, make the stability gate tolerate motion rather than demand stillness, or overlays will cost you far more than they should. Waiting for two byte-identical frames never converges on a screen with a countdown timer: a promotional sheet reading "40% off flash deals 14:19" made every observation pay the full timeout and turned a 2 second step into a 50 second one. The gate now settles when under 1.2% of pixels differ, because a clock moves well under 1% of the screen and a real transition moves tens of percent.

Check it yourself

The catalogue is one Python file with no dependencies, so it can be read, printed and diffed without a phone attached.

git clone https://github.com/Blomega/phoneshell && cd phoneshell

# the catalogue as the model receives it
python3 -c "from phoneshell.agent import playbook; print(playbook.as_text())"

# the fourteen entries, their tells and their never-tap rules
grep -n "kind=" phoneshell/agent/playbook.py
grep -n "never=" phoneshell/agent/playbook.py

# reproduce the share sheet case: open Safari, tap share, then dump the raw tree
bin/phoneshell up
curl -s "localhost:8100/source?format=json" | python3 -c "
import json,sys
def walk(n,d=0):
    r=n.get('rect') or {}
    if n.get('type')in('Popover','Sheet','Other') and (r.get('y') or 0)>100:
        print(n.get('type'), 'y=',r.get('y'), 'h=',r.get('height'), n.get('label') or '')
    for c in n.get('children') or []: walk(c,d+1)
walk(json.load(sys.stdin)['value'])"

The last command prints the candidate overlays by vertical offset. On a screen with the share sheet open, the row you want is the one that starts well below zero and runs to the bottom, not the tallest one.

What would prove this wrong

Sources

  1. phoneshell, phoneshell/agent/playbook.py, the fourteen catalogued overlay shapes with tells, ordered moves and never-tap rules. https://github.com/Blomega/phoneshell/blob/main/phoneshell/agent/playbook.py
  2. phoneshell, FINDINGS.md sections 3, 4 and 5, measured on a physical iPhone 17 Pro Max running iOS 26.6. https://github.com/Blomega/phoneshell/blob/main/FINDINGS.md
  3. Zhang, Yu and Yang, Attacking Vision-Language Computer Agents via Pop-ups, arXiv:2411.02391. https://arxiv.org/abs/2411.02391
  4. SALT-NLP, PopupAttack, the code for the paper above. https://github.com/SALT-NLP/PopupAttack
  5. SDET-QA, Appium Tutorial 24: Handling Alerts/Popups in iOS App, 31 May 2019. https://www.youtube.com/watch?v=AF1nOPvH9f4