DEV Community

Chris
Chris

Posted on Originally published at deadcatfound.org

How to Run Claude Code as a Background Worker on Your Mac

I run an AI trading desk on a Mac Mini: eight agents and scheduled jobs around the clock, all on a Claude subscription with no API key. It took me a few months of 3am breakages to get Claude Code working while I'm away from the keyboard. Below is the setup that held, plus the failures, so you can skip them.

Claude Code has a non-interactive mode (claude -p "prompt") that prints an answer and exits. You wrap it in a loop, let macOS keep the loop alive, and you get a worker on the subscription you already pay for.

1. Log in once, in a real terminal

Headless runs reuse the login from your normal session. Open Terminal and run:

claude auth login
claude -p "say OK"
Enter fullscreen mode Exit fullscreen mode

If the second command prints OK, background runs will work too. If it asks you to log in, fix that first. A background run can't answer a login prompt and will fail with nothing on screen.

2. Write the worker loop

This version watches a folder. Drop a .txt file with a prompt into ~/worker/inbox and the answer shows up in ~/worker/done. You can swap the folder for an API call or a database later and keep the same loop.

#!/bin/bash
# ~/worker/worker.sh
IN="$HOME/worker/inbox"; OUT="$HOME/worker/done"; LOG="$HOME/worker/worker.log"
mkdir -p "$IN" "$OUT"

while true; do
  for job in "$IN"/*.txt; do
    [ -e "$job" ] || continue
    name=$(basename "$job" .txt)
    echo "$(date) start $name" >> "$LOG"
    if out=$(claude -p "$(cat "$job")" < /dev/null 2>&1); then
      printf '%s\n' "$out" > "$OUT/$name.md"
      rm "$job"
    else
      echo "$(date) FAIL $name: $out" >> "$LOG"
      case "$out" in
        *"limit"*) sleep 1800 ;;   # usage cap: wait it out, retry later
      esac
    fi
  done
  sleep 30
done
Enter fullscreen mode Exit fullscreen mode
chmod +x ~/worker/worker.sh
Enter fullscreen mode Exit fullscreen mode

The script deletes a job file only after a successful run. A failed job stays in the inbox and the loop retries it on the next pass.

3. Keep it alive with launchd

Close a terminal tab and your loop dies with it. launchd, the macOS service manager, can start your script at login and restart it if it crashes. Save this as ~/Library/LaunchAgents/com.you.claude-worker.plist and change YOU to your username:

<?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.you.claude-worker</string>
  <key>ProgramArguments</key>
  <array><string>/bin/bash</string><string>/Users/YOU/worker/worker.sh</string></array>
  <key>EnvironmentVariables</key>
  <dict><key>PATH</key>
    <string>/Users/YOU/.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string></dict>
  <key>RunAtLoad</key><true/>
  <key>KeepAlive</key><true/>
  <key>StandardErrorPath</key><string>/Users/YOU/worker/stderr.log</string>
</dict></plist>
Enter fullscreen mode Exit fullscreen mode
launchctl load ~/Library/LaunchAgents/com.you.claude-worker.plist
echo "Write a haiku about cron jobs" > ~/worker/inbox/test.txt
# wait ~30s
cat ~/worker/done/test.md
Enter fullscreen mode Exit fullscreen mode

4. The four things that break it

PATH. launchd starts your script with a bare PATH, so the first error you'll likely hit is claude: command not found. Run which claude in Terminal and make sure that folder is in the plist's PATH line.

Auth expiry. Your login expires after a while, and a power outage can leave it stale. After that each run fails with a 401 until you log in again in a real terminal. The worker can't do that step for you. Check once an hour with claude -p "say OK" and send yourself an alert if it doesn't come back OK.

Usage caps. Your subscription has a session limit and a weekly limit, and the worker shares both with you. Read the error before you alert on it. My setup fired "auth failing" alarms for four hours one afternoon when I had only hit the session cap. When you see a limit message, have the worker wait and retry.

Sleep. Your Mac runs nothing while it sleeps. On a desktop, set it to never sleep in System Settings or run the worker under caffeinate -s. A laptop with the lid closed stops too, unless you plug it into power and an external display.

Watch your burn. If your agents can create their own follow-up work, they can eat a weekly limit in days. Mine used 11% of my week in a single day because each job queued "monitor" and "finalize" jobs of its own. I fixed it two ways: each job has to finish as done or blocked, and no job may queue new work for itself.

Where to take it next

Swap the inbox folder for a real queue. A small web service hands out jobs, and the worker on your Mac claims them with a token, runs them on your subscription, and posts the results back. You never hand the service an API key. If your Mac is off, the jobs wait, so show an "offline" badge to whoever is waiting on them.


I built The Floor on this pattern: a trading ops desk where the hosted side queues work and a small runner on your machine does it with Claude Code, Codex, Gemini CLI or Ollama. Paper trading only. Demo here.

Originally posted at deadcatfound.org.

Top comments (0)