Build CleanShot X

YESback to the verdict

0%0 of 20 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

A capture tool that takes region, window and full-screen shots through ScreenCaptureKit, records screen video with microphone audio in sync, lets you annotate with arrows, boxes and a blur that actually destroys pixels, trims the result, and exports to MP4 or GIF on your own disk.

estimated effort weekendthe files for this build are in the project pack

LanguageSwift 6 and SwiftUI in XcodeCaptureScreenCaptureKit, requires macOS 12.3 or newerVideoAVAssetWriter writing H.264 into MP4GIF exportffmpeg invoked as a subprocess with a generated paletteStorageFlat files in a capture directory plus a SQLite index

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. installfree if you already own a Mac

    Why ScreenCaptureKit needs 12.3 as an absolute floor, and the screenshot API used here is cleaner on 13 and later.

    Get it Apple menu, then About This Mac, to check your version.

    Verify About This Mac reports macOS 13 or higher

  2. installfree

    Why Builds and signs the app and provides the video debugging tools you will need for the sync work in Phase 3.

    Get it Install from the Mac App Store or developer.apple.com/xcode. It is a multi-gigabyte download. open ↗

    Verify xcodebuild -version prints 16 or higher

  3. installfree

    Why AVFoundation cannot write GIF. Without ffmpeg, Phase 4 loses GIF export and nothing else.

    Get it brew install ffmpeg, or download a static build from ffmpeg.org. Decide early whether you will bundle it or require it, because bundling affects your signing. open ↗

    Verify ffmpeg -version prints a version banner

  4. decidefree

    Why Every rebuild that changes the binary path or signature can invalidate the grant, and macOS will not re-prompt.

    Get it System Settings > Privacy and Security > Screen Recording. Remove the stale entry, then run the app again.

    Verify The app appears in the Screen Recording list and capture returns pixels rather than a black frame

  5. 99 USD per year

    Why Product phase only. Without it there is no Developer ID certificate, so no notarization, so a Gatekeeper warning on every other Mac.

    Get it Enrol at developer.apple.com/programs; review can take a day or two. open ↗

    Verify A Developer ID Application certificate is listed in Xcode > Settings > Accounts

Data model

Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.

- captures(id, kind, path, width, height, scale, created_at, duration_ms) · kind is image or video
- annotations(id, capture_id, type, geometry_json, color, thickness, z) · type is arrow, box, text, highlight or redact
- settings(key, value) · capture directory, default format, whether to copy to pasteboard on capture
- Annotations are stored separately from the pixels so an export can be re-run, except redaction, which is destructive at export time by design.

Environment variables

These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.

VariableNeededExampleWhere the value comes from
CAPTURE_DIRrequired~/Pictures/CapturesWhere images and videos are written. Pick something Spotlight indexes so you can find captures outside the app.
CAPTURE_DB_PATHrequired~/Library/Application Support/Shots/index.dbThe SQLite index of captures and annotations.
FFMPEG_PATHoptional/opt/homebrew/bin/ffmpegOnly needed if ffmpeg is not bundled and not on PATH. GIF export is disabled when it cannot be found.
RETENTION_DAYSoptional90Captures older than this are deleted on launch unless starred. Leave unset to keep everything.
SPARKLE_FEED_URLoptionalhttps://example.com/appcast.xmlProduct phase only. Where your signed appcast is published.

