Finding new screenshots, and the macOS 27 custom folder bug

A folder watcher, a one second poll, a capture tag and a preference key that moved. How a notch app notices a screenshot, and why a custom folder broke it.

By Arin Prajapati 9 min read
Black cover with the NotchStar logo, the kicker macOS 27 and the headline Finding new screenshots

TL;DR

  • To put a new screenshot in the notch, an app has to know which folder macOS saves into, notice a new file, decide it is a real capture, and wait until it has bytes. NotchStar does this with a folder event, a one second poll, an extended attribute and a name list.
  • In version 1.0.5 (12 October 2026) the changelog lists a fix: on macOS 27 with a custom screenshot folder, screenshots never reached the notch. The code now reads a second preference key, location-screenshot, besides the old location.
  • The first version of the watcher is in git and shows what was read before: only location. The macOS 27 behaviour itself is taken from the code comment and the changelog; it could not be reproduced on the Mac used for this post, which has no custom folder set.

The problem

NotchStar hangs screenshots in a strip under the notch, up to 12 of them, each for 5 minutes unless used. For that, the app needs a stream of “a new screenshot exists at this path” events, with no Screen Recording permission and no private API. It watches the folder macOS writes to.

That sounds like one line of code and it is five questions:

QuestionWhere it is answered
Which folder does macOS save intoScreenshotWatcher.systemFolder() and ScreenshotScan.folder
Did something new appeara file system event plus a poll
Is the new file a screenshotthe capture tag, the folder rule, a name list
Is the file finisheda size check
Did the user change the folder meanwhilethe poll compares the folder each time

Which folder: the preference and the key that moved

macOS keeps the screenshot settings in the com.apple.screencapture preferences domain. The folder is a string value there. The first version of the watcher, committed on 11 October 2026, read it like this:

let configured = UserDefaults.standard.persistentDomain(forName: "com.apple.screencapture")?["location"] as? String

On 12 October 2026, release 1.0.5 changed the reading. The comment on the folder function in ScreenshotScan.swift states the finding: macOS 27 keeps the folder in location-screenshot and ignores the old location, while earlier versions only have location. The commit message for the release says the same: the watcher reads location-screenshot (macOS 27) and location. The user visible line in the changelog is “Fixed: on macOS 27 with a custom screenshot folder, screenshots never reached the notch.”

The consequence is easy to see from the code. If the key your app reads is not the key the system writes, the answer is “not set”, and a not set folder means the default, which is the Desktop. A user with screenshots going to ~/Pictures/Screenshots would have an app quietly watching the wrong place. There is no error, because nothing failed.

The current reader asks the preferences system for both keys and takes the first one that is not blank:

static func configuredLocation() -> String? {
    let domain = "com.apple.screencapture" as CFString
    CFPreferencesAppSynchronize(domain)
    let modern = CFPreferencesCopyAppValue("location-screenshot" as CFString, domain) as? String
    let legacy = CFPreferencesCopyAppValue("location" as CFString, domain) as? String
    return [modern, legacy].compactMap { $0 }.first { !$0.trimmingCharacters(in: .whitespaces).isEmpty }
}

Two details deserve a sentence each. The new key comes first, so a Mac that has both uses the modern one. And CFPreferencesAppSynchronize runs before the read, because the user can change the folder at any time in the Screenshot app’s options and the app must see the current value, not a cached one.

What the Mac used for this post shows, recorded on 4 November 2026 (macOS 27.0.1, build 26A434), from defaults read com.apple.screencapture: the domain exists with the keys show-thumbnail, style, target-screenrecording, target-screenshot, video and last-analytics-stamp. Asking for location and for location-screenshot each answered that the key could not be found, because no custom folder is set here. So the new keys can be seen, but the claim that macOS 27 writes the folder to location-screenshot is the project’s, and was not reproduced for this post.

The rest of the folder logic is a function with tests. A blank value means Desktop, ~ and ~/ are expanded against the home folder, and if the folder does not exist or is not a directory, systemFolder() falls back to the Desktop, “the same way macOS falls back”. An environment variable, NOTCHSTAR_SCREENSHOT_DIR, overrides everything and is how tests point the watcher at a temporary folder.

XCTAssertEqual(ScreenshotScan.folder(home: home, configured: nil).path, "/Users/ada/Desktop")
XCTAssertEqual(ScreenshotScan.folder(home: home, configured: "~/Pictures/Screenshots").path, "/Users/ada/Pictures/Screenshots")

Noticing a new file: an event and a poll

The watcher uses two mechanisms together, and the doc comment says why: the system tells the app the moment the folder changes, and a slow poll also runs, to follow the folder if the user moves it.

The event is a dispatch source on the folder with .write and .rename:

let fd = await Task.detached(priority: .utility) { open(dir.path, O_EVTONLY) }.value
guard fd >= 0 else { return }
let src = DispatchSource.makeFileSystemObjectSource(fileDescriptor: fd, eventMask: [.write, .rename], queue: .main)
src.setEventHandler { [weak self] in
    Task { @MainActor [weak self] in
        try? await Task.sleep(for: .milliseconds(150))
        await self?.poll()
    }
}

open happens off the main thread because it can block while macOS asks permission for Desktop access. The handler waits 150 milliseconds before scanning, and the comment says the reason: macOS writes a hidden file and renames it.

