Skip to content

macOS Setup

This guide covers running PizzaPi as a persistent background service on macOS so the runner starts automatically on login, survives reboots, and has access to the macOS keychain for gh, SSH keys, and other credentials.


macOS has two ways to run background services via launchd:

TypeLocationRuns asKeychain accessGUI access
LaunchAgent~/Library/LaunchAgents/Current user, inside login session✅ Yes✅ Yes
LaunchDaemon/Library/LaunchDaemons/System-level (can specify user)❌ No❌ No

Always use a LaunchAgent for PizzaPi. The runner needs keychain access for:

  • gh CLI — GitHub authentication tokens are stored in the login keychain
  • SSH keys — if your keys use a passphrase managed by the macOS keychain agent
  • API credentials — any tool that stores secrets via security add-generic-password

A LaunchDaemon runs outside your login session, so the keychain is locked and inaccessible — any tool that needs it (like gh auth) will fail with errSecInteractionNotAllowed.


  1. If you haven’t already, install the CLI and run setup:

    Terminal window
    npm install -g @pizzapi/pizza
    pizzapi setup

    See the Quick Setup guide if this is your first time.

  2. /Users/yourname/.bun/bin/bun
    which bun
    which pizza
    # /Users/yourname/.bun/bin/pizza

    Note these paths — you’ll need them for the plist file.

  3. Terminal window
    mkdir -p ~/Library/LaunchAgents

    Create ~/Library/LaunchAgents/com.pizzapi.runner.plist with the following content. Replace the paths and environment variables with your own:

    <?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.pizzapi.runner</string>
    <key>ProgramArguments</key>
    <array>
    <string>/Users/yourname/.bun/bin/bun</string>
    <string>/Users/yourname/.bun/bin/pizza</string>
    <string>runner</string>
    </array>
    <key>EnvironmentVariables</key>
    <dict>
    <key>PIZZAPI_API_KEY</key>
    <string>your-api-key-here</string>
    <key>PIZZAPI_RELAY_URL</key>
    <string>https://your-relay-server.example.com/</string>
    <key>PATH</key>
    <string>/Users/yourname/.bun/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
    </dict>
    <key>WorkingDirectory</key>
    <string>/Users/yourname</string>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/Users/yourname/.pizzapi/logs/runner.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/yourname/.pizzapi/logs/runner-error.log</string>
    </dict>
    </plist>
  4. Terminal window
    mkdir -p ~/.pizzapi/logs
  5. Terminal window
    launchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plist
  6. Terminal window
    launchctl list | grep pizzapi
    # 12345 0 com.pizzapi.runner
    # Check the logs
    tail -20 ~/.pizzapi/logs/runner.log

    A 0 in the second column means the service started successfully. The runner should now appear in your relay’s web UI.


Terminal window
launchctl unload ~/Library/LaunchAgents/com.pizzapi.runner.plist
Terminal window
launchctl unload ~/Library/LaunchAgents/com.pizzapi.runner.plist
launchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plist
Terminal window
# Stdout
tail -f ~/.pizzapi/logs/runner.log
# Stderr
tail -f ~/.pizzapi/logs/runner-error.log
Terminal window
launchctl list | grep pizzapi

The output columns are: PID, exit code, label. A - for PID means the service isn’t currently running.


KeyRequiredDescription
LabelUnique identifier for the service
ProgramArgumentsCommand to run — must be full paths (no ~ expansion)
EnvironmentVariablesMust include PIZZAPI_API_KEY, PIZZAPI_RELAY_URL, and PATH
WorkingDirectoryDefault working directory for spawned sessions
RunAtLoadStart automatically when the agent is loaded (including on login)
KeepAliveRestart automatically if the process exits
StandardOutPathFile path for stdout logging
StandardErrorPathFile path for stderr logging

If you previously set up PizzaPi as a LaunchDaemon (in /Library/LaunchDaemons/), migrate to a LaunchAgent:

  1. Stop and unload the daemon

    Terminal window
    sudo launchctl unload /Library/LaunchDaemons/com.pizzapi.runner.plist
  2. Remove the daemon plist

    Terminal window
    sudo rm /Library/LaunchDaemons/com.pizzapi.runner.plist
  3. Create the LaunchAgent following the steps above.


ProblemCauseFix
gh auth fails with “token is invalid”Runner is a LaunchDaemon — no keychain accessMigrate to a LaunchAgent (see above)
errSecInteractionNotAllowedKeychain is locked — process has no login sessionEnsure you’re using a LaunchAgent, not a LaunchDaemon
Runner exits immediately (exit code 1)Missing env vars or wrong pathsCheck ~/.pizzapi/logs/runner-error.log and verify all paths in the plist
bun: command not foundPATH in plist doesn’t include BunAdd /Users/yourname/.bun/bin to the PATH in EnvironmentVariables
Runner doesn’t start on rebootPlist not loadedRun launchctl load ~/Library/LaunchAgents/com.pizzapi.runner.plist
Runner starts but doesn’t connectWrong PIZZAPI_RELAY_URL or PIZZAPI_API_KEYCheck the values in the plist match ~/.pizzapi/config.json
Sessions spawn but tools can’t access GitHubgh token in keychain but login keychain lockedThis shouldn’t happen with a LaunchAgent — check security list-keychains includes login keychain

You can test whether the runner process has keychain access:

Terminal window
# Should return the token (not an error)
security find-generic-password -s "gh:github.com" -w

If this returns errSecInteractionNotAllowed, the keychain is locked — you’re likely running as a LaunchDaemon.