Watching for File Changes on macOS

subset1 pts0 comments

Watching for file changes on macOS – alexwlchanSkip to main contentWatching for file changes on macOS<br>Posted 11 May 2026<br>Also filed in Swift, Blogging about blogging<br>When I’m working on this website, I want a local server with live reload. I want to be able to open the site in my web browser, make changes to the source files, and have my browser automatically refresh the page when the site is updated. I use this whenever I’m working on the site, and I find it helpful to see my writing in a different font/layout to my text editor; I spot lots of typos and mistakes that way.<br>When I was using Jekyll, I used the command jekyll serve --livereload. Now I’ve written my own static site generator, I need to build my own version. This was a fun challenge, because it touched a number of areas I’ve not worked in before – macOS filesystem events, non-blocking I/O, and HTTP long polling.<br>In this post I’ll explain how I detect changes to source files to trigger a rebuild; in my next post I’ll explain how that automatically refreshes any open pages in my browser. First we’re going to build a Swift script that detects changes using the FSEvents API, then we’ll get that information into a Python script.<br>The macOS FSEvents API<br>Setting up the event stream<br>There are several ways to detect changes to files on macOS; I’m going to use the File System Events API (also called “FSEvents” for short). This allows you to receive notifications about any changes to a directory tree, or files within it. One of the main purposes of this API is to allow backup software to detect incremental changes without continuously rescanning an entire tree, but we can use it for other things.<br>Apple has a File System Events Programming Guide which explains the FSEvents API in detail, and that it’s exactly what I need: “The file system events API is designed for passively monitoring a large tree of files for changes”. It mentions a couple of alternatives – kernel extensions for getting immediate notifications and pre-empting file changes, or kqueues for monitoring changes to a single file – but they’re not what I need, so I didn’t explore them further.<br>The guide is a little outdated, but the broad strokes are still correct. In Using the File System Events API, it explains the lifecycle of a file system events stream: create a stream, start listening, receive notifications, trigger a callback you provide, stop listening, release the stream.<br>Let’s start with a script that prints a static message whenever it sees a change – we don’t care about what file it was yet, for now we just want to know when any file changed.<br>Here are the steps:<br>Create a file systems event stream using FSEventStreamCreate. This function takes a lot of arguments and you can’t use named arguments, so I found it helpful to define each argument as a variable, then pass those variables into the function. I wrapped my FSEventStreamCreate call in another function:<br>import Foundation

/// Create a new file system events stream that watches for changes<br>/// in the given directories.<br>func createFSEventStream(_ pathsToWatch: [String]) -> FSEventStreamRef {<br>let callback: FSEventStreamCallback = { (_, _, _, _, _, _) in<br>print("Detected file change!")

// Flush stdout to ensure it's printed immediately<br>fflush(stdout)

let context: UnsafeMutablePointerFSEventStreamContext>? = nil<br>let sinceWhen = FSEventStreamEventId(kFSEventStreamEventIdSinceNow)<br>let latency = 0.01<br>let flags = FSEventStreamCreateFlags()

guard let eventStream = FSEventStreamCreate(<br>kCFAllocatorDefault, callback, context, pathsToWatch as CFArray, sinceWhen, latency, flags<br>) else {<br>fatalError("Failed to create FSEventStream: check your paths or permissions.")

return eventStream<br>} The callback function is an instance of FSEventStreamCallback, which will be called whenever a file changes. The arguments contain information about the file which just changed. For now we ignore all of that information, and just print a static message.<br>The context argument allows us to attach some context to the stream. I’m not sure what it’s for – perhaps for applications that have multiple event streams, and need to distinguish between them in the callback? I don’t think I need this, and the docs say I can pass NULL, so that’s what I’ve done.<br>The sinceWhen argument asks for events that happened after a given event ID. I imagine this is useful for long-running applications like backup software – it means they can resume an event stream if the app is quit and relaunched, without rescanning the tree on every app launch. I just need events from when the script started running, so I can use the kFSEventStreamEventIdSinceNow constant.<br>The latency argument is how long the OS will wait to coalesce rapid-fire events into a single event. A shorter latency means you get notifications faster, but you’ll get more of them. I’ll implement my own event coalescing later, so I set this quite low and accept the stream.<br>The flags modify the behaviour of the event stream. We’re...

file changes events stream event macos

Related Articles