Understanding the ref system that makes element interactions possible
Refs are how you reference UI elements across commands. A snapshot assigns refs like @n1, @n2, @n3 to elements in the Accessibility tree. You then use these refs in subsequent commands to click, type, or inspect specific elements.
A ref (reference) is a short identifier like @n1, @n2, etc. that points to a specific UI element. Think of refs as temporary “handles” for elements discovered during a snapshot.
Refs are persisted to disk at /tmp/agent-native-refs.json so they survive between CLI invocations. Each command runs as a separate process, so refs must be saved externally.From RefStore.swift:4-35:
struct RefEntry: Codable { let ref: String // "n1", "n2", etc. let pid: Int32 // Process ID of the app let role: String // AXButton, AXTextField, etc. let title: String? // Button text, window title let label: String? // Accessibility label let identifier: String? // Programmatic identifier let pathHint: String // XPath-like path in the tree}private static let storePath: URL = { let tmp = FileManager.default.temporaryDirectory return tmp.appendingPathComponent("agent-native-refs.json")}()static func save(_ entries: [RefEntry]) { ... }static func load() -> [RefEntry] { ... }
The RefStore contains only the metadata needed to re-find elements, not live AXUIElement pointers (which can’t be serialized).
When you use a ref like @n5, agent-native must resolve it back to a live AXUIElement. This happens by re-searching the tree using the stored attributes.From RefStore.swift:37-74:
static func resolve(ref: String) throws -> (element: AXUIElement, node: AXNode) { let entries = load() guard let entry = entries.first(where: { $0.ref == cleanRef }) else { throw AXError.elementNotFound("Unknown ref: @\(cleanRef). Run `snapshot` first.") } let appElement = AXEngine.appElement(pid: entry.pid) let results = AXEngine.findElements( root: appElement, role: entry.role, title: entry.title, label: entry.label, identifier: entry.identifier, maxDepth: 15 ) // Return first exact match if let match = results.first(where: { /* exact attribute match */ }) { return match } throw AXError.elementNotFound( "Could not re-resolve @\(cleanRef). The UI may have changed -- run `snapshot` again.")}
1
Load ref metadata
Read the entry from agent-native-refs.json.
2
Search the live tree
Use AXEngine.findElements() to search for elements matching the stored role, title, label, and identifier.
3
Match exactly
Return the element that exactly matches all stored attributes.
4
Error if not found
If the element no longer exists or has changed, throw elementNotFound.
The UI changes - If a button is removed or its text changes, the ref can’t be resolved
The app restarts - PIDs change, invalidating all refs for that app
A new snapshot is taken - Each snapshot command overwrites the entire RefStore
Refs are ephemeral. They’re valid for the current UI state only. If your interaction changes the UI significantly (e.g., navigating to a new screen), take a fresh snapshot.