Skip to content

Repository files navigation

macosrec

Screenshot, record, and OCR native macOS windows, displays, and screen regions from the command line.

Its defining capability: occluded windows capture correctly. A window sitting entirely behind another renders complete, with no raising, focusing, or rearranging. That is what makes it usable from a script or an agent — capture never disturbs what you are doing.

Built on ScreenCaptureKit. Requires macOS 15+.

Install

swift build -c release
make install                       # copies to ~/local/bin/
# or place it anywhere on PATH yourself:
cp .build/release/macosrec /usr/local/bin/

Screen Recording permission is required. Grant it under System Settings → Privacy & Security → Screen & System Audio Recording. Without it, commands exit 4 with an actionable message rather than returning empty results.

Usage

macosrec list       [--hidden] [--app NAME]
macosrec shot       <selector> [-o PATH] [--out-dir DIR]
macosrec record     <selector> [--duration SECS] [--fps N] [--format mov|gif] [-o PATH]
macosrec stop       [--abort]
macosrec ocr        <selector | --input FILE> [--clipboard] [--interactive]
macosrec transcribe --input FILE [--locale LOCALE]     # not yet implemented
macosrec version

Every subcommand accepts --json / --no-json and --quiet. macosrec --version and macosrec version both report the version. Run macosrec help <subcommand> for detail.

Selectors

Exactly one per invocation:

Selector Meaning
--window <id> Exact window ID. Unambiguous; preferred for scripting
--app <name> Case-insensitive app name; prefers on-screen windows
--title-match <substring> Narrows --app when several windows remain
--display <index> Whole display, zero-based index from list
--region <x,y,w,h> Screen-coordinate rectangle

A bare numeric positional argument is a window ID; other text is an app name.

Examples

# List windows and displays
macosrec list
macosrec list --hidden

# Screenshot
macosrec shot --app Finder
macosrec shot --window 15251 -o /tmp/window.png
macosrec shot --display 0 -o /tmp/screen.png
macosrec shot --region 100,100,800,600 -o /tmp/region.png

# Record for a fixed duration, then exit on its own
macosrec record --app Finder --duration 10 -o /tmp/clip.mov
macosrec record --app Finder --duration 3 --fps 10 --format gif -o /tmp/clip.gif

# Open-ended recording, stopped from another invocation.
# stop reports the finalized file, so the destination need not be chosen up front.
(macosrec record --window 15251 --out-dir /tmp > /tmp/rec.json 2>&1 &)
macosrec stop            # finalize; prints the path (JSON carries path, bytes, sha256)
macosrec stop --abort    # discard instead

# OCR
macosrec ocr --app Ghostty
macosrec ocr --window 15251 --clipboard
macosrec ocr --input /tmp/window.png
macosrec ocr --interactive          # select a region by hand

Output

Files default to ~/Pictures/macosrec/, with precedence -o → --out-dir → MACOSREC_OUT_DIR → that default. Generated names look like 2026-07-31T00-43-10Z-finder-kloccis1.png: UTC timestamp, lowercase slug, length-capped, and never containing a colon or a space.

Pixel dimensions account for the display's backing scale factor, so a 1280×1050-point window on a Retina panel yields a 2560×2100 image.

Scripting

stdout is JSON automatically when it is not a TTY, so a piped invocation needs no flag:

$ macosrec shot --app Finder | jq -r .path
/Users/you/Pictures/macosrec/2026-07-31T00-43-10Z-finder.png

Success carries path, width, height, bytes, sha256, and target; stop reports path, bytes, and sha256 for the finalized recording. Failure carries error.code, error.kind, error.message, and error.candidates where relevant. An explicit -o into a missing directory creates the parent, matching --out-dir.

Branch on the exit code rather than parsing messages:

Exit Meaning
0 Success
1 Runtime failure
2 Ambiguous target — see error.candidates, or add --title-match
3 Target not found — window IDs are volatile, re-run list
4 Screen Recording / Microphone permission denied
5 Recording-state conflict
64 Usage error

Behaviour worth knowing

  • Captures are refused while a recording is active (exit 5). Any ScreenCaptureKit call from another process truncates an in-flight recording, so refusing is deliberate — previously the screenshot succeeded and the recording was silently destroyed. ocr --input <file> touches no capture API and keeps working.
  • Window IDs are volatile. List immediately before capturing.
  • Tabbed apps report a window per tab, sometimes more, and only the active tab is isOnScreen. Inactive tabs are still capturable.
  • Windows orphaned by a display disconnect are filtered out. macOS retains surfaces for windows that lived on a removed display; they cannot be captured and are dropped from listings.
  • GIF is converted after recording ends, so it adds wall-clock time proportional to frame count and pixel area — roughly 19s for 5s of a full Retina display at 30fps. Prefer a window target and modest --fps.
  • transcribe is present in the interface but not implemented yet; it exits 1 with a structured not-implemented error.

Recorded GIFs can get large. gifsicle compresses them well:

gifsicle -i /tmp/clip.gif -O3 --colors 64 -o /tmp/clip-small.gif

For agents

macosrec's primary consumer is an autonomous agent: JSON when stdout is not a TTY, a stable exit-code contract, and bounded operations that fail fast rather than hang.

  • skills/macosrec/SKILL.md — a drop-in Agent Skill (Claude Code / Pi / compatible harnesses) documenting the enumerate-then-capture workflow, selectors, and the exit-code contract.
  • AGENTS.md — contributor guidance and the measured ScreenCaptureKit constraints (also read by Claude Code via the CLAUDE.md stub).

Relationship to upstream

A fork of xenodium/macosrec, which is effectively unmaintained. It has since been rewritten and diverges substantially:

  • ScreenCaptureKit throughout, replacing deprecated CGWindowList* APIs
  • Recording via SCRecordingOutput, which writes incrementally and hardware-encoded. The original buffered every frame uncompressed in RAM — a one-minute recording held several GB before writing a byte
  • Subcommands rather than a single command with mutually exclusive flags. The original shape came from its Emacs integration, which this fork does not target
  • JSON output, a documented exit-code contract, display and region targets, bounded record --duration, and non-interactive OCR
  • macOS 15 floor, Swift 6 strict concurrency, and a test suite

Original author: Álvaro Ramírez (xenodium.com).

Licence

GPLv3. See LICENSE. This rewrite remains a derived work of the original.

About

Agent-first macOS screen capture CLI (ScreenCaptureKit): screenshot, record, and OCR windows, displays, and regions — occluded windows capture correctly, no raising or focusing. JSON output, stable exit-code contract.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages