Shotee: my own screenshot app for the Mac

A native menu bar app I built with Claude Code to replace CleanShot X: 1 key for a window, a display or an area, the frozen screen up in about 56 ms, the file on disk 21 ms after the click.

The Shotee app icon: a capture frame around a red edelweiss on a red gradient square, with the name Shotee under it

Shotee is the screenshot app I now use on my Mac. It replaces CleanShot X, which I ran through Setapp: macOS app subsciption since 2022. I press ⌘⇧5, click a window, and the JPEG is on disk and on the clipboard before my hand is back on the keyboard. Speed is the main goal. Everything else in the design follows from it.

I built it with Claude Code: version 1.0 shipped on 21st October 2026, version 1.0.1 on 24th October. The app is 1.4 MB. The name follows the -ee family of my tools. It works with my recent-files app, see Lastee: my own recent-files app for macOS.

Why I replaced CleanShot X

CleanShot X is a good app. It counts its own use per year, and those counts showed how little of it I needed:

Year Captures Annotated Videos Cloud uploads
2022 (from 19th June) 578 292 10 0
2023 1,483 613 29 0
2024 1,499 316 78 0
2025 1,333 109 0 0
2026 (to 19th October) 1,923 151 13 0
  • About 8 captures a day, area and window both.
  • Claude Code is the main reader. About 40% of my captures show up by path in a Claude Code session within 30 days. Most show one of my own apps, to show the agent a bug.
  • Annotation is for sharing. 8% of captures get annotated. In 2026: 100 arrows, 85 pixelates, 68 rectangles, 29 highlights, 7 texts, 2 crops.
  • Never used: cloud upload (0 in 4 years), OCR, scrolling capture (0 since 2024), pin to screen, self-timer, the history window.

CleanShot does about 40 things. I do 5 of them. Shotee does those 5, and video stays in my screen recorder.

What Shotee does

1 key, 3 modes

⌘⇧5 freezes every display and opens the capture overlay:

Mode How I get there A click captures
Window The overlay opens on it: a red frame The window under it
Display Space, and Space again goes back The display under it
Area Press and hold 250 ms, or drag The area I drag
  • Enter captures the whole display under the pointer, in every mode. Esc cancels.
  • Area mode has a loupe at 8x with the pixel position and the selection size. ⇧ keeps a square, ⌥ draws from the centre, Space moves the selection, the arrow keys move an edge by 1 pixel.
  • The frozen screen keeps open menus and hover states as they were. No cursor, no desktop icons, no window shadow.
  • ⌘⇧6 captures the previous area again, without the overlay.
  • ⌥⌘5 opens the newest capture in the editor.

After a capture, a red frame shows around the captured area for 150 ms. No sound, no thumbnail.

25 Oct 2026 The menu bar icon is a camera viewfinder. It used to be my hexagon logo, like 5 of my other menu bar apps, and 6 identical hexagons in a row were impossible to tell apart. Each app now has its own SF Symbol glyph, and my design system says so: see My apps: everything I built for myself.

Names that say what's in the file

YYYYMMDD-HHMMSS-<app>-<site>.jpg, for example 20261021-183522-filee.jpg or 20261021-183522-edge-skyscanner.jpg (dummy names: the file names, paths and outputs in this note are examples, not my real captures). The app is the one with the largest visible part of the capture. For a browser, Shotee asks the browser for the tab's address through Apple Events, with a 150 ms limit, and falls back to the window title. Never the path, the query or the page title.

The path still reaches Claude the same way as before: ⌥⌘T in Lastee copies the path of the newest file.

The editor

Key Tool or action
V Select: move, resize, delete
A, R Arrow, rectangle
P Pixelate, with [ ] for the strength
H, T Highlight, text
C Crop, snaps to edges and objects
1 to 5 Colours
⌘S, Esc Save over the capture
⇧⌘S Save as, in the last folder used
⌘⌫ Back to the original capture
  • Lines start at width 8. The − and + buttons, or [ ], change it.
  • Pixelate strength goes from 1 to 20. With the Pixelate tool on, or a pixelated area selected, the same − and + set the strength instead of the width. Shotee keeps the last strength for the next capture.
  • A saved edit goes to the clipboard, like a new capture.

Saving over the capture keeps the name, so a path I already pasted into Claude stays valid. The original and the objects stay 30 days, so a capture opens again with editable arrows and boxes. I can drag the image out of the editor into a chat or an email.

A Shotee editor test image with flat coloured test shapes, annotated with a red arrow, a red outline rectangle, a translucent highlight and the text Bug here in red with a white outline

The image comes from QA, on a synthetic test screen. Shotee's QA never reads my real screen.

Speed

Measured in daily use

Shotee times every capture and keeps the numbers. shotee perf prints them against the budgets. From my real captures since version 1.0, on 4 displays:

Step Budget p50 p95
Key to the first frame < 100 ms 46 ms 80 ms
Key to the frozen overlay < 100 ms 56 ms 90 ms
Mouse up to the file on disk < 60 ms 21 ms 35 ms
Browser site lookup < 150 ms 16 ms 72 ms

100 ms is the line where a response feels instant. The frozen screen is up before my fingers leave the keys.

I never timed CleanShot X the same way, so I have no side-by-side table. What I can say: Shotee meets every budget at p95, and the steps that cost time are Apple's, not Shotee's.

The spike

Before any app code, the agent ran a spike: a signed test app on my 4 real displays, 2 rounds, on 21st October 2026. It answered the questions the design depended on:

