Prism Inspector

Docs

How to use Prism Inspector, instrument an app with the PrismKit SDK, and drive both from an AI agent. Every screenshot here is the tool running against a real app.

How it fits together

There are three pieces, and only the first is required:

When the SDK is present it streams over a local socket on port 9294. When it is not, the app falls back to what the accessibility tree exposes. Everything else — measuring, guides, the design check, export — works either way.

Install

Download the disk image and drag Prism Inspector into Applications in the Finder.

Dragging in the Finder is not cosmetic advice. An app copied by any other means keeps the quarantine flag macOS puts on downloads, and macOS then runs it from a randomised read-only folder — App Translocation. The app works, but it can never update itself, and the only symptom is a message about running "from the location it was downloaded to".

On first launch macOS asks you to confirm, because the app came from the internet. The prompt also says Apple checked it and found no malware — that is notarization, and it is the expected path. An app that was not notarized says something different: that it is damaged and cannot be opened.

Requires macOS 13 or later. The app is a universal binary — Apple silicon and Intel — and so is the MCP server inside it, so the agent integration works on both. Free.

Connect to your app

Your app has to carry the SDK. The inspector renders what the app streams to it, so an app without PrismKit shows nothing at all — not an empty tree, but no screen either, because capture only runs while something is streaming. Two lines get you there.

Add the package, in Xcode through File ▸ Add Package Dependencies… or in a Package.swift:

.package(url: "https://github.com/zalazara/PrismKit-SDK.git", from: "0.2.0")

Then put one modifier at the root of your view hierarchy — above your NavigationStack or TabView, so every screen you push is covered by the same scope:

import PrismKit

var body: some Scene {
    WindowGroup {
        ContentView()
            .measureScope()
    }
}

That is the whole integration. Individual views do not need annotating: everything in the view tree comes from the accessibility tree, so a screen you never touched still reports its elements. Adding measure to specific views is worthwhile later, for naming components and for design checks that pair by identity, but nothing requires it.

It compiles out of release builds entirely — the collection, overlay and streaming code is not in the shipping binary — so it does not need wrapping in #if DEBUG.

Now boot a simulator, run the app from Xcode or with xcrun simctl launch, and open Prism Inspector. It finds the running app, mirrors its screen, and fills the view tree.

Order does not matter. Start either one first; the connection retries every few seconds, so an app launched before the inspector still appears. If the app is there but the view tree is empty, the simulator has not built its accessibility tree — see Troubleshooting.

The toolbar's breadcrumb shows which app and which device you are looking at, with the app's real icon. If several simulators are booted, pick one there.

Or a real iPhone

Plug the phone in and it appears in the same menu, by name, under Attached devices. Pick it and everything works as it does for a simulator: the tree, the inspector, live values, the design check.

Nothing to configure. On a device the app listens and the Mac reaches in over the cable through usbmuxd — the same mechanism Xcode has always used to talk to attached devices — so the app has to be running and built with the SDK, and that is all. Devices paired over Wi-Fi work too and are labelled as such, because they are slower and it is worth knowing which one you are on.

One difference worth knowing: the canvas shows the app's own render rather than a photograph of the device, so the status bar and Dynamic Island are not in the picture — the system draws those outside the app's window. The inspector says so when it happens.

What works if your app has UIKit screens

Prism Inspector is built for SwiftUI, but the view tree does not come from SwiftUI — it comes from the accessibility tree, walked from your app's key window. So in a mixed app, UIKit screens are measured too, with correct geometry and text, as long as a measureScope is mounted somewhere in the app. You do not have to be on a SwiftUI screen for it to work.

Two limits are worth knowing before you rely on it:

The canvas

The canvas starts clean on purpose — no boxes, no labels — and behaves like a design tool.

Two product rows selected, the 42-point gap marked in red, and the combined size shown as 370 × 134.
Two rows selected. The 42 pt gap is red because 42 is not on this project's spacing scale — see Your spacing scale.

Layers

The Layers menu in the toolbar turns on the QA overlays, all off by default: bounds, size labels, internal padding, external spacing, grid, safe area, layout columns, concentric corner guides, rulers, and the design check.

Rulers and guides

Rulers run along the top and left. Drag out of a ruler to create a guide; drag one off the canvas or double-click it to remove it, or clear them all with ⌥⌘;.

Zoom, pause and crop

Pinch on a trackpad, use the toolbar slider, or ⌘+ / ⌘− / ⌘0. Pause (⌘P) freezes the preview on the current frame while selection, measuring and size edits keep working — useful for a screen that animates while you are trying to read it. Crop to scope in the Layers menu trims the preview to the region under your measurement scope.

The view tree

The left panel lists what is on screen with each element's size. Selecting there selects on the canvas and vice versa. The filter box at the bottom narrows by name.

Without the SDK the names come from UIKit's own classes — UIHostingView, CGDrawingLayer — which tells you the shape of the hierarchy but not what anything is. That is what the SDK changes.

The inspector

The right panel reports, for the current selection:

Editing sizes live

The W and H fields push a size override back into the running app, which re-lays-out for real — not a drawing on top of a screenshot. An Align menu positions the original content inside the new frame.

Size editing only works on views the SDK wraps, because there has to be something in the app to apply the override to. Arbitrary SwiftUI has no runtime object graph to edit from outside.

Live values

Size can be imposed from outside. Padding, colour and copy cannot — they are arguments a component was built with, and from outside the process there is no object left to ask. So the component offers them, by handing over the binding it already owns:

@State private var cardPadding: CGFloat = 16

VStack { … }
    .padding(cardPadding)
    .measureTweak("Card padding", $cardPadding, in: 0...48)

Anything offered this way appears under Live values in the inspector, with the editor its type deserves: a slider when you gave bounds, a plain field when you did not, a toggle for a flag, a text field for copy. Changing one goes through your own state, so the app re-lays-out for real — text rewraps, everything below it moves, and the measurements follow.

This is the honest version of something that was tried and removed: a preview-only mock that drew invented text over the screenshot. It looked the same and told you nothing, because the app had not changed. Here it has.

A component on its own

Select an instrumented component and On its own shows it drawn by itself beside the same patch of the screen. On screen a component covered by another, one at zero opacity and one with no height all look identical — like nothing at all — and they are three different bugs. The pair separates them.

A blank picture is an answer too: the component draws nothing itself and everything you see in it belongs to its children. If your app declines to send a picture of a view — see PrismKit.rendersComponent — the inspector says so rather than showing a blank.

Accessibility reader

Read screen plays the screen back in the order VoiceOver would announce it, speaking each element and outlining it on the canvas as it goes. With something selected the button becomes Read selection.

The reader mid-playback: the fifth announcement highlighted in the list and the card it names outlined on the canvas.
Announcement 5 is being read; the card it refers to is outlined on the canvas at the same moment.

Trait words — "Button", "Header", "Selected" — are appended from the traits SwiftUI already set, so what you hear is what a real user hears rather than what someone remembered to write. The globe menu picks the reading language independently of the interface language.

iOS builds no accessibility tree until the system accessibility flag is on. Until you enable it, the reader has nothing to read and the list is empty. The app has a button that sets it for the selected simulator; the app under test has to be relaunched afterwards. Geometry — sizes, spacing, the tree — works regardless.

Your spacing scale

Paddings and gaps are flagged when they are not on your scale, with the nearest token as the hint: 42→40. Out of the box this judges against PrismKit's built-in scale, which is not your design system — the pane says so.

The Design system settings pane: a spacing scale of 4, 8, 12, 16, 24, 32, 40, 48, a grid of 8 points, and a project folder picker.
Settings ▸ Design system.

Type your scale, set the grid, and press Apply. That saves it for this Mac. To share it, point Project folder at your repository and press Write .prismkit.json:

{
  "spacing": [4, 8, 12, 16, 24, 32, 40, 48, 64],
  "grid": 8
}

Commit that file and the app, the MCP server and everything an agent reports judge spacing by the same numbers. Resolution order is: the project file, then this Mac's setting, then the built-in scale.

Compare with a design

Two different things share this name, and they answer different questions.

The design check — numbers

Attach a design and every node is matched against what rendered, then reported as moved, wrong size, missing, or off-token. Expected positions are drawn onto the canvas next to where the element actually landed.

The design check reporting 5 of 6 nodes matched, one unmatched node and one element 18 points too wide.
One node the design expects that nothing matched, and one element 18 pt wider than specified.

A design is a plain JSON document — deliberately not Figma-specific. A Figma node, a Pencil document or an agent's own description all arrive in the same shape:

{
  "source": "Figma",
  "reference": "Shop / Home — v4",
  "frame": { "width": 402, "height": 874 },
  "origin": "safeArea",
  "nodes": [
    {
      "id": "node-1",
      "name": "Featured card",
      "match": "featuredCard",
      "frame": { "x": 16, "y": 300, "width": 168, "height": 170 },
      "text": "Studio Headphones"
    }
  ]
}

Two of those fields decide whether the report is something you can act on.

match is the name the app knows the view by. Set it and pairing is an identity rather than a guess, and every difference under that node inherits that certainty. Leave it out and the comparator falls back to identical copy, then to overlapping rectangles — which works, and produces findings you have to verify before believing. The name it looks for is a measure group; a SwiftUI accessibilityIdentifier is not enough on its own, because SwiftUI does not surface it to the runtime element the snapshot reads.

origin says what the frame’s top-left corner is measured from — "safeArea" for an ordinary screen drawn below the status bar, "screen" (the default) for content that runs to the glass.

Load it with Load design… in the toolbar, or have an agent attach it with attach_design — they are the same document, so the agent's work and yours are the same session.

When a node matches nothing, the report also names the nearest unclaimed element and its overlap. "It moved" and "it disappeared" look identical to a comparison that pairs by position, and only one of them is the bug you are chasing.

The mockup comparison — eyes

Compare in the toolbar takes an image instead, and puts it side by side with the running app, overlaid at an adjustable opacity, or behind a wipe slider. Guides are mirrored across both halves, so a guide you drag on the design lands in the same place on the app.

From Figma or Pencil to a design file

The comparator takes the neutral shape above, so getting a design in means translating from whatever tool drew it. There are two things you have to state, and one that is handled for you.

What you must translate: canvas coordinates

Design tools report positions in canvas coordinates — where the frame happens to sit on an infinite board, so a screen might start at x: 4820. The comparator expects node frames relative to the design frame’s own top-left corner.

So subtract the frame's origin from every node: a node at canvas x: 4836 inside a frame at x: 4820 becomes x: 16.

What you must state: where that corner sits on the device

A design frame’s numbers mean nothing until you say what they are measured from. y: 24 is the safe area on a phone with a Dynamic Island and the glass on a phone without one — the same number, two different places. Say which with origin and the inset the running device reports is added for you, so one design frame holds on every phone instead of being redrawn for each.

Anchor it wrong and the whole screen sits at a constant offset. That is reported once, as screenOffset, with the remaining differences measured with it removed — one mistake reads as one finding rather than as forty. The detection is deliberately strict: a layout bug high on a screen also pushes everything below it, but by amounts that scatter, so that stays reported element by element.

What is handled for you: the scale

You do not need to redraw the design at the device's size. The comparison scales by width — a 390 pt design checked against a 402 pt screen is multiplied by 402 / 390 — and the report says which factor it used.

Because the factor comes from the width alone, a design whose aspect ratio differs from the device drifts vertically the further down the screen you look. If your design frame is a different shape than the device, expect the bottom of the screen to report offsets that are the aspect ratio talking, not the layout.

Figma, via the REST API

Take the two ids out of the frame's URL. In figma.com/design/FILE_KEY/Name?node-id=1-234 the file key is the path segment, and the node id becomes 1:234 — the API wants a colon where the URL has a dash. Get a personal access token from Figma ▸ Settings ▸ Security.

import json, os, urllib.request

FILE_KEY = "abc123…"
NODE_ID  = "1:234"                 # the frame, colon not dash
TOKEN    = os.environ["FIGMA_TOKEN"]

