Welcome to my personal site. In addition to the posts you see below, feel free to explore other places where I stash content here. The now page is an occasionally-updated page about long-term plans and activities. I try to post several times a week and those updates appear on the journal page. Sometimes I post tidbits about things I learn in the til page. More info in: toolkit, readings, to learn, gallery, colophon, and changelog.


As a dual Canadian/U.S. citizen 🇨🇦 🇺🇸, I am opposed to the deeply corrupt and incompetent Administration of the U.S. government and urge Americans to vote all Republicans out of office.

Syncing Hazel rules between macOS computers

Hazel is a filesystem utility application that performs housekeeping activities on macOS directories. When it detects a change in a directory, it executes actions on files there based on matching criteria. That’s the good news. The bad news is that “sync,” in the way Hazel uses the term, means that all Macs share all the rules for a given shared directory. Individual rules cannot be excluded. For a given directory, the same rules run on all Macs that share it. Working around this takes some finagling. But if you’re a Hazel user, you’re probably already the sort that doesn’t mind some hacking. The workaround involves a few steps:

We’ll take each of these in turn.

Establish a common merged set of rules

This is the trickiest part of the process. The goal is to first create two sync files, one for Mac A and one for Mac B. Create these on a shared drive. I used my iCloud Drive, but other synced shares are fine. See Hazel Help for details on setting up rule sync.

Hazel Help page explaining rule sync limitations and the steps to set up a sync file

Initially, I had hoped that .hazelrules files were XML or JSON or the like, because that would facilitate merging rules between computers. But no such luck; it’s more complicated than that. Using xxd, I examined the .hazelrules file structure and found:

Hexadecimal view of a Hazel .hazelrules file showing its HZLR header and binary property list data

So, a .hazelrules file has two layers:

  1. A 40-byte Hazel header: HZLR (4 bytes), a 4-byte field whose value is 27 in these files, and a 32-byte SHA-256 checksum of the remaining bytes. I haven’t established what the 27 field means. Maybe it’s a version; I don’t know.
  2. A binary property list: This is an Apple NSKeyedArchiver archive. Its $top entry points to a HazelRuleSet, which points to an ordered list of HazelRule objects.

Each rule stores its name, identifier, modification date, conditions, actions, and options. Conditions are archived predicate objects; actions include objects such as “move to Trash” or “run shell script.” The archive uses numbered object references (UIDs), so a rule is a graph of linked objects rather than one self-contained block.

This is the point where, if I were more industrious, I would write an application to extract the original object graph and do some sort of intelligent merging. Maybe I will dive into that someday. But for now, I just allowed codex to examine the files and merge them. That way, it could properly repackage the merged files and compute the new checksum. With a little guidance from me—“For rule x, use the version from Computer A,” etc.—codex did a great job of merging.

Codex session prompted to merge two Hazel rule sets into a Desktop.hazelrules file

Once the files are satisfactorily merged, point the sync location for the affected directories on both Computer A and Computer B to the new merged .hazelrules file.

Using the computer name as a criterion

One of the problems with syncing rules between computers is that a given rule may apply to only one of them. Because Hazel syncing is a blunt instrument, there is no way to exclude rules from sync. It’s all or nothing. To circumvent that issue, you can create a criterion for the computer name. For example, if a rule should run only on my laptop, whose hostname is WokeAF.local, I can use hostname -s to act as a gate for the rule. To do this, add a “Passes shell script” criterion to an existing rule that you want to run only on that computer.

Hazel rule preview with a Passes shell script condition that checks the computer hostname

In the “Passes shell script” condition, when the script exits with 0, the criterion succeeds for this file. When the script exits with 1, the criterion fails. Succinctly, it’s:

# Use whatever computer name you want to run the rule
[[ "$(hostname -s | tr '[:upper:]' '[:lower:]')" == "wokeaf" ]]

Edit rule actions per computer

Some rule actions will also need to be modified to provide alternatives per computer. In this case, you must use the “Run shell script” action and provide conditional logic there. For example, my laptop and desktop are named “WokeAF” and “Vyger,” respectively, so I could set up the shell script logic in this way:

COMPUTER="$(hostname -s | tr '[:upper:]' '[:lower:]')"

case "$COMPUTER" in
    WokeAF)
        echo "Running WokeAF-specific actions"
        # commands for WokeAF go here
        ;;

    Vyger)
        echo "Running Vyger-specific actions"
        # commands for Vyger go here
        ;;

    *)
        echo "Unknown computer: $COMPUTER" >&2
        exit 1
        ;;
esac

exit 0

Alternatively, if you prefer not to use “Run shell script” actions and just want to use the built-in actions, you could create duplicate rules with different hostname criteria and different actions.


I wish this were a lot easier. And I wish that I did not have to use an LLM agent to complete the merger. (Though I’m happy it worked so well!) It’s entirely possible there may be an easier way to go about this. If you know of a better method, or if you just have questions or comments, please use my contact page.

Using Obsidian Linter plugin to update YAML modified date

When writing notes for my Obsidian vault, I like to keep track of when it was last modified. While I could certainly do that manually, it seems like something that would be easy to automate. It turns out to be a little difficult; and I’m not on the third iteration of this quest. Over the last few years, I’ve written twice in the blog about keeping the modification date (mdate:) in the YAML frontmatter of my Obsidian notes up to date. I no longer use either of those methods. Instead, I use the Linter plugin, which handles this within the Obsidian environment.

The earlier approaches

In November 2022, I described using fswatch to update note metadata. A shell script watched the vault for filesystem changes, read each changed note’s modification time with stat, and used sed to write that value into its YAML frontmatter. The script also had to filter out temporary files and unrelated events, and it needed to keep running in the background as a launchd user agent.

By May 2023, I was using a Hazel rule to trigger a similar shell script. In my discussion of file creation dates, I discussed a complication: rewriting a note with sed -i changed its filesystem creation date. The workaround used GetFileInfo to save the original creation date and SetFile to restore it after updating the YAML. Those utilities required the Xcode command line tools.

Both approaches put the responsibility for maintaining Obsidian metadata in tools outside Obsidian. There is now a much simpler way to handle this.

Using Linter

In Linter’s settings, open the YAML tab and find YAML Timestamp. Enable Date Modified. To keep using the field from my earlier examples, set Date Modified Key to mdate. The format YYYY-MM-DD HH:mm matches those examples as well. See the YAML timestamp documentation for the available options.

Linter settings with Date Modified enabled, mdate as the YAML key, File system as the source of truth, and YYYY-MM-DD HH:mm as the format
Linter's Date Modified settings, configured to update the mdate field using the file system as the source of truth.

The timestamp rule needs to run for the value to update. You can run Linter manually, or enable Lint on save in its general settings to run it when you explicitly save a note. Linter also offers Update YAML Timestamp on File Contents Update for updating the timestamp after edits to the active note; select an interval rather than Never if you want that behavior. The instructions for running rules explain the available triggers.

For me, the main advantage is that the solution lives inside Obsidian. Aside from the Linter plugin itself, it requires no external resources: no filesystem watcher, Hazel rule, background shell script; and it doesn’t require you to install Xcode and the Xcode Command Line Utilities (which, of course, made one of my solutions macOS-only). Overall, it’s just a lot less to maintain; and with the right sync configuration, the plugins can match-up between machinges.

Usually simpler is better.

If you have questions or comments, please get in touch via my contact page.