macOS dumps screenshots to ~/Desktop as PNG files named Screenshot 2026-04-25 at 10.30.00 AM.png. That's fine — until you're uploading them to a web form that wants JPG, or a design tool that renders WebP faster. Converting them manually is exactly the kind of task you automate once and forget about forever.

Two approaches work here: a launchd WatchPaths agent (the clean, shell-native approach) and a Folder Action (GUI-scriptable via Automator/Script Editor). Both detect new files within seconds. We'll cover the launchd path in depth because it's more reliable and scriptable, then show the Folder Action variant as an alternative.

TL;DR

A launchd agent watches ~/Desktop via WatchPaths. When the path modification time changes, it fires a shell script that finds new PNGs, POSTs them to the API, saves the converted file alongside the original, then marks them done. Load it with launchctl load and it survives reboots.

# One-time setup
cp com.ctf.screenshot-convert.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.ctf.screenshot-convert.plist

The use case

The typical trigger: you take a screenshot for a bug report or design review, and the destination (Jira, Notion, a CMS upload field) expects JPG or WebP. You end up with a PNG sitting on your Desktop that you either convert manually or attach as-is and let the other party deal with it.

With this setup:

  • You press Cmd+Shift+3 or Cmd+Shift+4
  • The PNG lands on your Desktop
  • Within 5 seconds, a .jpg (or .webp) version appears next to it
  • The original PNG is untouched

Common variants:

  • Screenshot-heavy devs who attach images to GitHub issues or Jira tickets (JPG preferred for size)
  • Designers who share screenshots via Slack (WebP loads faster in browser clients)
  • Content teams who ingest screenshots into CMS tools that strip EXIF but choke on PNG alpha

The ChangeThisFile API accepts PNG and auto-detects source format from the filename, so no source parameter is needed — just specify target=jpg or target=webp.

Working script and plist

Two files: the conversion script and the launchd plist.

1. The conversion script — save to ~/bin/ctf-screenshot-watch.sh and chmod +x:

#!/usr/bin/env bash
# ~/bin/ctf-screenshot-watch.sh
# Fired by launchd WatchPaths when ~/Desktop changes.
# Converts new PNG screenshots to JPG, preserves originals.

set -euo pipefail

API_KEY="${CTF_API_KEY:?CTF_API_KEY not set}"
WATCH_DIR="${HOME}/Desktop"
TARGET_FORMAT="${CTF_TARGET_FORMAT:-jpg}"  # or: webp
API_URL="https://changethisfile.com/v1/convert"
DONE_LOG="${HOME}/.ctf-screenshots-done"
LOG_FILE="${HOME}/Library/Logs/ctf-screenshot-watch.log"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"; }

touch "$DONE_LOG"