request = urllib.request.Request(
    f"https://api.figma.com/v1/files/{FILE_KEY}/nodes?ids={NODE_ID}",
    headers={"X-Figma-Token": TOKEN},
)
root = json.load(urllib.request.urlopen(request))["nodes"][NODE_ID]["document"]
origin = root["absoluteBoundingBox"]

nodes = []

def walk(node):
    box = node.get("absoluteBoundingBox")
    # Hidden layers still carry a box; they are not on screen, so they would
    # be reported as missing from a screen that is behaving correctly.
    if box and node.get("visible", True):
        entry = {
            "id": node["id"],
            "name": node.get("name"),
            "frame": {
                "x": round(box["x"] - origin["x"], 1),   # canvas → screen
                "y": round(box["y"] - origin["y"], 1),
                "width": round(box["width"], 1),
                "height": round(box["height"], 1),
            },
        }
        if node.get("type") == "TEXT":
            entry["text"] = node["characters"]
        nodes.append(entry)
    for child in node.get("children", []):
        walk(child)

for child in root.get("children", []):
    walk(child)

print(json.dumps({
    "source": "Figma",
    "reference": root.get("name"),
    "frame": {"width": origin["width"], "height": origin["height"]},
    "nodes": nodes,
}, indent=2))

Run that on a real screen and you will get several hundred nodes, because every vector and every auto-layout wrapper has a bounding box. Prune it. A design worth checking against is the elements you would actually review — the cards, the rows, the headings — not every shape that makes them. Filter by type (drop VECTOR, LINE, ELLIPSE), or keep only nodes whose name you deliberately set rather than the ones Figma named Rectangle 41.

Pencil

Pencil is driven through execute, which evaluates a small JavaScript snippet against the open document. Get walks a subtree and hands each node to a visitor; Print adds a line to the response. So instead of pulling the whole tree and pruning it, you ask for exactly the columns you need:

Get("FRAME_ID", (n, c) => Print([
  n.name,
  Math.round(c.bounds.x), Math.round(c.bounds.y),
  Math.round(c.bounds.width), Math.round(c.bounds.height),
  n.content ?? ""
].join(" | ")))

ctx.bounds resolves in the parent’s coordinate space, so walk the parentCtx chain to accumulate an absolute position, then subtract the frame’s own origin — the same subtraction as above. Call get_app_state first: it lists the top-level frames with their ids, and with include_schema it returns the document schema and the full execute API.

Name your layers after the measure groups in the app and the translation becomes mechanical: the layer name is the node’s match, and every finding comes back paired by identity.

Two things will otherwise cost you an afternoon. The MCP server has to be started with --agent; with --app desktop alone every call fails with “you are probably referencing the wrong .pen file”, which reads like a problem with your document. And the first call on a fresh connection hangs for about a minute while the agent name registers, then fails — the immediate retry answers in well under a second. Earlier builds exposed snapshot_layout and batch_get; 1.2 folds both into execute.

Or let the agent do it

This is the shortest path, and the reason the format is neutral in the first place. Give your agent both MCP servers — the design tool's and this one — and the translation is a step it can take on its own:

“Read frame 1:234 from that Figma file, convert the cards and rows to PrismKit's design shape with coordinates relative to the frame, attach it, then tell me what does not match on the screen currently running.”

The agent calls attach_design and then compare_to_design, and because the attachment is shared, the same design appears drawn on your canvas while it works.

Check it against the example design

Before pointing this at your own screens, watch it work on one where the answer is already known. The SDK repository ships Example.pen — a six-frame Pencil document holding five screens and the design system behind them — and Example/PrismKitExample, that design built in SwiftUI. The app drifts from the design in seven measured places, on purpose.

All seven are listed in Example/DESIGN-DEMO.md with the finding each one should produce. That list is the point: run the check and compare against it. A finding that is not on the list, or a row on the list with no finding, is worth chasing. It is the difference between a demo you watch and a test that can fail.

  1. Open Example.pen in Pencil, and boot a simulator 393 pt wide (iPhone 15 Pro). Matching the design’s width keeps the scale at exactly 1, so every number you read is the layout rather than the rescaling.
  2. Run the example app with -autopush checkout, which opens the screen whose defects are the most legible. If the view tree comes back empty, see the accessibility list is empty.
  3. Ask your agent to read screen-checkout, translate it, attach it, and compare with tolerance: 6.

