ZoneClaw cuts persistent-memory attacks from 372 of 480 to 6; phoneshell's screen memory stores 16,815 on-screen labels as first seen, with no record of who wrote them
ZoneClaw's point is that an agent's memory is where untrusted content goes to become trusted. We counted what phoneshell's screen memory holds on the rig: 808 screens, 16,815 labels, no provenance. Only 178 can reach a prompt today, and one of them carries a Wi-Fi network name, text anyone in radio range can choose.
ZoneClaw, posted on 30 September 2026 by researchers at the National University of Singapore, shows that text a computer-use agent remembers acquires authority over later tasks: 372 of 480 persistent-memory attacks succeeded against an undefended OpenClaw-style agent, and separating memory into trust zones cut that to 6. phoneshell's screen memory on our rig holds 16,815 control labels from 808 screens, stored as first seen with no record of whether Apple, an app, an ad network or a stranger's notification wrote them.
What ZoneClaw found
Haokai Ma, Chieh Lin, Yupeng Qiu and Ee-Chien Chang start from a probe on 30 Workspace-Bench tasks. A writing convention seeded only into the agent's memory file was adopted in 14 of 30 tasks, with task quality barely moved (43.5% rubric pass against 46.4%). A user instruction overriding it brought adoption to 0 of 30. Remembering, by itself, confers authority. The attacks then run across four scenarios (BCC, Chat Mirror, Source Redirection, Marketplace), two injection settings (user-triggered and scheduled) and four backbones. The chain is "malicious context, memory injection, malicious execution": the attacker never touches the later task.
The defense does not filter what gets remembered. External claims still persist, in a low-trust zone, and acquire the power to guide action only by crossing an explicit authority boundary, where promotion is cross-checked against zones the attacker cannot write. Role-specific processes enforce it so that no process both ingests external content and acts outward. On Claude Sonnet 4.6, attack success was 0 of 15 in all eight scenario and setting combinations. The authors also report that each of four existing defenses works in some settings and fails in others.
phoneshell's memory, counted
phoneshell/memory.py exists to skip work. Its docstring prices a step at 159 ms for a tree read, 72 ms for a screenshot, 504 ms for the stability gate and 1 to 3 s for the model, so the gain is "not asking the model at all". For each screen it stores a layout fingerprint, the controls, which control led to which screen, and how long the screen took to settle. On the next visit the agent gets a MEMORY: block, and with skip_image_on_known: true in the rig's config the screenshot can be skipped. We counted the rig's store on 4 October 2026.
| Measure | Value | Source |
|---|---|---|
| App files | 28 (Apple apps plus Grab, Gmail, YouTube, Facebook, X, others) | runtime/memory/*.json |
| Screens remembered | 808; 348 seen at least twice (trusted at min_visits_to_trust: 2) | runtime/memory, runtime/config.yaml |
| Controls stored | 16,815; 11,719 with an accessibility identifier | runtime/memory |
| Eligible for a hint | 178 (tapped at least once and led somewhere), 69 of them in Settings; 91 keyed by identifier | memory.py hint(); runtime/memory |
| Identifier keys with more than one stored label across screens | 89 of 1,248 | runtime/memory |
| Largest case | one SpringBoard notification key, 19 different notification texts (messages, news pushes) | runtime/memory |
| Ad labels stored | 17 ("Advertisement, ..." App Store slots and similar); 0 tapped | runtime/memory |
| Agent-written names and notes | 0 named screens, 0 notes | runtime/memory |
| Provenance or trust field | none in the schema | memory.py ControlMemory, ScreenMemory |
How a label from the screen becomes a line under MEMORY
Three details in memory.py make the chain. see() creates a control the first time it appears, with label=(e.label or e.name or e.text)[:60], and on later visits updates only its position. control_key() hashes the element type with the normalised identifier when there is one, and uses the label only when there is none, so a control keeps its key while its text changes. And hint() prints the stored label, not the live one: [{live id}] 'stored label' -> where (Nx), under the heading "what these controls did last time".
The live case on the rig is mild and instructive. The Wi-Fi row on the Settings root screen (identifier com.apple.settings.wifi) is hint-eligible on two remembered screens seen 42 and 59 times, and each stores the row's label as it read on first sight: "Wi-Fi, " followed by the network name the phone was on. The network name is text chosen by whoever runs the access point, and any nearby access point can broadcast one. Today it can reach the prompt only as the label of a control the agent tapped once, under a 60-character cap. That is a small channel, but it is the ZoneClaw shape exactly: environment-written text, persisted without a source, re-presented later as the agent's own experience. The notification and ad labels sit in the same store, one tap away from eligibility.
The store also raises a plainer problem. The 19 notification texts under one SpringBoard key are messages and news pushes from other people, kept on disk in runtime/memory indefinitely. We do not quote them here. A memory built to fingerprint layouts does not need them; the fingerprint already normalises volatile text, but the stored label does not.
What to change, and what it costs
- Refresh or drop the stored label. Printing the live element's text in
hint()removes stale text at no cost, since the live tree is already in hand when the hint is built. - Record provenance per control: app chrome (an identifier in the app's own namespace, a structural type) versus content (cells, notification rows, ad slots, values after a comma). Only chrome labels should appear in a hint. That is ZoneClaw's zone split at the scale of one field.
- Never store SpringBoard notification rows or anything matching "Advertisement". They do not help fingerprint a layout and they are the most third-party text the phone shows.
- The cost is a weaker hint on screens whose useful controls are content (a merchant list in Grab). The 178 eligible controls today are mostly chrome, 69 of them in Settings, so the loss is small now and grows as replay is built.
Check it yourself
- The code path:
grep -n "def see" -A25 phoneshell/memory.py(label set once),grep -n "def control_key" -A4 phoneshell/memory.py,grep -n "def hint" -A30 phoneshell/memory.py(printscontrol.label). - Your own store, if you run phoneshell:
python3 -c "import json,glob;c=[x for f in glob.glob('runtime/memory/*.json') for s in json.load(open(f))['screens'].values() for x in s['controls'].values()];print(len(c),sum(1 for x in c if x['taps']>=1 and x['led_to']))"prints stored and hint-eligible controls. - Ad labels: add
sum(1 for x in c if x['label'].startswith('Advertisement'))to the same one-liner. - ZoneClaw's numbers: abstract, Table 1 and Table 2 of
https://arxiv.org/abs/2610.00450; code athttps://github.com/euph00/ZoneClaw-code.
What would prove this wrong
Our claim is that a label stored from a third party can reach phoneshell's prompt unchanged after it has changed on screen. Test: rename the phone's Wi-Fi network, open Settings twice, and read the observation notes. If the MEMORY block shows the new network name rather than the stored one, our reading of hint() is wrong. We expect it to show the old name on phoneshell as of commit eed3fb1, and we expect phoneshell to either print live labels or add a provenance field by 31 March 2027; if neither has happened, this note's recommendation was not taken.
Sources
- Ma, Lin, Qiu, Chang, ZoneClaw: Mitigating Persistent Memory Attacks by Establishing Memory-Zoning in OpenClaw-Style Computer-Use Agents, arXiv:2610.00450, 30 September 2026
- ZoneClaw code release
- Blolabel, earlier note on iOS identifiers that carry instance data
- phoneshell source: phoneshell/memory.py, phoneshell/config.py (read 4 October 2026); counts from the rig's local runtime/memory store and runtime/config.yaml