shopt -s nullglob
for f in "$WATCH_DIR"/*.png "$WATCH_DIR"/*.PNG; do
  [[ -f "$f" ]] || continue
  filename=$(basename "$f")

  # Only process files whose name starts with "Screenshot"
  [[ "$filename" == Screenshot* ]] || continue

  # Skip already-processed files
  grep -qF "$filename" "$DONE_LOG" && continue

  stem="${filename%.*}"
  out_file="$WATCH_DIR/${stem}.${TARGET_FORMAT}"

  log "Converting: $filename -> ${stem}.${TARGET_FORMAT}"

  http_status=$(curl -sf \
    --max-time 60 \
    -w "%{http_code}" \
    -o "$out_file" \
    -H "Authorization: Bearer $API_KEY" \
    -F "file=@$f" \
    -F "target=$TARGET_FORMAT" \
    "$API_URL") || {
    log "ERROR: curl failed for $filename"
    continue
  }

  if [[ "$http_status" == "200" ]]; then
    echo "$filename" >> "$DONE_LOG"
    log "OK: $filename (output: $(du -h "$out_file" | cut -f1))"
  else
    rm -f "$out_file"
    log "ERROR: HTTP $http_status for $filename"
  fi
done

2. The launchd plist — save to ~/Library/LaunchAgents/com.ctf.screenshot-convert.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.ctf.screenshot-convert</string>

  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>/Users/YOUR_USERNAME/bin/ctf-screenshot-watch.sh</string>
  </array>

  <!-- Fire when ~/Desktop modification time changes -->
  <key>WatchPaths</key>
  <array>
    <string>/Users/YOUR_USERNAME/Desktop</string>
  </array>

  <!-- Pass API key as environment variable -->
  <key>EnvironmentVariables</key>
  <dict>
    <key>CTF_API_KEY</key>
    <string>ctf_sk_your_key_here</string>
    <key>CTF_TARGET_FORMAT</key>
    <string>jpg</string>
  </dict>

  <key>StandardOutPath</key>
  <string>/Users/YOUR_USERNAME/Library/Logs/ctf-screenshot-watch.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/YOUR_USERNAME/Library/Logs/ctf-screenshot-watch-err.log</string>

  <key>RunAtLoad</key>
  <false/>
</dict>
</plist>

Replace YOUR_USERNAME with your actual macOS username (whoami). Then load:

launchctl load ~/Library/LaunchAgents/com.ctf.screenshot-convert.plist

# Verify it's loaded
launchctl list | grep ctf

Folder Action alternative — if you prefer a GUI approach, open Script Editor and attach this to ~/Desktop:

on adding folder items to this_folder after receiving added_items
  repeat with item_ref in added_items
    set item_path to POSIX path of (item_ref as alias)
    set item_name to name of (info for item_ref)
    if item_name starts with "Screenshot" and item_name ends with ".png" then
      do shell script "/Users/YOUR_USERNAME/bin/ctf-screenshot-watch.sh"
    end if
  end repeat
end adding folder items to

Right-click ~/Desktop in Finder → ServicesFolder Actions Setup to attach the script. The launchd approach is more reliable for high-frequency saves.

Error handling and edge cases

Several macOS-specific edge cases are worth handling explicitly:

Partial writes. macOS writes screenshots atomically via a temp file rename, so by the time WatchPaths fires, the PNG is complete. But if you're watching a folder where other apps write non-atomically, add a size-stability check:

# Wait for file size to stabilize (non-atomic writers)
wait_for_stable() {
  local f="$1"
  local prev_size=0
  local cur_size
  for _ in 1 2 3 4 5; do
    cur_size=$(stat -f%z "$f" 2>/dev/null || echo 0)
    [[ "$cur_size" -eq "$prev_size" ]] && [[ "$cur_size" -gt 0 ]] && return 0
    prev_size=$cur_size
    sleep 0.5
  done
  return 1  # File never stabilized
}

WatchPaths fires for any Desktop change. If you save a non-screenshot file to Desktop, the script runs but the Screenshot* glob filter means it's a no-op. The done log prevents double-conversion if WatchPaths fires twice in quick succession (it can).

Sleep/wake behavior. launchd agents survive sleep. After wake, if you took screenshots while the machine was sleeping (unlikely but possible with screen recording tools), the WatchPaths event fires on next filesystem access and catches them.

Log rotation. The log file at ~/Library/Logs/ctf-screenshot-watch.log grows unbounded. Add a simple rotation check to the script head:

# Rotate log if over 5MB
if [[ -f "$LOG_FILE" ]] && [[ $(stat -f%z "$LOG_FILE") -gt 5242880 ]]; then
  mv "$LOG_FILE" "${LOG_FILE}.1"
fi

API key security. The plist contains your API key in plaintext. macOS launchd plists in ~/Library/LaunchAgents/ are readable only by your user account, so the exposure is limited. For a more hardened setup, store the key in Keychain and retrieve it with security find-generic-password:

# Store key in Keychain (one-time)
security add-generic-password -a ctf -s ctf-api-key -w ctf_sk_your_key_here

# Retrieve in script
API_KEY=$(security find-generic-password -a ctf -s ctf-api-key -w)

launchd vs Folder Actions: which to use

launchd WatchPaths is the right choice for most engineers:

Propertylaunchd WatchPathsFolder Actions
ReliabilityHigh — survives Finder restartsModerate — breaks if Finder crashes
Trigger latency~1-3 seconds~2-5 seconds
DebuggabilityStructured log filesAppleScript error dialogs
Shell accessFull bash scriptdo shell script (limited)
Multiple foldersMultiple WatchPaths entriesSeparate scripts per folder

To watch additional folders (e.g., ~/Downloads for downloaded images), add more <string> entries to the WatchPaths array and update the script's WATCH_DIR to iterate multiple paths.

Reload after any plist change:

launchctl unload ~/Library/LaunchAgents/com.ctf.screenshot-convert.plist
launchctl load   ~/Library/LaunchAgents/com.ctf.screenshot-convert.plist

Check if it's running:

launchctl list com.ctf.screenshot-convert
# "LastExitStatus" = 0 means last run was clean

Production tips

  • Change the screenshot save location. macOS Ventura+ lets you set a custom screenshot folder in Cmd+Shift+5 → Options → Save to. Point it to ~/Screenshots/ instead of Desktop to keep your Desktop clean and the WatchPaths target unambiguous.
  • Target WebP for Slack/Notion. Both render WebP natively in 2026. A 2MB PNG screenshot often compresses to 200-400KB as WebP with no visible quality loss — 5-10x smaller attachments.
  • Free tier covers 1,000 conversions/month. The average engineer takes 15-30 screenshots/day that warrant conversion. At 20/day, that's ~600/month — within the free tier. Heavy screenshot workflows (QA testers, technical writers) should consider the $29/mo plan for 10K conversions.
  • Use launchctl kickstart for testing. Rather than taking a fake screenshot to test, force a run: launchctl kickstart -k gui/$(id -u)/com.ctf.screenshot-convert. The -k flag kills any running instance first.
  • Preserve originals intentionally. The script never deletes the source PNG. This is the right default — the converted JPG is a derivative, not a replacement. If disk space is a concern, add an archive step that moves originals to ~/Screenshots/originals/ after 7 days rather than deleting immediately.

A launchd WatchPaths agent is the cleanest way to hook into macOS filesystem events without a background daemon or polling loop. Combined with a 60-line bash script and the ChangeThisFile API, you get automatic screenshot conversion that fires within seconds, preserves originals, and survives reboots. Get a free API key — 1,000 conversions/month at no cost, enough for most screenshot workflows.