You should get seven findings on that screen: a button 8 pt short and the caption that moved because of it, two copy differences — one of which is not a bug, a delivery date the app computes and the mock could only guess — and the residue of two text engines measuring the same string differently.

That last part is why tolerance exists. A design tool and CoreText disagree by a point or two on every line of text, and those differences accumulate down a screen. Six points clears the disagreement while still catching an eight-point mistake; zero would report a perfect screen as broken.

One defect, followed all the way through

The checkout screen's primary button is 8 pt short. Here is every step of that, so you can see where the number comes from rather than taking the report's word for it.

The checkout screen in the simulator: a Review order title, three products, a payment summary and a blue Place order button.
The screen being checked. The button at the bottom is the one the design and the app disagree about — 8 pt is not something you catch by looking.

1. What the design says. The node in Example.pen:

checkout-place-order-button    x 16   y 553   361 × 52

2. What the app does. One line in the example project:

CheckoutScreens.swift:192        .frame(height: 44)

3. What the check reports. Verbatim, from an actual run:

sizeMismatch   checkout-place-order-button
  checkout-place-order-button: height is 44 pt, design says 52 pt (-8 pt).

4. Why that is trustworthy, which is the part worth learning to check. Two fields in the same report decide it:

A finding matched by overlap deserves the opposite reflex. It means the pairing was a guess from geometry, and a wrong guess produces a confident, well-formatted, entirely invented defect. Check those before acting on them.

Reading a whole report

The same run reports seven differences on that screen. Only two are defects, and telling them apart is the skill:

textMismatch     checkout-delivery-estimate
  reads "Arrives Tue, Aug 4", design says "Arrives Thu, Aug 6".
textMismatch     checkout-summary-title
  reads "Payment Summary", design says "Payment summary".
positionMismatch checkout-legal-caption            y is 661.3, design says 676   (-14.7)
positionMismatch checkout-place-order-button-label y is 617,   design says 627   (-10)
sizeMismatch     checkout-place-order-button       height is 44, design says 52  (-8)
sizeMismatch     checkout-item-sneakers-name       width is 101.3, design says 108 (-6.7)
positionMismatch checkout-place-order-button       y is 605.3, design says 612   (-6.7)
Prism Inspector showing the checkout screen with the design drawn over it and the findings listed on the right.
The same comparison in the app: the design drawn over the running screen with each difference labelled, and the findings listed on the right. Shown at the app's default tolerance of 1, which reports the sub-point text differences too — the list above is the same run at 6.
FindingWhat it is
checkout-place-order-button heightA defect. The button is 44 where the design says 52.
checkout-summary-title textA defect. "Payment Summary" against "Payment summary" — a capital letter nobody would catch by eye.
the caption, the label, the button's own yThe same defect, three more times. A button 8 pt short moves everything below it. One cause, one fix.
checkout-delivery-estimate textNot a defect. The design states a fixed date because a mock has to state something; the app computes the real one. Both are correct.
checkout-item-sneakers-name widthNot a defect. 6.7 pt is Pencil and CoreText measuring the same string differently. This is the noise tolerance exists for.

That distribution is normal, and worth expecting: of seven differences, two were worth fixing, three were the same bug seen from below, and two were the tool being honest about things it cannot know. A report that only ever showed defects would be hiding the last two rather than solving them.

Both non-defects are copy or text width, which is why the geometry pass is usually run with compare_text: false once the wording has been read once. That drops this screen from seven findings to five — and takes the capitalisation bug with it, which is the trade to make deliberately rather than by default.

Sessions and export

⌘E exports the canvas exactly as you see it — screenshot, active layers, selection readouts, rulers and guides — at native device scale. It is meant as QA evidence you can paste into a ticket.

⌘S saves a session: the snapshot and the screenshot in one file. ⌘O opens one later with no simulator and no app running, and selection, measuring, the accessibility list, export and local size edits all still work.

