Operator OS
All pages

Automation

What this layer is / Why it matters

Automation means Claude works while you do not: a briefing waiting at breakfast, a weekly review on Sunday, a backup every night. The pattern is simple: the prompt is a file, and a scheduler runs it. You can read and edit the prompt like any note. The scheduler on macOS is launchd.

The hard part is not the prompt. It is making sure jobs run when your laptop is asleep, and telling you when they quietly stop.

Core (everyone)

What you get

In automation/:

Job What it does When
morning-briefing Calendar, mail that needs you, focus plan once a day
weekly-review Wins, stuck, open loops, next week's top 3 once a week
inbox-triage Sorts mail, drafts replies every 2 hours
backup Zips your work folders, keeps the last 7 nightly
check-heartbeats Warns you if a job stopped hourly
reaper Kills forgotten dev servers every 5 minutes
tmp-cleanup Deletes old scratch files and logs nightly

Install

cd automation
./install-jobs.sh --list              # see what exists
./install-jobs.sh morning-briefing check-heartbeats

It asks before copying and before loading anything. With no keyboard (for example when Claude runs it for you), pass flags: --yes skips every question and loads the job; --yes --no-load copies the scripts and writes the plists but does not touch launchd, so you can review them and load later with the launchctl bootstrap line it prints.

./install-jobs.sh --yes --no-load morning-briefing check-heartbeats

Edit the prompts in ~/.operator-os/prompts/ to match your life, and settings in ~/.operator-os/config.env.

Jobs are read-only by default

run-claude-job.sh gives Claude only Read, Glob, Grep plus the specific connector tools each job needs. Inbox triage can draft but has no send tool. To widen a job, change its tool list on purpose, in config.env. Treat email text as data, never as orders: the triage prompt says so.

Do connectors work in headless mode?

Tested on Claude Code 2.1.x: yes. claude -p loads the claude.ai connectors you are logged in with (Gmail, Google Calendar, Drive...), and the names in the allowlist (mcp__claude_ai_Gmail__search_threads, mcp__claude_ai_Gmail__create_draft, mcp__claude_ai_Google_Calendar__list_events) work as written. Two things to know:

  • --allowedTools only pre-approves tools. It does not remove the others, and your own settings.json allow rules still apply. So run-claude-job.sh also passes --disallowedTools with every send/reply/forward/delete/label/calendar-write/Drive-write tool plus Bash/Write/Edit. Override with OOS_DISALLOWED_TOOLS if you really mean to.
  • Connectors need the claude.ai login (not an API key) and a network. If a connector is missing, the prompts say "if a source is not available, say so and continue", so the job still produces a shorter briefing instead of failing.

Fallback if connectors are unavailable (API-key setups, a connector not authorized for this account): use the gws CLI through integrations/bin/gws-as and allow it explicitly, read-only commands only:

# config.env
OOS_BRIEFING_TOOLS="Read,Glob,Grep,Bash(gws-as default calendar events list:*),Bash(gws-as default gmail users threads list:*)"
OOS_DISALLOWED_TOOLS="Write,Edit"   # Bash must stay allowed for this route, so scope it by pattern as above

Inbox triage should then stay read-only (no draft tool), since drafting through gws needs a write scope.

You will be told about failure

Every job writes a heartbeat file when it succeeds. check-heartbeats raises a macOS notification if one gets old. Without it, a broken job can stay silent for weeks.

Advanced (developers)

Anatomy

  • lib.sh: config loading, logging, wait_for_net, heartbeats, once-a-day stamps.
  • run-claude-job.sh <job> <prompt-file>: waits for the network, runs claude -p "<prompt>" --allowedTools ... with a timeout, saves the output under ~/.operator-os/output/<job>/, logs, writes the heartbeat only on success, and exits with the real code.
  • Plist templates use {{HOME}}; install-jobs.sh renders, lints (plutil -lint) and bootstraps them.

launchctl cheat-sheet

launchctl print gui/$(id -u)/com.operator-os.backup | grep -E "state|last exit"
launchctl kickstart -k gui/$(id -u)/com.operator-os.backup   # test THROUGH launchd
launchctl bootout   gui/$(id -u)/com.operator-os.backup
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.operator-os.backup.plist

Debugging a job

  1. last exit code non-zero? 78 means launchd could not open a file: see lessons below.
  2. Read ~/Library/Logs/operator-os/<job>.log, then .err.
  3. Run it by hand, then via kickstart. Running by hand always works because your terminal has permissions launchd lacks.
  4. claude not found? launchd has a tiny PATH; lib.sh adds common locations.
  5. Check ~/.operator-os/heartbeats/<job>.

Sleep settings (pmset)

sudo pmset -a sleep 0 displaysleep 0 stops idle sleep, but closing the lid still sleeps the Mac. Never idle-sleeping costs heat and battery; disablesleep 1 ignores the lid and runs hot. pmset repeat keeps only one wake schedule; pmset schedule wakeorpoweron can queue several. Pick: leave the lid open on power, or accept catch-up runs. Check both directions with pmset -g.

Lessons from real use

  • DarkWake. With the lid closed on battery, the Mac wakes for seconds with no network, and launchd still fires. Every request failed in under a second and errors looked like auth problems (a DNS failure showed up as 401). Fix: wait_for_net, and "once a day" jobs that fire hourly and record a done-today stamp, so the next hour catches up.
  • Do not hide the exit code. A wrapper ending in echo made launchd report success for weeks. Always exit with the real status.
  • Files in ~/Documents kill jobs silently. Scripts or log paths there make launchd fail with exit 78 before line one, leaving no log. Backups died unnoticed for five weeks. Keep scripts and logs under ~/.operator-os and ~/Library/Logs; let the script cd into your project.
  • Silent failure needs a watchdog. Heartbeats plus check-heartbeats turn "found out a month later" into a notification within the hour.
  • Orphaned dev servers ate all RAM. One session fanned out agents, each starting next dev and tsc: about 30 processes, 24 GB Mac, swap full. reaper.sh kills dev tooling whose owner session is gone. Start Claude with oos-claude so children carry CLAUDE_PID. Optional idle rule: OOS_REAP_IDLE_MIN=20.
  • Agent logs can lie. A job claimed fallback drafts that never existed. Verify the result where it lives.