macOS is the platform. Raycast, Hammerspoon, Hazel, launchd, and the rest are the runtime. Build the personal OS that runs your day — without you operating it.
The personal OS isn't one app. It's a stack. Capture flows in. Automation shapes it. Memory holds it. Triggers fire it. Output ships it. Each layer is one or two tools, no more.
If capturing costs a click, you won't capture. The capture layer has to be reachable in under a second, from anywhere, with zero modal flow.
The automation layer is the difference between a productivity stack and a job. Every repeated action is a workflow that should run without you.
Notion is a database someone else owns. The memory layer is plain markdown, on your filesystem, queryable with rg, port-out in 60 seconds.
A trigger is anything that wakes a workflow. A clock. A file landing. A USB plug. The trigger layer is the wiring between the world and your automations.
The output layer is the part most people skip. They capture, they automate, they forget to ship. The output layer is a one-way function: local artifact → deployed URL → indexed by Google.
Most "productivity advice" optimizes the wrong layer. They teach you to plan better, journal harder, wake up earlier. None of it scales. The personal OS scales because each script runs forever.
cron on macOS is technically there, but launchd is the right answer. It respects sleep/wake, runs missed jobs on resume, and lives at ~/Library/LaunchAgents/ (the tilde expands to $HOME, the standard Unix convention). Three plist files, and your day runs itself.
# ~/Library/LaunchAgents/com.kai.morning.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.kai.morning</string> <key>ProgramArguments</key> <array> <string>/bin/zsh</string> <string>-c</string> <string>~/bin/morning.sh</string> </array> <key>StartCalendarInterval</key> <dict> <key>Hour</key><integer>6</integer> <key>Minute</key><integer>30</integer> </dict> <key>RunAtLoad</key><true/> </dict> </plist> # ~/bin/morning.sh #!/bin/zsh # 6:30am. Lights, music, inbox triaged, day planned. osascript -e 'tell application "System Events" to set volume 5' # gentle wake volume osascript -e 'tell application "Music" to play playlist "Morning"' hs -A -c "lightMode('sunrise')" # Hammerspoon: warm lights via Hue ~/bin/triage-inbox.sh # labels, archives, surfaces 3 must-reads ~/bin/draft-daily-note.sh # Obsidian daily note pre-populated terminal-notifier -message "Day is set. Top 3 ready." # desktop notification
A worked example: the weekly review that used to take 4 hours and now takes 20 minutes, because the system pre-drafts the agenda and ships the report.
A launchd job fires. ~/bin/weekly-review.sh runs. It pulls last week's git activity, last week's calendar, last week's inbox labels, and last week's Obsidian daily notes into one markdown file.
An Obsidian weekly note appears with: shipped-this-week, parked-this-week, replied-this-week, replied-but-stuck, meetings-that-shipped, meetings-that-didn't. Plus three open threads the LLM thinks need attention.
# ~/bin/weekly-review.sh (excerpt) # 1. Pull the last 7 days of git gh pr list --state merged --search "merged:>=$(date -v-7d +%Y-%m-%d)" \ --json number,title,mergedAt \ | jq -r '.[] | "- #\(.number) \(.title)"' >> /tmp/shipped.md # 2. Pull the last 7 days of calendar icalBuddy -n -ec "Meetings" -ps "/Time Zone/⏰/🔁" -b "" \ -eep "notes,location" eventsFrom:$(date -v-7d +%Y-%m-%d) to:$(date +%Y-%m-%d) \ >> /tmp/meetings.md # 3. Pull the last 7 days of Obsidian daily notes rg -l '^# Daily' ~/Notes/daily/*.md -t markdown | tail -7 \ | xargs cat >> /tmp/journal.md # 4. LLM synthesizes the threads cat /tmp/shipped.md /tmp/meetings.md /tmp/journal.md \ | llm -m claude-haiku-4-5 \ -s "Pull 3 open threads from this week, 3 dropped balls, 3 wins." \ > ~/Notes/weekly/$(date +%Y-%m-%d)-review.md # 5. Open the note in Obsidian, ready for the user to scan open -a Obsidian "~/Notes/weekly/$(date +%Y-%m-%d)-review.md" Total runtime: 38 seconds. The user reads, adds 2 minutes of edits, ships.
Cross-platform picks for the same architecture. The layers don't change. The bindings do.
Nine stacks. One philosophy. Zero babysitting.