Why add the SDK

Everything above works on an app you have not modified. The SDK buys four things:

Install and instrument

dependencies: [
    .package(url: "https://github.com/zalazara/PrismKit-SDK.git", from: "0.2.0")
]

Mark a scope once, near the root of the screen you are working on:

import PrismKit

struct ShopScreen: View {
    var body: some View {
        NavigationStack { content }
            .measureScope()
    }
}

Then name the components you care about:

ProductRow(item: item)
    .measure("productRow", role: .container)

Text(item.title)
    .measure("productRow", role: .title)

Two measurements sharing a group name form one component: reporting the container and its content under the same group is what lets the inspector show the padding between them.

Put the scope at the root and it follows navigation — whichever screen is visible is the one that reports. Covered screens stay mounted in a NavigationStack, so PrismKit gates on appearance rather than on existence.

API reference

measureScope

func measureScope(
    enabled: Bool = true,
    configuration: MeasureConfiguration = .default,
    selection: Bool = false,
    toolbar: MeasureToolbarVisibility = .automatic,
    streaming: Bool = true
) -> some View
ParameterWhat it does
enabledTurns collection on for the whole subtree. Nothing is collected when off.
configurationWhich overlays draw, the grid size, and the spacing tokens to validate against.
selectionStarts in selection mode. While active the overlay intercepts touches on measured elements.
toolbar.automatic shows the floating toolbar on device and hides it while the Mac app is connected. .visible / .hidden override.
streamingStreams to the Mac app. A silent no-op when nothing is listening, so leaving it on costs nothing.

measure

func measure(
    _ group: String,
    role: MeasurementRole = .container,
    metadata: [String: String] = [:],
    file: String = #fileID,
    line: Int = #line
) -> some View

Roles: .container, .content, .title, .subtitle, .icon, .image, .background, and .custom("…").

The last two are filled in by the compiler and record where the component is written, so a finding can point at a line of code instead of at a name to go and search for. Do not pass them — with one exception. A helper that calls measure for you must declare and forward them, or every view it wraps reports the helper's own line:

func designNode(
    _ name: String,
    file: String = #fileID,
    line: Int = #line
) -> some View {
    accessibilityIdentifier(name).measure(name, file: file, line: line)
}

measureTweak

func measureTweak(_ name: String, _ value: Binding<CGFloat>, in bounds: ClosedRange<CGFloat>? = nil) -> some View
func measureTweak(_ name: String, _ value: Binding<Bool>) -> some View
func measureTweak(_ name: String, _ value: Binding<String>) -> some View

Offers a value for the inspector to change while the app runs. Names must be unique in the app.

PrismKit.rendersComponent

PrismKit.rendersComponent = { id in !id.hasPrefix("payment-") }

Decides whether a component may be drawn on its own and sent to the Mac. Two reasons to say no: cost, since each one is a real render on the main thread; and content, since a picture leaves the process and lands in a saved session file. Refusing one does not hide it — its geometry is still measured and reported.

MeasureConfiguration

gridSize, spacingTokens, and the layer toggles showsGrid, showsSafeArea, showsBounds, showsSizeLabels, showsInternalPadding, showsExternalSpacing, validatesTokens. The floating toolbar can change them at runtime.

Bounds, sizes and padding start off. Drawing them for every instrumented view at once is legible on a demo screen and unreadable on a real one, so a screen comes up clean and you turn on what you need. On a busy screen the select button beside the toolbar's ruler is the better route: arm it, tap a component and only that one is drawn; tap a second and you get the distance between them.

It works in Xcode previews as well as in the simulator.

What ships in a release build

Nothing. That is the point, and it is checkable rather than promised.

The measurement collection, the overlay, the accessibility walk and the network client all sit behind #if DEBUG. In a release build measureScope and measure return the view unchanged, no socket is opened, and the networking code is not in the binary at all — verified against a real consumer's release build, where 148,000 Network-framework symbol references became zero.

The package's own CI runs its test suite in both debug and release, because a guarantee that only holds in the configuration you test is not a guarantee.

