Build CleanShot X
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.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- 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 - 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 - 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 - 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 - account99 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.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
CAPTURE_DIR | required | ~/Pictures/Captures | Where images and videos are written. Pick something Spotlight indexes so you can find captures outside the app. |
CAPTURE_DB_PATH | required | ~/Library/Application Support/Shots/index.db | The SQLite index of captures and annotations. |
FFMPEG_PATH | optional | /opt/homebrew/bin/ffmpeg | Only needed if ffmpeg is not bundled and not on PATH. GIF export is disabled when it cannot be found. |
RETENTION_DAYS | optional | 90 | Captures older than this are deleted on launch unless starred. Leave unset to keep everything. |
SPARKLE_FEED_URL | optional | https://example.com/appcast.xml | Product phase only. Where your signed appcast is published. |
The build, in order
Permission and a region screenshot
Dragging a region writes a correct PNG to disk, with the Screen Recording permission handled honestly.
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.swiftA borderless transparent window at screen level covering every display, with a crosshair cursor and a dimmed region outside the selection. Escape cancels.
Files
SelectionOverlay.swiftSCContentFilter 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 passeswatch 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.
Annotation, and a redaction that is real
You can draw on a capture, and a redacted region is unrecoverable in the exported file.
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.swiftFor 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.swiftDraw 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 passeswatch 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.
Screen recording with microphone audio
A recording plays back with audio and video still in sync a minute in.
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.swiftAVCaptureSession 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.plistCreate 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 passeswatch 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.
Trim, export and the capture pipeline
Every capture lands where you expect, in the format you asked for, with one keystroke.
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.swiftRun 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.
terminalffmpeg -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
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 passeswatch 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.
Library and cleanup
Captures accumulate somewhere you can find them, and old ones do not fill the disk quietly.
Read captures ordered by created_at, render thumbnails generated at capture time rather than by decoding full images on scroll.
Files
Library.swiftOne 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.swiftA 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 passeswatch 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.
Signing, notarization and updatesproduct builder
Someone else can install it without a Gatekeeper warning and receive fixes.
Enable hardened runtime, sign with Developer ID Application, and confirm the microphone and screen capture usage strings are present in Info.plist or the prompts will not appear.
terminalcodesign --force --options runtime --sign "Developer ID Application: YOUR NAME" Shots.app
Submit the zip with notarytool and staple the ticket so validation works offline.
terminalxcrun notarytool submit Shots.zip --keychain-profile AC --wait xcrun stapler staple Shots.app
Generate an EdDSA key pair, keep the private half out of the repository, and serve appcast.xml over HTTPS.
Files
appcast.xml
done when · tick each as it passeswatch out- Because the Screen Recording grant is bound to the signature, your first signed release invalidates the grant your testers gave the unsigned build. Say so in the release notes and tell them where to remove the stale entry.
That is the whole plan for CleanShot X. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Scrolling capture of long pages or documents.
- Hosted sharing links, which is the actual product behind the cloud tiers.
- Viewer analytics, which cannot exist without hosting.
- Team libraries, shared branding and the annotation polish of a decade-old app.
- deep macOS integration, scrolling capture, cloud sharing, and years of polish
- instant hosted sharing
- viewer analytics
- team libraries
- cross-device capture
- Window capture with the window shadow removed, which is fiddly and worth it
- A capture history hotkey that pastes the previous capture without opening the app
- Self-hosted sharing by writing to an S3-compatible bucket you own
Need the files? The project pack on the verdict page hands your agent the whole brief · more screen recording.