---
title: "The harness, DroppyKit"
description: "Run your droplet in Droppy&#x27;s Settings panel, without installing it."
canonical: "https://getdroppy.app/docs/droppykit/harness"
last-updated: "2026-09-09"
---

# The harness

Run your droplet in Droppy's Settings panel, without installing it.

`DroppyKitHarness` opens your droplet in a window that **is** Droppy's Settings panel: the window chrome, the sidebar rows, the page opener and the content column are the app's own source files, copied into the SDK. What it adds is room — the window is larger than the real one, so a solo and a paired widget fit side by side without scrolling.

It never touches the system. Preferences live in memory, permissions are whatever you toggle, and `workspace.open` records the URL instead of opening it, so a droplet under test cannot fling Safari at you every time you press its button. That is also why you can run it while Droppy itself is running: the two never fight over shortcuts, the pasteboard, or the notch.

### Adding it

One file in an executable target:

```
import DroppyKit
import DroppyKitHarness

@main
struct WorldClockHarness: DropletHarnessApp {
    static func makeDroplet() -> any Droplet { WorldClockDroplet() }
}
```

**Important.** The file must not be called `main.swift`. Swift treats that name as top-level code, which cannot coexist with `@main`.

### Running it

```
droppykit run                        # or Scripts/run-harness.sh WorldClockHarness
droppykit run --page shelf-widget    # open straight onto one surface
```

The harness is built into a real signed `.app` and launched with `open`. A bare `swift run` produces an unbundled binary, and macOS gives one of those fallback window chrome — visibly not the panel you are trying to match.

### The pages

| Page | Shows |
| --- | --- |
| Overview | Identity, icon (and the built icon in all six icon styles), creator, and which surfaces you provide against which you declared |
| Shelf widgets | Every widget solo and paired, at its own declared widths; wrapped around the camera when it opts in; the wash controls to use |
| Live activity | The compact row in a simulated notch, the expanded detail, and the seat |
| Expanded surface | The takeover at the size you return, and the chrome it suppresses |
| Settings pane | Your pane in the real content column |
| Lock screen | Your published row |
| Menu bar | Your extra as a Command Center tile, Small and Medium, and the detail it opens at the panel's width |
| Capabilities | A switch per declared capability, and a warning for undeclared ones |
| Stored values | Every key you have written, with the key Droppy would really use |
| Activity | Every host call and log line, in order |

### What it catches

**Surfaces you declared but did not implement**, and the reverse. The Overview page compares your conformances against `droplet.json` and names the difference. The two disagreeing is the single most common reason a droplet validates locally and then does nothing once installed.

**Refusals.** Turn a capability off and the Activity page shows the call being refused, with the capability that was missing — which is exactly what a user who declined a permission will experience.

**The notchless case.** Toggle **Hardware notch** off on Overview and every surface re-renders as a floating island. Content that assumes wings either side of a cut-out has nowhere to go, and this is where you find that out.

**A row under the camera.** A widget that declares [`notchWrap`](https://getdroppy.app/docs/droppykit/api/shelf-widget-layout-traits#notch-wrap) gets a third stage on the Shelf widgets page: alone on a notched shelf, raised beside the camera, with the [`ShelfWidgetNotchWrap`](https://getdroppy.app/docs/droppykit/api/shelf-widget-notch-wrap) the host would hand it, computed the host's way for the simulated notch, and the housing outlined over it. A half of the header that strays under the camera shows there, and so does a widget that offsets its row on top of the host's.

**The controls to use.** Under the stages, the Shelf widgets page draws the wash controls on the shelf's black at every size Droppy uses (the neutral and accent disc at 34, 28 and 24, the pill at 34 and 28), each labelled with the style that draws it. See [Design guidelines](https://getdroppy.app/docs/droppykit/design).

**Losing the live-activity seat.** Take the seat away from the Live activity page and watch what your droplet does. Losing it is normal; a droplet that keeps working as though it still had it is not.

### Pictures of every surface

```
droppykit run -- --shots ./shots
droppykit run -- --shots ./shots --shot-height 1900
```

`--shots` renders every harness page to a PNG and exits, without ever putting a window on screen. A relative directory is taken from your package root, so `./shots` lands beside `Package.swift`. The images are of the real views, laid out and drawn by the same code, so they are the pictures a submission wants and the fastest way to compare a change against the previous build. The default height is the harness window's; `--shot-height` renders a page taller than that in one image instead of cutting it at the fold, which a page with several stages needs.

A headless run stays in the foreground and prints what it wrote, so a script or a coding agent gets the exit status and the file list rather than a window that opened somewhere else.

### A verdict a tool can read

```
droppykit run -- --shots ./shots --report ./shots/report.json
droppykit run -- --report ./report.json
```

`--report` writes the harness's findings as JSON: the same verdicts the Overview and Capabilities pages draw, for something that cannot read a window. `surfaces[]` carries `declared`, `implemented` and a `verdict` per surface (`provided`, `missing`, `undeclared`, `not-provided`); `problems` lists everything the pages would flag, in words: a manifest that did not decode, an `activate(host:)` that threw, an id that disagrees between Swift and `droplet.json`, a widget that leaves a width to the host, artwork that did not load. `activity` and `log` are the Activity page, refused calls marked. With `--shots` the report also lists the images. A droplet is not done while `problems` has anything in it, which is exactly the rule the coding-agent brief gives an agent; see [Build with a coding agent](https://getdroppy.app/docs/droppykit/coding-agents).

### What it does not do

It does not simulate Droppy's open and close springs. Those are the app's, and a harness guessing at them would teach the wrong timing. The page header's icon is a flat preview of the icon document, not the composited macOS icon, and while you build that is the scaffold's placeholder: Jordy designs the Store icon (see [The icon](https://getdroppy.app/docs/droppykit/icons)). The Overview's six icon styles are the real thing, drawn by the system's renderer from the bundle `droppykit build` wrote into `.build`, on macOS 26 and later; before the first build, or on an older macOS, the page says so instead.

`DropletHarnessApp` is the protocol the entry point conforms to. It lives in the `DroppyKitHarness` module, which ships alongside this one.

## See also

[Your first dropletFrom an empty folder to a droplet running in the harness.](https://getdroppy.app/docs/droppykit/first-droplet) [Build with a coding agentClaude Code, Codex and Cursor: the brief, the MCP server, and pictures an agent can look at.](https://getdroppy.app/docs/droppykit/coding-agents) [Droppy PlaygroundA free, separate app: the real surfaces, the media player, and your droplets.](https://getdroppy.app/docs/droppykit/playground) [SubmittingReview, signing, and how a droplet reaches somebody's Mac.](https://getdroppy.app/docs/droppykit/submitting)
