Skip to content

Fix macOS SSH desktop capture with Quartz - #1

Merged
rylena merged 1 commit into
rylena:mainfrom
patp:fix/macos-ssh-quartz-capture
Aug 21, 2026
Merged

Fix macOS SSH desktop capture with Quartz#1
rylena merged 1 commit into
rylena:mainfrom
patp:fix/macos-ssh-quartz-capture

Conversation

@patp

@patp patp commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

The problem

SSHDESK on macOS can be fully installed (venv, forced command, sudoers, Screen Recording, Accessibility) and still fail the moment a client connects:

could not create image from display
Traceback (most recent call last):
  ...
  File ".../PIL/ImageGrab.py", line 56, in grab
    raise subprocess.CalledProcessError(retcode, args)
subprocess.CalledProcessError: Command '['screencapture', '-x', '/tmp/...png']' returned non-zero exit status 1

That is not a missing TCC prompt and not a broken OpenSSH config. sshdesk-server never gets a frame.

Root cause: NativeCapture calls Pillow ImageGrab.grab(). On Darwin, current Pillow does not use CoreGraphics. It shells out to /usr/sbin/screencapture. That CLI has failed from SSH sessions for years, even when the Python process already has Screen Recording. On a headless Mac whose only display is a Screen Sharing virtual display, it also fails under launchctl asuser.

The docs said NativeCapture used “Pillow’s CoreGraphics path.” The code did not.

Second bug, same session: if capture is fixed first, PyObjC 12 then crashes input with AttributeError: AXIsProcessTrusted. That symbol is no longer exported on the Quartz module, so MacOSInput dies before any click/key is injected.

The fix

  • Capture Darwin frames with Quartz CGDisplayCreateImage (already a sshdesk[macos] dependency) inside the TCC-permitted Python process. Do not call screencapture.
  • Resize to CGDisplayPixelsWide × CGDisplayPixelsHigh so mouse coordinates match MacOSInput.
  • Leave Windows on Pillow ImageGrab.
  • Treat Quartz.AXIsProcessTrusted as optional; fall back to the ApplicationServices C API.
  • Tests for bitmap conversion/resize, the Screen Recording error, skipping ImageGrab on Darwin, and the Accessibility fallback.

Verified

Mac Studio, Darwin 25 / macOS 26, Apple M3 Ultra, headless Screen Sharing virtual display, SSHDESK 0.4.5:

  • screencapture -xcould not create image from display (exit 1)
  • CGDisplayCreateImage → real 2642×1640 frame
  • After this change: sshdesk-server --check1321x820 NativeCapture capture; input=enabled
  • sshdesk-agent screenshot returned the live Finder desktop
  • Interactive ssh -t set the title to SSHDESK - Martins-Mac-Studio.local and started drawing

Test plan

python3 -m venv .venv
.venv/bin/python -m pip install -e '.[dev]'
.venv/bin/python -m unittest discover -s tests -v
.venv/bin/ruff check src tests

On a macOS host with Screen Recording + Accessibility granted to the venv Python:

sshdesk-server --check
ssh -t sshdesk@host

Pillow ImageGrab on Darwin shells out to screencapture, which fails from
OpenSSH even when Screen Recording is already granted to Python. Capture
through CGDisplayCreateImage instead, and keep the frame in
CGDisplayPixelsWide/High space so mouse coordinates still match Quartz
input.

PyObjC 12 also stopped exporting AXIsProcessTrusted on the Quartz
module, so Accessibility checks fall back to ApplicationServices.
@rylena

rylena commented Aug 21, 2026

Copy link
Copy Markdown
Owner

lgtm 👍🏾

@rylena
rylena merged commit e314aa5 into rylena:main Aug 21, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants