Pure-Go binding to the macOS Accessibility (AX) API.
Navigate and manipulate the system-wide UI tree — inspect running applications, find buttons by semantic identity, read text field contents, click elements without hardcoded pixel coordinates.
Single binary, zero cgo, go install-able. Built on AXUIElement
via purego + an embedded companion dylib — the same pattern as
sckit-go,
kinrec, and
input-go.
kinax-go is part of KinKit — the pure-Go macOS system library
family powering the LocalKin agent swarm. It
pairs with input-go: kinax-go sees the UI, input-go moves
it.
go install github.com/LocalKinAI/kinax-go/cmd/kinax@latest
kinax tree --bundle com.apple.Safari --depth 4
kinax find AXButton --bundle com.apple.Safari
kinax click "Continue"
kinax at-point 200 100- Element tree navigation —
Children,Parent,Windows,FocusedWindow,FocusedElement,AttributeElement. - Typed attribute readers —
Attribute(string),AttributeInt,AttributeBool,AttributePoint(CGPoint),AttributeSize(CGSize),AttributeElements(array). - Attribute + action introspection —
AttributeNames,ActionNamesreturn live lists from the element. - Semantic search —
FindFirst/FindAllwith composable matchers (MatchRole,MatchTitle,MatchTitleContains,MatchIdentifier,MatchAll,MatchAny). - Hit testing —
ElementAtPoint(x, y)returns whatever AX element is at the given global coords (what Accessibility Inspector shows when you hover). - App targeting —
FocusedApplication,ApplicationByPID,ApplicationByBundleID,SystemWide. - Action + write —
Perform("AXPress"),SetString,SetBool. - Menu navigation (v0.4) —
Element.NavigateMenu("File > New Window")walks the AX menu tree and presses the leaf;MenuItemShortcut()reads the keyboard equivalent (e.g.⌘⇧N) without firing the action. - Push-based event subscriptions (v0.3) —
Observer+SubscribedeliverAXFocusedUIElementChanged/AXValueChanged/ etc. via a Go channel; CFRunLoop is internalized. - Batch attribute fetch (v0.2) —
Element.GetMany(names...)issues a singleAXUIElementCopyMultipleAttributeValuescall instead of N round-trips. - No cgo: downstream projects stay pure Go. The ObjC companion
dylib is
//go:embedded (~70 KB universal arm64+x86_64) and extracted to~/Library/Cacheson first call.
# CLI
go install github.com/LocalKinAI/kinax-go/cmd/kinax@latest
# Library
go get github.com/LocalKinAI/kinax-goRequires macOS 12+ and Go 1.22+.
macOS requires the invoking binary to be listed in
System Settings → Privacy & Security → Accessibility for AX*
calls to succeed. Unlike input-go (which silently no-ops without
permission), kinax-go returns real errors — every accessor will
fail until permission is granted.
if err := kinax.RequireTrust(); err != nil {
kinax.PromptTrust() // shows system dialog
log.Fatal("grant Accessibility permission, then rerun")
}package main
import (
"fmt"
"log"
"github.com/LocalKinAI/kinax-go"
)
func main() {
if err := kinax.Load(); err != nil {
log.Fatal(err)
}
if err := kinax.RequireTrust(); err != nil {
log.Fatal(err)
}
// Attach to Safari (must be running)
app, err := kinax.ApplicationByBundleID("com.apple.Safari")
if err != nil { log.Fatal(err) }
defer app.Close()
// Walk all windows
wins, _ := app.Windows()
for _, w := range wins {
t, _ := w.Title()
fmt.Println("window:", t)
w.Close()
}
// Find every text field, print its value
fields := app.FindAll(kinax.MatchRole(kinax.RoleTextField), 30)
for _, f := range fields {
v, _ := f.Value()
fmt.Println("field:", v)
f.Close()
}
// Click the "New Tab" button
if btn, ok := app.FindFirst(kinax.MatchTitle("New Tab"), 20); ok {
defer btn.Close()
btn.Perform(kinax.ActionPress)
}
}# Dump the AX tree of the focused app (default target)
kinax tree --depth 4
# Dump a specific app's tree
kinax tree --bundle com.apple.Safari --depth 5
kinax tree --pid 1234
# Inspect one attribute of the app element
kinax attr AXTitle --bundle com.apple.Safari
# List every attribute or action the app exposes
kinax attrs --focused
kinax actions --focused
# Find every button (optionally with a specific title)
kinax find AXButton --bundle com.apple.Safari
kinax find AXButton "New Tab" --bundle com.apple.Safari --depth 25
# Click a button by title
kinax click "Continue"
kinax click "OK" --role AXButton --bundle com.apple.SystemPreferences
# Hit-test a screen coordinate
kinax at-point 200 100
# → AXMenuBar Apple
# Permission
kinax trust # prints 1 or 0
kinax trust --prompt # shows system dialogEvery *Element returned by kinax-go wraps a retained
CFTypeRef. The caller must call (*Element).Close — forgetting
to leaks a handle for the process lifetime.
Traversal helpers (FindFirst, FindAll) return fresh handles; any
siblings they walk past are closed automatically. You only need to
close what's returned to you.
// Correct
wins, _ := app.Windows()
for _, w := range wins {
defer w.Close()
}
// Also correct — find returns a fresh handle
if btn, ok := app.FindFirst(kinax.MatchTitle("OK"), 20); ok {
defer btn.Close()
btn.Perform(kinax.ActionPress)
}kinax finds the element; input drives the cursor there. This is
the natural UI-automation loop:
import (
"github.com/LocalKinAI/kinax-go"
"github.com/LocalKinAI/input-go"
)
btn, _ := app.FindFirst(kinax.MatchTitle("Save"), 20)
defer btn.Close()
pos, _ := btn.Position()
size, _ := btn.Size()
cx := float64(pos.X + size.X/2)
cy := float64(pos.Y + size.Y/2)
input.MoveSmooth(ctx, cx, cy, 300*time.Millisecond)
input.ClickAt(ctx, cx, cy)For many cases btn.Perform(kinax.ActionPress) is enough — clicking
via AX is faster and doesn't require Accessibility permission on the
binary that runs input.Click (though it does require it on the one
that runs kinax.Perform). Use input-go for pixel-level drag
gestures, hover highlights, and scrolling.
kinax-go follows the embedded dylib pattern documented in
Paper #9 of localkin.dev/papers.
Go code ─── purego.Dlopen ────► libkinax_sync.dylib (embedded)
│
└──► AXUIElement* APIs
objc/kinax_ax.m— ~450 LOC ObjC shim exposing 20 C-ABI functions (kinax_element_attr_string,kinax_element_perform, etc.).internal/dylib/libkinax_sync.dylib— universal Mach-O, committed.- Opaque
uintptrhandles for elements;CFRetainon the ObjC side,CFReleaseviakinax_element_releasewhen Go Closes. - JSON encoding for list attributes (attribute names, action names) — avoids shipping a full CF→Go type system across the FFI.
- macOS only. The Accessibility API is macOS-specific — no cross-platform ambitions.
- Numeric attributes only as int64. Float-valued attributes
(
AXValueon sliders) currently string-stringify.AttributeFloatplanned for v0.5. - CGPoint/CGSize setters not exposed yet. Window-move via
SetPosition/SetSizeplanned for v0.5. - Single main thread assumption for some CF calls. In practice
kinaxworks from any goroutine because we don't use CFRunLoop except inside the [Observer] subsystem. - Tested only on macOS 26.3 arm64 so far; Intel + macOS 14/15 verification pending CI.
- v0.2 —
Element.GetManybatch attribute fetch viaAXUIElementCopyMultipleAttributeValues. Shipped 2026-04-19. - v0.3 — Push-based AX event subscriptions
(
Observer+Subscribe+Next/Eventschannel). Shipped 2026-04-29. - v0.4 —
Element.NavigateMenu(path)+MenuItemShortcut()- menu/action constants. Shipped 2026-05-08.
- v0.5 (planned) — typed numeric attributes (
AttributeFloat), geometry setters (SetPosition/SetSize),WaitForWindowhigh-level convenience. - v0.6 (planned) —
FindMenuItem(path)(split off fromNavigateMenuso callers can introspect the leaf without firing the action; needed by kinclaw'sui shortcutreading path),Element.Snapshot()+Snapshot.Diff()for state-change verification.
git clone https://github.com/LocalKinAI/kinax-go
cd kinax-go
make dylib # rebuild universal Mach-O after ObjC changes
make test # unit tests (no Accessibility permission needed)
make test-integration # requires Accessibility permission
make lint # go vet + staticcheck + golangci-lintMIT. See LICENSE.
sckit-go— ScreenCaptureKit (screen pixels).kinrec— screen + audio recorder.input-go— mouse + keyboard synthesis.- Embedded Dylib paper — the architectural pattern.