The build, in order

  1. Permission and a region screenshot

    Dragging a region writes a correct PNG to disk, with the Screen Recording permission handled honestly.

    1. Call CGRequestScreenCaptureAccess early. If it returns false, show a panel explaining what is lost and a button that opens x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture.

      Files Permissions.swift

    2. A borderless transparent window at screen level covering every display, with a crosshair cursor and a dimmed region outside the selection. Escape cancels.

      Files SelectionOverlay.swift

    3. SCContentFilter for the display, set the SCStreamConfiguration width and height to the selection multiplied by the display backing scale factor, then write PNG through CGImageDestination.

      Files Capture.swift

    done when · tick each as it passes
    watch out
    • The Screen Recording grant is tied to your code signature. Re-signing with a different identity invalidates it and macOS will not prompt a second time; the user has to delete the entry in System Settings first. This will bite you during development every time the binary moves.
    • A captured image is in physical pixels, not points. On a Retina display a 400 point selection is an 800 pixel image, and forgetting the backing scale factor gives you a half-size or double-size file.
  2. Annotation, and a redaction that is real

    You can draw on a capture, and a redacted region is unrecoverable in the exported file.

    1. Shapes are values with a type, a rect or a pair of points, a colour and a z index. Render them in a SwiftUI Canvas over the image. Undo is a stack of shape arrays.

      Files Editor.swiftShape.swift

    2. For each redact shape, crop the region, resize it to roughly eight pixels across with CIImage, scale it back to the region size, and draw the result into the output context before any other annotation.

      Files Redact.swift

    3. Draw the base image, then redactions, then the remaining shapes in z order, into a CGContext at full pixel size. Write PNG or JPEG.

      Files Export.swift

    done when · tick each as it passes
    watch out
    • A blur or a black box drawn as an overlay leaves the original pixels in the file if you ever export the layered form, and pixelation with a large enough block size is still reversible if the block grid is fine. Downsample the region to a few pixels and scale it back up, or fill it with a solid colour. Then verify by opening the exported file somewhere else.
  3. Screen recording with microphone audio

    A recording plays back with audio and video still in sync a minute in.

    1. SCStreamConfiguration with the display size, a frame rate you choose deliberately, and showsCursor as a setting. Handle the stream stopping when the display configuration changes.

      Files Recorder.swift

    2. AVCaptureSession with an audio device. Microphone is its own privacy grant, separate from Screen Recording, and it has its own usage description string in Info.plist.

      Files Audio.swiftInfo.plist

    3. Create AVAssetWriter with a video input and an audio input. Call startSession(atSourceTime:) once with the first video sample's timestamp, and append audio using the same timebase.

      Files Writer.swift

    done when · tick each as it passes
    watch out
    • Screen samples and microphone samples arrive on separate callbacks with unrelated clocks. Start the AVAssetWriter session once, from the first video sample's presentation timestamp, and translate audio timestamps into that same timeline. Starting each input from its own first sample is the standard way to get audio that drifts steadily out of sync.
  4. Trim, export and the capture pipeline

    Every capture lands where you expect, in the format you asked for, with one keystroke.

    1. AVAssetExportSession with timeRange set. Verify the output duration, because setting a range on a player is not the same as writing a trimmed file.

      Files Trim.swift

    2. Run ffmpeg twice: once with palettegen into a temporary PNG, once with paletteuse. Bundle ffmpeg or detect it and tell the user how to install it.

      terminal
      ffmpeg -i in.mp4 -vf "fps=15,scale=800:-1:flags=lanczos,palettegen" palette.png
      ffmpeg -i in.mp4 -i palette.png -lavfi "fps=15,scale=800:-1:flags=lanczos[x];[x][1:v]paletteuse" out.gif
    3. Three hotkeys for region, window and full screen. On capture, write the file and optionally put it on the pasteboard so the next Cmd-V pastes the image.

      Files Hotkeys.swift

    done when · tick each as it passes
    watch out
    • A one-pass GIF from ffmpeg uses a fixed palette and looks like it came from 1998. Generate a palette with palettegen from the actual frames, then apply it with paletteuse. It is two commands and it is the whole difference.
  5. Library and cleanup

    Captures accumulate somewhere you can find them, and old ones do not fill the disk quietly.

    1. Read captures ordered by created_at, render thumbnails generated at capture time rather than by decoding full images on scroll.

      Files Library.swift

    2. One delete function removes the file then the row. On launch, drop rows whose file is missing and offer to import files with no row.

      Files Store.swift

    3. A setting for maximum age and maximum total size. Apply on launch and after each capture, oldest first, never starred.

      Files Retention.swift

    done when · tick each as it passes
    watch out
    • Deleting the row without deleting the file, or the file without the row, is the bug you will ship. Make deletion one function that does both and reconciles orphans on launch.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more screen recording.