Step Measured
1 display, captureImage 52 to 68 ms
3 displays started together, the last one in 135 to 144 ms
3 displays as sample buffers 224 ms
Pointer's display first, alone, on screen 73 ms
First capture after 5 to 30 min idle 128 to 167 ms
Trigger by Darwin notification 4 ms
Trigger by shotee:// URL 66 ms

3 results shaped the app:

  • ScreenCaptureKit serves captures 1 at a time, about 45 ms apart, in no fixed order. So Shotee asks for the display under the pointer alone, shows it, then asks for the others. The eye is on the first display; by the time it moves, the others are up.
  • The capture service gets cold. After 5 minutes idle, the first capture took 2x to 3x longer. Shotee refreshes its display and window cache every 120 s while the screens are awake. It never captures just to stay warm.
  • Darwin notifications are 15x faster than the URL scheme. My Stream Deck and Hammerspoon use them.

Rules that keep it fast

These rules are in the spec. A change that breaks one needs a measured reason.

  1. Capture first, then show, then take the focus. Activating the app closes another app's open menu, so Shotee activates only after the frames are in.
  2. Nothing to look up at the key press. Displays and content filters are ready before I press anything.
  3. The fast capture where it's safe. A plain rectangle capture takes 35 to 50 ms but can't leave out windows. A filtered capture takes 75 to 90 ms. Shotee uses the fast one everywhere, and replaces it with a filtered frame only on a display that may show desktop icons.
  4. No CPU drawing in the overlay. The dimming, the loupe and the red frame are Core Animation layers. Implicit animations are off.
  5. Pre-built windows. The overlay windows and the editor exist from launch, hidden.
  6. Hover capture. When the red frame rests on a window for 60 ms, Shotee captures that window in the background. The click then only encodes.
  7. The save first, then the overlay goes. Ordering 3 panels out before the save cost 20 to 40 ms.
  8. 1 colour conversion, with vImage. It converts a frame to sRGB in 5 to 20 ms. ImageIO's own conversion took 155 to 180 ms on the same frames.
  9. Never read a protected folder. Listing the Desktop to count icons put a privacy prompt on screen and froze the main thread.

How it's built

Stack

  • Swift 6.3, AppKit and Core Animation. No SwiftUI, Settings window included.
  • ScreenCaptureKit for frames, CGWindowListCopyWindowInfo (3 to 7 ms) for hit tests and app names.
  • ImageIO for JPEG at quality 0.85. A 4K JPEG encodes and writes in 27 ms. PNG took 85 ms and HEIC 103 ms in the benchmark, so JPEG stays.
  • Carbon hotkeys, which need no Accessibility permission.
  • SQLite for the history and the timings of every capture.
  • A Python CLI, standard library only.
  • No dependencies.

Why not Rust: the milliseconds of a capture go into ScreenCaptureKit, the window server, the display refresh and ImageIO. Every language calls the same code. Shotee's own code on the hot path runs in under 1 ms.

Private by default

  • No network code. Apple Events go only to the browsers on this Mac.
  • No EXIF, no XMP, no display profile in the file. ImageIO adds a small EXIF block and an empty Photoshop block to every JPEG; Shotee strips both from the bytes before the write, in under 0.1 ms.
  • 1 Finder attribute, so Spotlight's kind:screenshot still works. Attributes stay on the Mac: a drag into another app sends the bytes only.

Gotchas worth knowing

  • Started from a terminal, the binary inherits the terminal's Screen Recording grant. Test the real app through open, never from the shell.
  • 2 apps can register the same Carbon hotkey. Shotee can't warn about it, so CleanShot had to give up its keys first.
  • A display without a name. 1 of my displays reports an empty name. Shotee calls it display-<id> and can exclude a display by its size.
  • The prompter stays out. A dimmed overlay on the display in front of my camera would show in a call.
  • Pixelated text can be read back. The redaction tool planned for phase 2 uses solid boxes, never a pixelate.

The agent interface

Every command takes --json, so NicAI can use Shotee inside a task (see What is NicAI). Commands that act open a shotee:// URL and confirm once the app echoes the request id:

shotee capture --mode window --json   # the front window, saved at once: returns the path
shotee last --json                    # the newest capture
shotee perf --days 7 --json           # p50 and p95 per step against the budgets

A capture answers with 1 JSON document. Dummy example:

{
  "ok": true,
  "confirmed": true,
  "action": "capture",
  "mode": "window",
  "path": "/Users/you/Screenshots/20261021-183522-filee.jpg",
  "created_at": "2026-10-21T18:35:22+02:00"
}

How it was built

Shotee followed my appee process for apps:

  1. Usage review: CleanShot's statistics and settings, my screenshots folder, and the key maps of my other apps, so no hotkey clashes.
  2. Questions: 30, each with a recommended answer, answered on 21st October 2026. CleanShot's settings became Shotee's defaults: JPEG, frozen screen, no shadow, no sound.
  3. Spike and spec: the measurements first, then the rules and budgets.
  4. Build and QA: version 1.0 on the evening of 21st October. A smoke test on the real displays changed 4 things in the code before I used it.
  5. Version 1.0.1 on 24th October: window mode first, every capture on the clipboard, and the agent interface.

QA at version 1.0: 18 unit tests and 84 end-to-end checks. QA runs the real app on fake displays made of test images, with fake browser answers and a private clipboard. It never registers global keys and never takes the focus, so it runs while I work.

Next

Phase 2 adds text: OCR in a short-lived helper process, a key to copy the text of an area, redaction of emails, keys and IBANs, and shotee search over the text of every capture.

Further reading

The app Shotee replaces:

Apple's framework behind every capture:

The tool I built it with:

NicAI
Written by NicAI, Nic's AI assistant, for his personal knowledge base. Researched and drafted by the model, not hand-written by Nic. Verify anything you plan to act on.