Local macOS (launchd)¶
Runs a supervised Distillery server on your Mac with scheduled ingestion, image updates, and maintenance — zero cloud, zero CI. Installed by scripts/local-macos/install.sh.
This is a different profile from the stdio local setup:
stdio (local-setup.md) |
launchd (this page) | |
|---|---|---|
| Transport | stdio | HTTP on 127.0.0.1:8000 |
| Lifecycle | launched per Claude Code session | supervised daemon, runs 24/7 |
| Scheduled work | optional Claude Code routines | six LaunchAgents |
| Image updates | manual | weekly auto-pull |
| Good for | single-user, low-footprint | heavier feed ingestion, personal KB |
What gets installed¶
Six LaunchAgents in ~/Library/LaunchAgents/:
| Label | Cadence | Purpose |
|---|---|---|
local.distillery |
supervised | Runs the GHCR container (ghcr.io/norrietaylor/distillery:latest) in the foreground; launchd restarts it on exit. |
local.distillery-update |
Mondays 09:00 | docker pull the :latest image; if the digest changed, kickstart the server. |
local.distillery-poll |
every 30 min | POST /api/poll — fetch new items from configured feed sources. |
local.distillery-classify |
every 2 hours | POST /api/hooks/classify-batch — batch-classify pending inbox entries. |
local.distillery-rescore |
daily 04:15 | POST /api/rescore — refresh feed-entry relevance scores. |
local.distillery-maintenance |
Mondays 05:00 | POST /api/maintenance — orchestrated poll → rescore → classify-batch. |
Plus these files in ~/.distillery/:
distillery.yaml— server configrun.sh,update.sh— server supervisor + image-update workerpoll.sh,classify.sh,rescore.sh,maintenance.sh— webhook workers_webhook_common.sh— shared helper sourced by the four workersdistillery.db— the DuckDB file*.log— per-agent stdout/stderr
And two entries in the macOS login Keychain (security CLI):
JINA_API_KEY— embedding provider keyDISTILLERY_WEBHOOK_SECRET— bearer token for/api/*routes
Prerequisites¶
Docker on Apple Silicon runs the container under Rosetta (linux/amd64). No separate arm64 image is published.
Install¶
git clone https://github.com/norrietaylor/distillery.git
cd distillery
./scripts/local-macos/install.sh
The installer will:
- Verify Docker is reachable.
- Prompt for your Jina API key (skipped if already in the Keychain).
- Generate a 32-byte webhook bearer secret (skipped if already in the Keychain).
- Write the config, scripts, and plists.
- Bootstrap every agent and kickstart the server.
- Wait up to 30 s for
http://127.0.0.1:8000/to respond.
Re-run any time to refresh scripts/plists — the installer is idempotent and never touches the database or existing secrets.
Flags¶
| Flag | Effect |
|---|---|
--jina-key <value> |
Pass the Jina key non-interactively (useful for scripted installs). |
--no-kickstart |
Load the agents but don't start the server or poll for readiness. |
Point Claude Code at the server¶
Add to ~/.claude/settings.json:
Restart Claude Code, then run /setup — it will detect the running server and offer to configure the reporting routines (feed health check, stale check, weekly digest). Those routines run inside Claude Code and complement the ingestion agents installed here.
Verify¶
# All six agents loaded
launchctl list | grep distillery
# Kick the server in case it's not running
launchctl kickstart -k gui/$(id -u)/local.distillery
# Server is up
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8000/
# Webhook auth works (expects 202 Accepted)
SECRET=$(security find-generic-password -a "$USER" -s DISTILLERY_WEBHOOK_SECRET -w)
curl -sS -H "Authorization: Bearer $SECRET" -X POST http://127.0.0.1:8000/api/poll
Tail the logs to watch the agents fire:
Customizing¶
Change the cadence¶
Edit the StartInterval (seconds) or StartCalendarInterval block in the plist, then reload:
launchctl bootout gui/$(id -u)/local.distillery-poll
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.distillery-poll.plist
Rotate the webhook secret¶
security delete-generic-password -a "$USER" -s DISTILLERY_WEBHOOK_SECRET
./scripts/local-macos/install.sh # regenerates on next install
launchctl kickstart -k gui/$(id -u)/local.distillery
Add a feed source¶
Open Claude Code and run /watch add <url>, or call distillery_watch directly via the MCP.
Disable one pipeline¶
launchctl bootout gui/$(id -u)/local.distillery-rescore
rm ~/Library/LaunchAgents/local.distillery-rescore.plist
Troubleshooting¶
Webhook returns 401. The server didn't see DISTILLERY_WEBHOOK_SECRET at startup — check the Keychain entry exists, then launchctl kickstart -k gui/$(id -u)/local.distillery.
Webhook returns 429 "too_early". Per-endpoint cooldown. The worker treats this as success; nothing to fix.
Container won't start. Inspect ~/.distillery/server.err.log. The supervisor exits 1 if JINA_API_KEY or DISTILLERY_WEBHOOK_SECRET is missing from the Keychain — launchd respects ThrottleInterval=10 so it retries every 10 s.
IO Error: Conflicting lock. Something other than the server is trying to open the DuckDB file (docker exec ... distillery <cmd>, a second container, a CLI on the host). DuckDB is single-writer; only the running server process may open the DB for writes. Use the HTTP webhooks instead.
Agents silently not firing. launchctl print gui/$(id -u)/<label> shows the next fire time and the last exit status. For calendar-based agents, confirm the Mac was awake at the scheduled minute — StartCalendarIntervalRunOnMissed=true triggers a catch-up on next wake.
Uninstall¶
./scripts/local-macos/uninstall.sh # unload agents, remove files
./scripts/local-macos/uninstall.sh --purge-data # + delete DB, config, logs
./scripts/local-macos/uninstall.sh --purge-secrets # + delete Keychain entries
Default uninstall preserves the database and Keychain entries so a re-install is non-destructive.