The MCP server

prismkit-mcp speaks the Model Context Protocol over stdio and ships inside the app bundle — someone who downloads the app has no Swift package checked out anywhere, so a server living outside it would exist only for people who built the app themselves.

The AI agents settings pane showing the tool list and a ready-to-paste JSON configuration pointing at the server inside the app bundle.
Settings ▸ AI agents. The Copy button puts the JSON on the clipboard.
{
  "mcpServers": {
    "prismkit": {
      "command": "/Applications/Prism Inspector.app/Contents/MacOS/prismkit-mcp"
    }
  }
}

The app and the server both listen on port 9294, so only one can run at a time. Quit the app while an agent is working, or set PRISMKIT_PORT for the server and point the SDK at the same port to run both.

Tools

ToolArgumentsReturns
get_measurements Everything on the current screen: instrumented elements and accessibility-derived ones, with ids, groups, roles, frames, padding and spoken descriptions.
measure_distancefirst_id, second_id Per-axis gaps between two elements, or the four insets when one contains the other. Ids come from get_measurements.
check_alignmenttolerance Which edges align exactly, which nearly align within the tolerance, and which paddings are off the spacing scale.
attach_designdesign Keeps a design attached to the session so it need not be resent while you navigate. The app draws it too.
compare_to_designdesign, tolerance, compare_text, minimum_overlap Per-node differences in position, size and wording.
detach_designForgets it; the app stops drawing it.
get_design_tokens The spacing scale every judgement uses, and where it was resolved from.
set_design_tokensspacing_tokens, grid_size, project_path Sets the scale. With project_path it writes a committable .prismkit.json.
list_simulatorsBooted simulators: name, UDID, runtime.
capture_screenshotudidA PNG of the simulator screen.

compare_to_design defaults to not comparing text, because a running app shows real data where a design shows placeholders, and reporting that as a defect trains people to ignore the report.

A worked example

What a review loop looks like from the agent's side:

  1. Set the team's scale once, and commit it:
    set_design_tokens(
      spacing_tokens: [4, 8, 12, 16, 24, 32, 40, 48],
      grid_size: 8,
      project_path: "/Users/you/Projects/Shop"
    )
  2. Attach the design being built against — a Figma node the agent has read, exported into the neutral shape:
    attach_design(design: { "frame": …, "nodes": [ … ] })
  3. Navigate the app, then ask for the differences. Repeat after each fix without resending the design:
    compare_to_design(tolerance: 2)
    check_alignment(tolerance: 2)
  4. Open Prism Inspector to look at what it found — the same attachment, drawn on the canvas.

Keyboard shortcuts

KeyAction
EscClear the selection
⌘+ / ⌘− / ⌘0Zoom in, out, reset
⌘RReload the capture
⌘PPause / resume the preview
⌘EExport the canvas as PNG
⌘S / ⌘OSave / open a session
⇧⌘RRulers
⌥⌘;Clear all guides
⌥⌘1 / ⌥⌘2View tree / inspector
⌘,Settings
⌘?Help

Updates

Prism Inspector ▸ Check for Updates… Updates are signed with a key that never leaves the release machine, and the app refuses any download whose signature does not match — controlling the download server is not enough to push a tampered build.

If it says the app cannot be updated from where it is running, the app is translocated: see Install.

Troubleshooting

No app appears

Check a simulator is booted and the app is in the foreground. If you are using the SDK, confirm the scope is on a screen that is actually visible — a scope inside a covered navigation destination reports nothing by design.

The accessibility list is empty

The system accessibility flag is off. Turn it on from the app and relaunch the app under test — see Accessibility reader.

The agent cannot connect

The Mac app is probably holding port 9294. Quit it, or give the server its own port with PRISMKIT_PORT.

Everything is flagged off-token

You are being judged against the built-in scale rather than yours. Set it in Settings ▸ Design system.

Names are all UIKit classes

That is the zero-instrumentation path working as intended. Add the SDK to get your own names.

Something missing here?

Open an issue on the repository — gaps in these docs are worth reporting as much as bugs in the app.