That claim was checked with a small standalone program on 4 November 2026. It watched a temporary folder, wrote a hidden .Screenshot.png.tmp and renamed it to Screenshot test.png. The event handler fired twice for the one capture, about 0.5 ms apart in the first run (0.50 and 0.52 ms) and 1.5 ms apart in the second (1.48 and 1.54 ms), measured from just before the write. Only the renamed file was visible to a directory listing that skips hidden files. One capture, two events: that is the case for waiting a moment and for scanning in a way that tolerates being called twice.

The scanning function does that. poll() guards against overlapping runs with two flags, scanning and rescan, so a second trigger during a scan marks a repeat and is not run in parallel:

func poll() async {
    if scanning { rescan = true; return }
    scanning = true
    defer { scanning = false }
    repeat {
        rescan = false
        // list the folder, compare with `known`, admit new captures
    } while rescan
}

The poll itself runs every second (pollInterval is .seconds(1)), even without events. If the folder path has changed since the last pass, the watcher adopts the new folder, takes a fresh listing as the baseline and restarts the event source on it. The first listing, at startup, also only remembers what is already there, so old screenshots do not flood the strip.

The earlier shape of the watcher shows what the event source replaced. The version committed on 11 October 2026 polled every 400 milliseconds, with no event source, and on a folder change it only took a new baseline and returned. Release 1.0.5 lengthened the poll to one second and added the event for speed. Fewer wake ups and a faster reaction came from the same change, though neither was measured as a number.

Is it a screenshot: three tests in order

Listing the Desktop gives every file on it, not just captures. ScreenshotScan.scan decides for each new path:

  1. Not an image extension (png, jpg, jpeg, heic, tif, tiff): remember it and move on.
  2. If the folder is dedicated, meaning its name is Screenshots, any image counts. Otherwise the file must be tagged as a capture or have a name that looks like one.
  3. If the file has zero bytes, do not remember it; look again next pass.

The tag is an extended attribute that macOS sets on captures, whatever the file is called:

static func isTaggedCapture(_ url: URL) -> Bool {
    url.withUnsafeFileSystemRepresentation { path in
        guard let path else { return false }
        return getxattr(path, "com.apple.metadata:kMDItemIsScreenCapture", nil, 0, 0, 0) >= 0
    }
}

It works in every language and survives a rename. The name list is the fallback: 8 prefixes, compared case insensitively, including screenshot, screen shot, bildschirmfoto, captura de pantalla, schermata and skärmavbild. The tests cover both ways of failing: Screenshot.txt and notes.png on the Desktop are remembered and never admitted, an empty file is admitted only once it has 40,000 bytes in the test, and an already seen file stays off the strip.

Numbers and method

MeasureValueMethod
Poll interval1 spollInterval = .seconds(1) in ScreenshotWatcher.swift
Wait after a folder event150 msTask.sleep(for: .milliseconds(150))
Events for one hidden write plus rename2standalone swiftc program, 4 November 2026, run twice
Gap between the two events0.02 ms and 0.06 mssame program, from the printed values
Name prefixes8namePrefixes in ScreenshotScan.swift
Strip capacity and lifetime12 items, 5 minutesproject facts, ScreenshotTray.swift
OS and build usedmacOS 27.0.1, 26A434sw_vers, 4 November 2026
Keys found on this Macshow-thumbnail, style, target-screenrecording, target-screenshot, video, last-analytics-stampdefaults read com.apple.screencapture, 4 November 2026

No timing is claimed for how long the whole path takes from pressing the shortcut to the strip. That depends on macOS saving the file, and the preview setting matters: with the floating thumbnail on, the file may not be saved for about five seconds. NotchStar’s optional “Show screenshots instantly” turns that preview off, saves the user’s own value first and puts it back when the app quits.

What is still bad

  • The macOS 27 key behaviour rests on the project’s own finding and was not reproduced for this post. The Mac used here has no custom folder.
  • A future macOS can rename the key again. The code has no way to detect that except a user report.
  • The Desktop case depends on the capture tag or the name list. A localisation whose prefix is not in the list, with the tag missing, would not be recognised on the Desktop. In a folder called Screenshots any image is accepted, including ones that are not screenshots.
  • The dedicated folder rule is a name match on Screenshots. A custom folder with another name is treated like the Desktop.
  • The poll runs every second for as long as the watcher is on. Its energy cost has not been measured.
  • Listing a folder can be slow when it is a cloud synced Desktop. The code lists off the main thread for that reason, and a slow listing still delays admission.

Try it yourself

  1. Run defaults read com.apple.screencapture and note which keys exist on your Mac.
  2. Change the save location in the Screenshot app options, and read the domain again. See which key changed. Put it back afterwards.
  3. In a scratch program, open the folder with O_EVTONLY and attach a dispatch source for .write and .rename. Take a screenshot and count the events.
  4. On a captured file, run xattr -l on it and look for com.apple.metadata:kMDItemIsScreenCapture.
  5. Rename the file and check whether the attribute is still there.

Cheat sheet

NeedDo this
Find the save folderRead location-screenshot, then location, then use the Desktop
Get a fresh valueCall CFPreferencesAppSynchronize first
React quicklyA dispatch source on the folder with .write and .rename
Survive missed events and folder changesAlso poll, and compare the folder each time
Avoid half written filesWait about 150 ms after an event and skip zero byte files
Recognise a capture in any languageThe kMDItemIsScreenCapture attribute, then a name list
Stay off the main threadList and open in a detached utility task

Close

NotchStar is a Mac app that puts ten tools in the notch, and this watcher feeds its screenshot strip. If you want to try it, the details are at notchstar.com.

NotchStar is a native Mac notch app, $4 once, with a 7 day free trial. Try it, or read the ten tools.