Skip to content

dan99git/Pickle-agent

Repository files navigation

Pickle Agent

An autonomous AI assistant framework built on Anthropic's Claude Code CLI, designed for 24/7 unattended operation on macOS.

Pickle Agent turns a Mac into a persistent, autonomous AI teammate. It watches for iMessages, monitors infrastructure health, performs scheduled research and writing tasks, manages email, and maintains its own memory across sessions — all without human intervention.

Originally built for a mental health occupational therapy practice, the architecture is fully generalizable to any domain where you want an always-on AI agent that communicates via iMessage and manages services.


Table of Contents


Overview

Pickle Agent is an autonomous AI agent framework that:

  • Runs continuously on macOS using launchd for process management — the same system macOS itself uses to manage its own services. No cron jobs, no screen sessions, no Docker containers for the agent itself.
  • Communicates via iMessage by reading the native chat.db SQLite database and sending replies through an MCP (Model Context Protocol) server. This means the agent has a real phone number and appears as a normal iMessage contact.
  • Monitors infrastructure including Docker containers, native services, disk usage, and network endpoints. It detects issues, attempts remediation, and alerts humans only when necessary.
  • Performs autonomous tasks on a schedule: research scans, email triage, blog post drafting, memory maintenance, and custom domain-specific work.
  • Maintains persistent memory across sessions via a structured file-based memory system. Each Claude Code CLI session is ephemeral, but the agent's knowledge, task list, contact notes, and incident history survive between invocations.
  • Enforces contact hours so it never texts people outside business hours (except when replying to someone who texted first).
  • Uses a three-daemon architecture where each daemon has a distinct responsibility, budget, and cadence.

The agent is not a single long-running process. Instead, it is a collection of shell scripts managed by launchd that spawn short-lived Claude Code CLI sessions in response to events (new message), schedules (health check, heartbeat), or conditions (detected issue). Each session receives the full system prompt, MCP configuration, and tool allowlist, executes its task, writes to the shared memory system, and exits. This design is intentionally stateless at the process level and stateful at the filesystem level.


Architecture

                          +---------------------------+
                          |        launchd            |
                          |   (macOS process mgr)     |
                          +---------------------------+
                           /           |            \
                          /            |             \
              +-----------+    +-------+------+    +----------+
              |  WATCH    |    |   HEALTH     |    | HEARTBEAT|
              |  Daemon   |    |   Daemon     |    |  Daemon  |
              +-----------+    +--------------+    +----------+
              | Poll every |    | Run every    |    | Run every|
              | 2 seconds  |    | 30 minutes   |    | 60 mins  |
              | for new    |    | Check Docker |    | Read     |
              | iMessages  |    | + ports +    |    | task     |
              |            |    | disk usage   |    | checklist|
              +-----+------+   +------+-------+   +-----+----+
                    |                  |                  |
                    v                  v                  v
              +-----------+    +--------------+    +----------+
              | Claude    |    | Claude Code  |    | Claude   |
              | Code CLI  |    | CLI session  |    | Code CLI |
              | session   |    | (if new      |    | session  |
              | (per msg) |    |  issues)     |    | (per     |
              |           |    |              |    |  beat)   |
              +-----+------+  +------+-------+   +-----+----+
                    |                 |                  |
                    +--------+--------+--------+--------+
                             |                 |
                             v                 v
                     +---------------+  +-------------+
                     | MCP Servers   |  |  Memory     |
                     | (iMessage,    |  |  System     |
                     |  Mail, APIs)  |  |  (files)    |
                     +---------------+  +-------------+

The three daemons share:

  • CLAUDE.md — the system prompt that defines the agent's identity, rules, and capabilities
  • mcp-config.json — configuration for all MCP server integrations
  • pickle-tools.conf — the shared tool allowlist
  • memory/ — the persistent memory directory

But they differ in:

  • Trigger — event-driven (watch) vs. time-driven (health, heartbeat)
  • Budget — per-session spending cap in USD
  • Prompt — each daemon constructs a different prompt for its Claude session
  • Cadence — 2s polling vs. 30min interval vs. 60min interval

Message Watcher (pickle-watch.shcom.pickle.watch)

The message watcher is the reactive daemon. It gives the agent the ability to receive and respond to iMessages from approved contacts.

How it works:

  1. Polling loop. The script runs an infinite while true loop with a sleep $PICKLE_POLL_INTERVAL (default: 2 seconds) between iterations. Each iteration queries the macOS Messages database.

  2. Database query. It reads ~/Library/Messages/chat.db using sqlite3. The query selects messages with a ROWID greater than the last processed ID, filtering for inbound messages only (messages where is_from_me = 0). The query joins the message, chat_message_join, and chat tables to extract the sender's phone number and the message text.

  3. Contact filtering. Each message's sender phone number is checked against contacts.conf. This file contains one phone number per line in E.164 format (e.g., +61400000000). Messages from unknown numbers are silently ignored. This prevents the agent from responding to spam, group chats, or unexpected contacts.

  4. State tracking. The last processed ROWID is stored in a .last_rowid file in the agent's working directory. On startup, if this file does not exist, the script initializes it to the current maximum ROWID (so it does not process historical messages). This means the agent only responds to messages that arrive after it starts.

  5. Message parsing. Raw message text is extracted using python3 for safe handling of Unicode, emoji, multiline text, and special characters. Shell-based parsing is unreliable for iMessage content, which frequently contains curly quotes, emoji, newlines, and non-ASCII characters. The Python parser handles all of these correctly and escapes the text for safe injection into a shell command.

  6. Session spawning. For each new message, the watcher spawns a fresh Claude Code CLI session. The prompt includes:

    • The sender's phone number
    • The message text
    • Instructions to reply via the iMessage MCP tool
    • The current date and time
    • A reminder of the agent's identity and rules (from CLAUDE.md)
  7. Session isolation. Each message gets its own Claude Code process. Sessions do not share state except through the filesystem (memory directory). This means a slow response to one message does not block processing of the next message, and a crash in one session does not affect others.

  8. Log capture. The stdout and stderr of each Claude Code session are captured to logs/claude-sessions/ with a timestamp and sender identifier in the filename.

Key design decisions:

  • Why polling instead of filesystem events? The chat.db WAL (Write-Ahead Log) file changes on every message, but fswatch/kqueue on SQLite databases is unreliable because the OS may batch writes. A 2-second poll is cheap (single SQLite query) and guarantees we never miss a message.
  • Why one session per message? Claude Code CLI sessions are stateless — they do not maintain conversation history between invocations. By giving each message its own session, we avoid context window pollution and ensure each response is grounded in the system prompt and memory files, not a drifting conversation.
  • Why Python for parsing? A line like I'm feeling "great" today 😊\nHow are you? will break naïve shell quoting. Python's json.dumps() produces safe, escaped strings every time.

Health Monitor (pickle-health.shcom.pickle.health)

The health monitor is the proactive daemon. It periodically checks the status of infrastructure and alerts when things go wrong.

How it works:

  1. Scheduled execution. The launchd plist runs the script at a fixed interval (default: 1800 seconds / 30 minutes). The script also supports a boot wait period (PICKLE_BOOT_WAIT, default: 120 seconds) — on first execution after system boot, it sleeps to give Docker containers and services time to start before checking their health.

  2. Docker container checks. The script runs docker ps --format '{{.Names}} {{.Status}}' and compares the running containers against an expected list:

    • open-webui
    • litellm
    • ragflow-server
    • litellm_db
    • ragflow-redis
    • ragflow-es-01
    • ragflow-minio
    • cloudflared

    Any container not in the Up state is flagged as an issue.

  3. Native service port checks. The script uses nc -z localhost <port> (or curl) to verify that non-Docker services are listening:

    Port Service Description
    8000 MCPO MCP Orchestrator
    8888 Jupyter Jupyter notebook server
    11434 Ollama Local LLM inference
    5151 Whisper Speech-to-text API
    5050 TTS Text-to-speech (edge-tts)
  4. Disk usage check. The script runs df -h / and parses the usage percentage. If disk usage exceeds 90%, it is flagged as a critical issue.

  5. Alert deduplication. This is a critical feature. The script maintains a LAST_ISSUES variable (persisted to a state file) containing the set of issues detected on the previous run. On each run, it compares the current issues to the previous issues. A Claude Code session is only spawned if there are new issues that were not present in the previous check. This prevents the agent from sending the same alert every 30 minutes for a persistent issue.

    For example:

    • Run 1: ragflow-server is down → new issue → spawn Claude session → alert sent
    • Run 2: ragflow-server is still down → same issue → no session spawned
    • Run 3: ragflow-server is still down AND litellm is now down → new issue (litellm) → spawn Claude session
    • Run 4: ragflow-server is back up, litellm still down → no new issues → no session
  6. Work hours awareness. Before sending an iMessage alert, the Claude session checks the current time against work hours (Mon–Fri 8am–6pm AEST). If outside work hours:

    • The issue is still logged to memory/incidents.jsonl
    • Automated remediation is still attempted (e.g., docker compose up -d)
    • But no iMessage is sent — the notification is saved for the morning briefing instead
  7. Claude session prompt. When a session is spawned, the prompt includes:

    • The list of detected issues (with details)
    • Instructions to attempt remediation
    • Instructions to alert via iMessage if during work hours
    • Instructions to log the incident to memory

Heartbeat Daemon (pickle-heartbeat.shcom.pickle.heartbeat)

The heartbeat is the autonomous daemon. It gives the agent the ability to perform scheduled tasks without any human trigger.

How it works:

  1. Scheduled execution. Runs every 60 minutes (configurable via PICKLE_HEARTBEAT_INTERVAL). Has a longer boot wait (default: 180 seconds) because it depends on services that the health monitor also checks.

  2. Task checklist. The daemon reads HEARTBEAT.md, which contains a structured checklist of tasks the agent should consider on each beat. The checklist is organized by frequency:

    • Every beat: Quick service health verification, check for unread emails
    • Every 4 hours: Research scan (check configured topics for new papers/articles)
    • Daily (morning): Morning briefing compilation, memory maintenance
    • Weekly: Blog post drafting, knowledge base review
  3. Higher budget. Heartbeat sessions have a higher budget (PICKLE_HEARTBEAT_BUDGET, default: 0.75 USD) than watcher or health sessions (0.50 USD) because they may need to perform complex multi-step tasks like research synthesis or blog writing.

  4. Work hours enforcement. The heartbeat prompt includes the current time and instructs the agent to:

    • Skip tasks that would generate notifications outside work hours
    • Still perform silent tasks (memory maintenance, research) anytime
    • Queue notification-generating tasks for the next work-hours beat
  5. Session prompt. The heartbeat prompt includes:

    • The full HEARTBEAT.md task checklist
    • The current date, time, and day of week
    • The contents of memory/tasks.md (carry-over tasks)
    • Instructions on work hours and notification rules

File Structure

pickle-agent/
├── CLAUDE.md                  — System prompt: the agent's identity, rules, capabilities,
│                                and instructions. This is the "brain" — it defines who
│                                Pickle is, what it can do, and how it should behave.
│                                Loaded via --system-prompt on every Claude CLI session.
│
├── SOUL.md                    — Personality definition: communication style, values,
│                                tone of voice, humor preferences, and interpersonal
│                                guidelines. Referenced by CLAUDE.md. Separating soul
│                                from instructions keeps both files focused.
│
├── HEARTBEAT.md               — Autonomous task checklist read by the heartbeat daemon.
│                                Structured as a markdown checklist with frequency tags
│                                (every-beat, 4-hourly, daily, weekly). The agent reads
│                                this file and decides which tasks to execute.
│
├── mcp-config.json            — MCP (Model Context Protocol) server configuration.
│                                Defines every external tool integration: iMessage,
│                                Apple Mail, Paper Search, PubMed, ICD-11, Zanda,
│                                Giphy. Contains API keys (not committed to git).
│
├── pickle-tools.conf          — Shared tool allowlist. Sourced by all three daemon
│                                scripts. Builds the comma-separated --allowedTools
│                                string passed to Claude Code CLI. Controls exactly
│                                which MCP tools and built-in tools the agent can use.
│
├── contacts.conf              — Allowed phone numbers, one per line, E.164 format.
│                                Only messages from these numbers are processed.
│                                First entry is used as default alert contact.
│                                Example: +61400000000
│
├── pickle-watch.sh            — Message watcher daemon script. Polls chat.db,
│                                filters messages, spawns Claude sessions.
│                                Managed by launchd via com.pickle.watch plist.
│
├── pickle-health.sh           — Health monitor daemon script. Checks Docker
│                                containers, service ports, disk usage. Deduplicates
│                                alerts. Managed by com.pickle.health plist.
│
├── pickle-heartbeat.sh        — Heartbeat daemon script. Reads HEARTBEAT.md,
│                                executes scheduled autonomous tasks. Managed by
│                                com.pickle.heartbeat plist.
│
├── pickle-start.sh            — Start script. Copies plist files to
│                                ~/Library/LaunchAgents/, loads them with launchctl,
│                                and verifies they are running.
│
├── pickle-stop.sh             — Stop script. Unloads all three daemons via
│                                launchctl and optionally removes plist files.
│
├── pickle-test.sh             — End-to-end verification script. Tests each
│                                component independently: chat.db access, sqlite3
│                                availability, Claude CLI, MCP servers, contacts
│                                file, directory structure, permissions.
│
├── .gitignore                 — Ignores logs/, memory/daily/, .last_rowid,
│                                .env, mcp-config.json (contains secrets),
│                                and other runtime state files.
│
├── skills/                    — Reusable procedure library. Each file is a
│   │                            markdown document describing a multi-step
│   │                            procedure the agent can follow.
│   │
│   ├── README.md              — How to create and use skills
│   ├── research-scan.md       — Procedure for scanning academic databases
│   │                            for new papers on configured topics
│   ├── email-draft.md         — Procedure for drafting professional emails
│   │                            with appropriate tone and structure
│   ├── service-restart.md     — Procedure for safely restarting services
│   │                            with pre/post health checks
│   ├── incident-report.md     — Procedure for writing structured incident
│   │                            reports after service outages
│   ├── morning-briefing.md    — Procedure for compiling the daily morning
│   │                            briefing from overnight events
│   ├── blog-writing.md        — Procedure for researching and drafting
│   │                            blog posts on mental health topics
│   └── zanda-lookup.md        — Procedure for looking up client/appointment
│                                data in the Zanda practice management system
│
├── memory/                    — Persistent memory across sessions. This is
│   │                            the agent's long-term storage. All Claude
│   │                            sessions read from and write to these files.
│   │
│   ├── incidents.jsonl        — Append-only structured log. One JSON object
│   │                            per line. Records every significant event:
│   │                            messages received, health issues, tasks done.
│   ├── knowledge.md           — Accumulated learnings about the environment,
│   │                            systems, people, and domain. Deduplicated.
│   ├── contacts.md            — Personal notes about known contacts:
│   │                            preferences, roles, past interactions.
│   ├── tasks.md               — Carry-over task list. Items that couldn't
│   │                            be completed in one session persist here.
│   ├── daily/                 — Ephemeral daily notes. One file per day
│   │                            (YYYY-MM-DD.md). Auto-cleaned after 30 days.
│   ├── blog/                  — Blog post drafts in progress. Organized
│   │                            by topic and date.
│   └── research/              — Research findings. Papers, summaries,
│                                and synthesis documents.
│
├── launchd/                   — macOS LaunchAgent plist templates
│   ├── com.pickle.watch.plist     — Plist for message watcher (KeepAlive)
│   ├── com.pickle.health.plist    — Plist for health monitor (StartInterval)
│   └── com.pickle.heartbeat.plist — Plist for heartbeat (StartInterval)
│
└── logs/                      — Runtime logs (gitignored)
    ├── pickle-watch.log       — Watcher daemon stdout/stderr
    ├── pickle-health.log      — Health monitor stdout/stderr
    ├── pickle-heartbeat.log   — Heartbeat daemon stdout/stderr
    └── claude-sessions/       — Individual session transcripts, one file
                                 per Claude Code CLI invocation, named with
                                 timestamp and trigger context.

Prerequisites

Required

Requirement Why How to verify
macOS (13 Ventura or later) Uses launchd, iMessage, chat.db, Apple frameworks sw_vers
Claude Code CLI The AI engine — every session is a Claude Code invocation claude --version
Anthropic API key Configured within Claude Code (not in agent config) claude should work interactively
Full Disk Access for Terminal Required to read ~/Library/Messages/chat.db System Settings → Privacy & Security → Full Disk Access → Terminal ✓
sqlite3 Queries chat.db — ships with macOS which sqlite3
Python 3.12+ Used for safe message text parsing and the Apple Mail MCP server python3 --version
Node.js 18+ Required for most MCP servers (they run as Node processes) node --version
iMessage configured The Mac must be signed into iMessage with an Apple ID Messages.app → Settings → iMessage ✓

Optional (depending on your setup)

Requirement Why
Docker Desktop Only if you are monitoring Docker containers
Ollama Only if you run local LLMs that the agent monitors
Homebrew Convenient for installing Node.js, Python, etc.

macOS Permissions Checklist

The agent needs these permissions to function. All are configured in System Settings → Privacy & Security:

  1. Full Disk Access — for Terminal.app (or whichever terminal runs launchd jobs). Without this, reading chat.db returns a permission error.
  2. Automation — Terminal may need permission to control Messages.app if using AppleScript-based iMessage sending (depends on your MCP server implementation).
  3. Accessibility — not required for standard operation, but some MCP servers may request it.

Quick Start

1. Clone the repository

git clone https://github.com/your-org/pickle-agent.git
cd pickle-agent

2. Configure allowed contacts

Edit contacts.conf with the phone numbers that should be able to message the agent. One number per line, E.164 format:

+61400000000
+61400000001

The first number in the file is used as the default alert contact (where health alerts are sent).

3. Configure MCP servers

Copy the example config and fill in your API keys:

cp mcp-config.example.json mcp-config.json

Edit mcp-config.json and provide:

  • API keys for any external services (Semantic Scholar, PubMed, ICD-11, Zanda, Giphy)
  • Paths to MCP server executables/scripts
  • Any per-server configuration

See the MCP Servers section for details on each integration.

4. Update launchd plists with your paths

Each plist file in launchd/ contains hardcoded paths that must match your system. Edit each file and update:

<key>WorkingDirectory</key>
<string>/Users/YOUR_USERNAME/pickle-agent</string>

<key>ProgramArguments</key>
<array>
    <string>/bin/bash</string>
    <string>/Users/YOUR_USERNAME/pickle-agent/pickle-watch.sh</string>
</array>

<key>StandardOutPath</key>
<string>/Users/YOUR_USERNAME/pickle-agent/logs/pickle-watch.log</string>

<key>StandardErrorPath</key>
<string>/Users/YOUR_USERNAME/pickle-agent/logs/pickle-watch.log</string>

Replace /Users/YOUR_USERNAME/pickle-agent with the absolute path to your clone.

5. Create required directories

mkdir -p logs/claude-sessions
mkdir -p memory/{daily,blog,research}
touch memory/incidents.jsonl
touch memory/knowledge.md
touch memory/contacts.md
touch memory/tasks.md

6. Copy plists to LaunchAgents

cp launchd/com.pickle.watch.plist ~/Library/LaunchAgents/
cp launchd/com.pickle.health.plist ~/Library/LaunchAgents/
cp launchd/com.pickle.heartbeat.plist ~/Library/LaunchAgents/

7. Run the test script

chmod +x pickle-test.sh
./pickle-test.sh

This verifies every component: chat.db access, sqlite3, Claude CLI, directory structure, contacts.conf format, MCP config validity, and disk permissions. Fix any failures before proceeding.

8. Go live

chmod +x pickle-start.sh
./pickle-start.sh

This loads all three daemons into launchd. The watcher starts polling immediately (after its boot wait). The health monitor and heartbeat will fire on their respective intervals.

Verify they are running:

launchctl list | grep pickle

You should see three entries:

-    0    com.pickle.watch
-    0    com.pickle.health
-    0    com.pickle.heartbeat

9. Send a test message

Send an iMessage from one of your allowed contacts to the Mac's iMessage account. Within a few seconds, you should receive a reply. Check logs/pickle-watch.log for debug output.


Configuration Reference

All configuration is done through environment variables. These can be set in the launchd plist files (via EnvironmentVariables dict), exported in the daemon shell scripts, or set in a .env file sourced by the scripts.

Watch Daemon Variables

Variable Default Description
PICKLE_POLL_INTERVAL 2 Seconds between chat.db polls. Lower values mean faster response but more CPU. Values below 1 are not recommended.
PICKLE_MAX_BUDGET 0.50 Maximum USD spend per watcher Claude session. If the agent exceeds this budget on a single message response, the session is terminated. Prevents runaway costs from complex queries.
PICKLE_WATCH_BOOT_WAIT 30 Seconds to wait after daemon start before beginning to poll. Gives the system time to initialize after boot.
PICKLE_MODEL sonnet Claude model identifier. Passed to --model. Options: sonnet, opus, haiku. Sonnet is recommended for the best cost/performance balance.
PICKLE_ALERT_CONTACT First entry in contacts.conf Phone number (E.164) used for outbound alerts. Overrides the default if set.

Health Daemon Variables

Variable Default Description
PICKLE_HEALTH_INTERVAL 1800 Seconds between health checks (30 minutes). Set in the launchd plist as StartInterval. The shell script itself does not loop — launchd re-invokes it.
PICKLE_HEALTH_BUDGET 0.50 Maximum USD spend per health check session. Health sessions are usually cheap (just sending an alert), but remediation attempts (restarting services) can use more tokens.
PICKLE_BOOT_WAIT 120 Seconds to wait on first invocation after boot. Docker containers, databases, and network services need time to start. The health daemon should not raise false alarms during boot.
PICKLE_DISK_THRESHOLD 90 Disk usage percentage that triggers an alert. Integer, no percent sign.

Heartbeat Daemon Variables

Variable Default Description
PICKLE_HEARTBEAT_INTERVAL 3600 Seconds between heartbeat invocations (60 minutes). Set in launchd plist as StartInterval.
PICKLE_HEARTBEAT_BUDGET 0.75 Maximum USD spend per heartbeat session. Higher than other daemons because heartbeat tasks (research, blog writing) are more complex and token-intensive.
PICKLE_BOOT_WAIT 180 Seconds to wait on first invocation after boot. Longer than health daemon because the heartbeat depends on services that the health daemon also checks.

Shared Variables

Variable Default Description
PICKLE_MODEL sonnet Claude model for all sessions. Can be overridden per-daemon by setting it in the specific plist.
PICKLE_AGENT_DIR Script's directory Base directory for the agent. All relative paths resolve from here. Auto-detected from the script location.
PICKLE_LOG_DIR $PICKLE_AGENT_DIR/logs Directory for log files.
PICKLE_MEMORY_DIR $PICKLE_AGENT_DIR/memory Directory for persistent memory files.

Example: Setting variables in a launchd plist

<key>EnvironmentVariables</key>
<dict>
    <key>PICKLE_POLL_INTERVAL</key>
    <string>2</string>
    <key>PICKLE_MAX_BUDGET</key>
    <string>0.50</string>
    <key>PICKLE_MODEL</key>
    <string>sonnet</string>
    <key>PATH</key>
    <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
</dict>

Note: The PATH variable is important in launchd plists because launchd jobs run with a minimal environment. You must include the paths to claude, python3, node, docker, and sqlite3.


MCP Servers

MCP (Model Context Protocol) servers extend Claude's capabilities by providing tool integrations. Each server runs as a separate process and communicates with Claude Code CLI via stdio or HTTP. The agent's mcp-config.json defines all available MCP servers.

iMessage (Apple Extension)

  • Purpose: Send and read iMessages. This is the agent's primary communication channel.
  • Transport: Native macOS integration via AppleScript or direct Messages framework access.
  • Tools provided:
    • send_message — send an iMessage to a phone number or email
    • read_messages — read recent messages from a conversation
    • list_conversations — list active iMessage conversations
  • API keys: None (uses local macOS iMessage account)
  • Permissions: The process running Claude Code must have access to Messages.app automation.

Apple Mail (Python MCP)

  • Purpose: Read, search, and draft emails via the macOS Mail.app.
  • Transport: Python MCP server using the pyobjc framework to access Apple Mail via ScriptingBridge.
  • Tools provided:
    • list_mailboxes — list available mailboxes
    • search_mail — search emails by subject, sender, date range
    • read_email — read the full content of a specific email
    • create_draft — create a new email draft in Mail.app
    • list_unread — list unread emails across mailboxes
  • API keys: None (uses local Mail.app)
  • Requirements: Python 3.12+, pyobjc-framework-ScriptingBridge, pyobjc-framework-Cocoa
  • Note: Mail.app must be configured with at least one email account.

Paper Search (Semantic Scholar)

  • Purpose: Search for academic papers across multiple databases. Used for research scans and blog post research.
  • Transport: Node.js MCP server wrapping the Semantic Scholar API.
  • Tools provided:
    • search_papers — keyword/phrase search across papers
    • get_paper — retrieve full paper details by ID
    • get_citations — get papers citing a given paper
    • get_references — get papers referenced by a given paper
    • search_authors — search for authors
  • API keys: Semantic Scholar API key (free tier available, recommended for rate limits)
  • Databases searched: Semantic Scholar (aggregates PubMed, ArXiv, ACL, DBLP, and others)

PubMed

  • Purpose: Direct PubMed integration for medical/health literature search.
  • Transport: Node.js MCP server wrapping the NCBI E-Utilities API.
  • Tools provided:
    • search_pubmed — search PubMed with MeSH terms and keywords
    • get_abstract — retrieve abstract for a given PMID
    • get_full_details — retrieve full bibliographic details
  • API keys: NCBI API key (optional but recommended; without it, rate limited to 3 req/sec)

ICD-11 API

  • Purpose: WHO International Classification of Diseases, 11th revision. Useful for looking up diagnostic codes in a health practice context.
  • Transport: Node.js MCP server wrapping the WHO ICD-11 API.
  • Tools provided:
    • search_icd11 — search for ICD-11 codes by term
    • get_code_details — get full details for a specific ICD-11 code
    • linearization_search — search within a specific linearization (e.g., MMS)
  • API keys: WHO ICD API client_id and client_secret (free registration at icd.who.int/icdapi)

Zanda (Power Diary)

  • Purpose: Practice management system integration. Look up appointments, clients, invoices, and practitioner schedules.
  • Transport: Node.js MCP server wrapping the Zanda/Power Diary API.
  • Tools provided:
    • search_clients — search for clients by name or ID
    • get_appointments — get appointments for a date range
    • get_practitioner_schedule — view practitioner availability
    • get_invoice — retrieve invoice details
  • API keys: Zanda API key (generated in Zanda admin panel)
  • Special note: This server uses a "code-mode pattern" where sensitive operations require confirmation via the permissions model. The agent can look up data freely but cannot modify records without approval.

Giphy

  • Purpose: Search for and send GIFs. Adds personality to iMessage conversations.
  • Transport: Node.js MCP server wrapping the Giphy API.
  • Tools provided:
    • search_gifs — search for GIFs by keyword
    • get_trending — get trending GIFs
    • get_random — get a random GIF for a tag
  • API keys: Giphy API key (free tier available at developers.giphy.com)

Example mcp-config.json structure

{
  "mcpServers": {
    "imessage": {
      "command": "node",
      "args": ["/path/to/imessage-mcp/index.js"],
      "env": {}
    },
    "apple-mail": {
      "command": "python3",
      "args": ["/path/to/apple-mail-mcp/server.py"],
      "env": {}
    },
    "paper-search": {
      "command": "node",
      "args": ["/path/to/paper-search-mcp/index.js"],
      "env": {
        "SEMANTIC_SCHOLAR_API_KEY": "your-key-here"
      }
    },
    "pubmed": {
      "command": "node",
      "args": ["/path/to/pubmed-mcp/index.js"],
      "env": {
        "NCBI_API_KEY": "your-key-here"
      }
    },
    "icd11": {
      "command": "node",
      "args": ["/path/to/icd11-mcp/index.js"],
      "env": {
        "ICD_CLIENT_ID": "your-id",
        "ICD_CLIENT_SECRET": "your-secret"
      }
    },
    "zanda": {
      "command": "node",
      "args": ["/path/to/zanda-mcp/index.js"],
      "env": {
        "ZANDA_API_KEY": "your-key-here"
      }
    },
    "giphy": {
      "command": "node",
      "args": ["/path/to/giphy-mcp/index.js"],
      "env": {
        "GIPHY_API_KEY": "your-key-here"
      }
    }
  }
}

Tool Allowlist

The file pickle-tools.conf controls exactly which tools the agent is allowed to use. It is sourced (. pickle-tools.conf) by all three daemon scripts and exports a variable containing a comma-separated list of tool identifiers.

How it works

  1. The conf file defines arrays of tools by category.
  2. The script joins them into a single comma-separated string.
  3. This string is passed to Claude Code CLI via the --allowedTools flag.
  4. Claude Code will refuse to call any tool not in this list, even if an MCP server offers it.

Why this matters

The allowlist is a security boundary. Even though --dangerously-skip-permissions is set (required for autonomous operation), the allowlist prevents the agent from using tools that could cause damage. For example, you might allow docker ps and docker restart but not docker rm or docker compose down.

Tool categories

The allowlist is organized into logical categories:

Built-in Claude Code tools:

  • Read — read files from disk
  • Write — write files to disk
  • Edit — edit files with find-and-replace
  • Bash — execute shell commands
  • Glob — find files by pattern
  • Grep — search file contents

iMessage tools:

  • mcp__imessage__send_message — send iMessages
  • mcp__imessage__read_messages — read iMessage conversations

Email tools:

  • mcp__apple-mail__search_mail — search emails
  • mcp__apple-mail__read_email — read email content
  • mcp__apple-mail__create_draft — draft new emails
  • mcp__apple-mail__list_unread — list unread emails

Research tools:

  • mcp__paper-search__search_papers — search academic papers
  • mcp__paper-search__get_paper — get paper details
  • mcp__pubmed__search_pubmed — search PubMed
  • mcp__pubmed__get_abstract — get paper abstracts
  • mcp__icd11__search_icd11 — search ICD-11 codes

Practice management tools:

  • mcp__zanda__search_clients — search Zanda clients
  • mcp__zanda__get_appointments — view appointments

Fun tools:

  • mcp__giphy__search_gifs — search for GIFs

Modifying the allowlist

To add a new tool, add its identifier to the appropriate category array in pickle-tools.conf. The identifier format for MCP tools is mcp__<server-name>__<tool-name>. For built-in tools, use the tool name directly (e.g., Bash, Read).

To find available tool identifiers for an MCP server, run Claude Code interactively with the MCP config and ask it to list available tools.


Permissions Model

The agent operates in --dangerously-skip-permissions mode, which disables Claude Code's interactive approval prompts. This is necessary for autonomous operation (there is no human at the keyboard to click "Allow"). Instead, the agent enforces a three-tier permissions model through its system prompt (CLAUDE.md) and the tool allowlist.

Tier 1: Free (No Permission Needed)

These actions can be performed autonomously at any time:

Action Examples
Diagnostics docker ps, docker logs, df -h, curl health endpoints, ping, nc -z
Service restarts docker restart <container>, brew services restart <service>
Email operations Reading emails, drafting emails (drafts only — never auto-sends)
Research Searching academic databases, reading papers, synthesizing findings
Memory operations Writing to memory/, reading from memory/, appending to incidents.jsonl
iMessage replies Responding to incoming messages from allowed contacts
File reads Reading any file in the agent directory or monitored services
Log inspection Reading log files for debugging

Tier 2: Requires Confirmation

These actions are allowed but only after confirming with the human via iMessage. The agent must ask and receive explicit approval before proceeding:

Action Why confirmation needed
Git operations git commit, git pull, git checkout — code changes should be reviewed
Configuration edits Editing docker-compose.yaml, nginx configs, .env files — could break services
Stopping services docker stop, brew services stop — intentional downtime needs approval
Installing packages brew install, pip install, npm install — system modifications
Sending emails Actually sending (not drafting) emails — should be reviewed first

Tier 3: Always Forbidden

These actions are hardcoded as forbidden in the system prompt. The agent will refuse to perform them regardless of instructions:

Action Why forbidden
docker compose down Destroys all containers and networks — catastrophic
Secret/key modification Editing .env files containing API keys, modifying mcp-config.json secrets
git push Pushing code to remote — should always be done by a human
git push --force Destructive force push — never
File deletion outside memory/ Deleting code, configs, or data files
System modification Modifying launchd plists, system configs, crontabs
Network configuration Changing DNS, firewall rules, Cloudflare settings
Database writes Direct writes to MySQL, PostgreSQL, Elasticsearch

How tiers are enforced

  1. Tool allowlist (pickle-tools.conf) — first line of defense. Tools not in the list simply cannot be called.
  2. System prompt (CLAUDE.md) — contains explicit rules about what requires confirmation and what is forbidden. Claude follows these instructions.
  3. Budget cap (--max-budget-usd) — limits total spend per session, preventing runaway operations.
  4. Monitoring — all sessions are logged, providing an audit trail.

This is a defense-in-depth approach. No single mechanism is relied upon exclusively.


Memory System

The memory system is what gives Pickle continuity across sessions. Each Claude Code CLI session is ephemeral — it starts fresh with no memory of previous sessions. The memory directory bridges this gap by providing persistent, file-based storage that every session can read from and write to.

incidents.jsonl

Purpose: Append-only structured event log.

Format: One JSON object per line (JSON Lines format). Each entry records a significant event.

Schema:

{
  "timestamp": "2026-03-19T10:30:00+11:00",
  "type": "message|health|task|error|alert",
  "source": "watch|health|heartbeat",
  "summary": "Brief description of what happened",
  "details": "Extended details, error messages, etc.",
  "action_taken": "What the agent did in response",
  "contact": "+61400000000",
  "resolved": true
}

Usage rules:

  • Append only — never edit or delete existing entries
  • Every Claude session should log its significant actions here
  • Used by the morning briefing skill to compile overnight summaries
  • Used by the heartbeat to track patterns (recurring failures, etc.)

knowledge.md

Purpose: Accumulated learnings about the environment, systems, and domain.

Structure: Markdown with sections for different knowledge categories. Each entry is a bullet point.

Usage rules:

  • Before adding a new entry, the agent must check for duplicates
  • Entries should be factual and specific (not "Docker is useful" but "ragflow-server needs ragflow-redis and ragflow-es-01 to be healthy before it will start")
  • Periodically reviewed and pruned by the heartbeat daemon

Example entries:

## Infrastructure
- ragflow-server takes ~90 seconds to become healthy after container start
- litellm_db must be fully ready before litellm can connect (check pg_isready)
- Cloudflare tunnel restarts automatically but takes ~30s to reconnect

## Domain
- OT = Occupational Therapy, not Operations/Other
- NDIS reporting deadlines are quarterly
- Zanda API rate limit is 100 requests per minute

contacts.md

Purpose: Personal notes about known contacts. Helps the agent maintain continuity in conversations.

Structure: Markdown with a section per contact (identified by phone number and name).

Example:

## Sarah (+61400000000)
- Practice owner, occupational therapist
- Prefers brief updates, not lengthy reports
- Usually checks messages between 8-9am
- Interested in sensory processing research

tasks.md

Purpose: Carry-over task list. When a session cannot complete a task (ran out of budget, needs information, waiting on something), it records the task here for a future session to pick up.

Structure: Markdown checklist.

## Pending Tasks
- [ ] Finish blog post on sensory diets (draft in memory/blog/)
- [ ] Follow up on ragflow indexing issue from 2026-03-18
- [ ] Research new NDIS pricing framework changes

## Completed (last 7 days)
- [x] 2026-03-17: Sent weekly research digest to Sarah
- [x] 2026-03-16: Fixed litellm_db connection timeout

daily/

Purpose: Ephemeral daily notes. One markdown file per day, named YYYY-MM-DD.md.

Usage: Used for tracking within-day state, such as "already sent morning briefing today" or "ragflow was restarted at 2pm." The heartbeat daemon auto-cleans files older than 30 days.

blog/

Purpose: Blog post drafts. Organized by topic. Posts go through multiple stages: outline → draft → review → final.

research/

Purpose: Research findings. When the agent performs a research scan, it saves summaries and paper references here for later synthesis.


Skills System

Skills are reusable, structured procedures stored as markdown files in the skills/ directory. They serve as the agent's "playbook" — step-by-step instructions for complex, multi-step tasks.

Why skills exist

Claude is capable of figuring out how to do things from scratch, but:

  1. Consistency — skills ensure the same task is done the same way every time
  2. Quality — skills encode best practices and lessons learned
  3. Efficiency — skills reduce token usage by providing a clear plan instead of requiring the agent to reason from first principles
  4. Auditability — skills make it clear what the agent will do before it does it

Skill format

Each skill is a markdown file with the following structure:

# Skill Name

## Purpose
What this skill accomplishes.

## When to use
Triggers or conditions under which this skill should be invoked.

## Prerequisites
What must be true before starting.

## Steps
1. First step with specific details
2. Second step
   - Sub-detail
   - Sub-detail
3. Third step

## Output
What the skill produces (e.g., a file, a message, a log entry).

## Error handling
What to do if something goes wrong at each step.

How skills are used

Skills are referenced in the system prompt (CLAUDE.md) and task checklists (HEARTBEAT.md). When the agent encounters a task that matches a skill, it reads the skill file and follows its steps. For example:

  • HEARTBEAT.md says: "If it's Monday morning, execute the blog-writing skill"
  • The agent reads skills/blog-writing.md
  • It follows the steps: choose topic → search papers → outline → draft → save to memory/blog/

Creating a new skill

  1. Create a new markdown file in skills/ with a descriptive name (e.g., client-report.md)
  2. Follow the format above
  3. Reference it in CLAUDE.md, HEARTBEAT.md, or another skill as appropriate
  4. Test it by sending the agent a message asking it to perform the skill

Contact Hours System

The agent enforces work hours to avoid sending iMessages at inappropriate times.

Configuration

Work hours are defined in the system prompt (CLAUDE.md) and daemon scripts:

  • Days: Monday through Friday
  • Hours: 8:00 AM to 6:00 PM
  • Timezone: AEST (Australia/Eastern, UTC+10/+11 depending on DST)

Enforcement by daemon

Message Watcher: No hours restriction. If someone texts the agent at 2am, it replies. The principle is: if a human initiated contact, they want a response regardless of the hour.

Health Monitor: Checks the current time before sending alerts.

  • During work hours: Sends iMessage alert to the configured contact.
  • Outside work hours: Logs the issue to memory/incidents.jsonl, attempts automated remediation (restart containers, etc.), but does not send an iMessage. The issue is queued for the next morning briefing.
  • Exception: Critical issues (all services down, disk at 99%) may override this rule if configured.

Heartbeat Daemon: Checks the current time before executing notification-generating tasks.

  • During work hours: Executes all applicable tasks, sends results via iMessage if needed.
  • Outside work hours: Only executes silent tasks (memory maintenance, research that saves to files). Skips tasks that would generate messages.

How time checking works

Each daemon script checks the time using shell date commands:

current_hour=$(date +%H)
current_dow=$(date +%u)  # 1=Monday, 7=Sunday

if [[ $current_dow -ge 1 && $current_dow -le 5 && $current_hour -ge 8 && $current_hour -lt 18 ]]; then
    WORK_HOURS=true
else
    WORK_HOURS=false
fi

This is also reinforced in the Claude session prompt, so even if the shell check is bypassed, the agent's system prompt instructs it not to send messages outside hours.


Session Lifecycle

Watch Flow (Message Arrives)

1. launchd keeps pickle-watch.sh running (KeepAlive=true)
2. pickle-watch.sh polls chat.db every PICKLE_POLL_INTERVAL seconds
3. New message detected (ROWID > .last_rowid, is_from_me=0)
4. Sender phone number checked against contacts.conf
5. ✓ Match → continue / ✗ No match → skip, update .last_rowid
6. Message text extracted via python3 (safe unicode handling)
7. .last_rowid updated to current ROWID
8. Claude Code CLI spawned:

   claude -p "You received a message from {phone} at {time}: '{text}'.
   Reply via iMessage." \
     --system-prompt "$(cat CLAUDE.md)" \
     --mcp-config mcp-config.json \
     --allowedTools "$ALLOWED_TOOLS" \
     --dangerously-skip-permissions \
     --model "$PICKLE_MODEL" \
     --max-budget-usd "$PICKLE_MAX_BUDGET" \
     2>&1 | tee "logs/claude-sessions/watch-{timestamp}-{phone}.log"

9. Claude session:
   a. Reads system prompt (CLAUDE.md) → understands identity and rules
   b. Reads memory files (incidents.jsonl, knowledge.md, contacts.md)
   c. Formulates response
   d. Calls mcp__imessage__send_message to reply
   e. Logs interaction to memory/incidents.jsonl
   f. Session ends (budget exhausted or task complete)

10. pickle-watch.sh returns to polling loop

Health Flow (Issue Detected)

1. launchd invokes pickle-health.sh (StartInterval=1800)
2. Script checks if boot wait is needed (first run after boot)
3. Docker container checks run:
   docker ps --format '{{.Names}} {{.Status}}'
4. Port checks run:
   nc -z localhost 8000 (MCPO)
   nc -z localhost 8888 (Jupyter)
   nc -z localhost 11434 (Ollama)
   nc -z localhost 5151 (Whisper)
   nc -z localhost 5050 (TTS)
5. Disk usage checked:
   df -h / | awk '{print $5}' | tail -1
6. Issue list compiled: ["ragflow-server: Exited", "port 5151: unreachable"]
7. Compared against LAST_ISSUES from previous run
8. New issues found? → Continue / No new issues? → Exit
9. Work hours checked
10. Claude Code CLI spawned:

    claude -p "Health check detected issues:
    - ragflow-server: container exited
    - port 5151 (Whisper): unreachable
    Attempt remediation. Alert via iMessage if work hours." \
      --system-prompt "$(cat CLAUDE.md)" \
      --mcp-config mcp-config.json \
      --allowedTools "$ALLOWED_TOOLS" \
      --dangerously-skip-permissions \
      --model "$PICKLE_MODEL" \
      --max-budget-usd "$PICKLE_HEALTH_BUDGET" \
      2>&1 | tee "logs/claude-sessions/health-{timestamp}.log"

11. Claude session:
    a. Reads skills/service-restart.md for remediation procedure
    b. Attempts to restart affected services
    c. Verifies health post-restart
    d. Logs incident to memory/incidents.jsonl
    e. If work hours: sends iMessage alert with status
    f. If outside hours: saves alert for morning briefing
    g. Session ends

12. LAST_ISSUES updated with current issue set
13. Script exits (launchd will reinvoke after interval)

Heartbeat Flow

1. launchd invokes pickle-heartbeat.sh (StartInterval=3600)
2. Script checks if boot wait is needed
3. Current date, time, day-of-week determined
4. HEARTBEAT.md read into variable
5. memory/tasks.md read into variable
6. Claude Code CLI spawned:

   claude -p "Heartbeat at {datetime} ({dayname}).
   Task checklist:
   {HEARTBEAT.md contents}

   Carry-over tasks:
   {tasks.md contents}

   Execute applicable tasks." \
     --system-prompt "$(cat CLAUDE.md)" \
     --mcp-config mcp-config.json \
     --allowedTools "$ALLOWED_TOOLS" \
     --dangerously-skip-permissions \
     --model "$PICKLE_MODEL" \
     --max-budget-usd "$PICKLE_HEARTBEAT_BUDGET" \
     2>&1 | tee "logs/claude-sessions/heartbeat-{timestamp}.log"

7. Claude session:
   a. Evaluates task checklist against current time/day
   b. Checks work hours for notification-generating tasks
   c. Executes applicable tasks (research, email check, etc.)
   d. Updates memory files with results
   e. Updates tasks.md (mark completed, add new)
   f. If morning + work hours: compiles and sends briefing
   g. Session ends

8. Script exits (launchd will reinvoke after interval)

Claude Code CLI Flags

Every Claude Code CLI invocation uses the following flags. Understanding each one is essential for debugging and customization.

-p "prompt"

Non-interactive prompt mode. Instead of opening an interactive REPL, Claude Code executes the given prompt and exits. This is what enables autonomous operation — there is no human typing in a terminal.

The prompt is a string that tells Claude what to do. For the watcher, it contains the incoming message. For health, it contains detected issues. For heartbeat, it contains the task checklist.

--system-prompt "$(cat CLAUDE.md)"

Custom system prompt. Overrides Claude Code's default system prompt with the contents of CLAUDE.md. This is the agent's "brain" — it defines identity, rules, capabilities, and behavioral guidelines.

The $(cat CLAUDE.md) shell substitution reads the entire file into the argument. This means changes to CLAUDE.md take effect on the next session without restarting any daemons.

--mcp-config mcp-config.json

MCP server configuration. Tells Claude Code which MCP servers to start and connect to. Each server provides tools (functions) that Claude can call. The config file specifies the command, arguments, and environment variables for each server.

--allowedTools "tool1,tool2,tool3"

Tool allowlist. A comma-separated list of tool identifiers that Claude is permitted to use. Any tool not in this list will be unavailable, even if an MCP server provides it. This is the primary security mechanism in autonomous mode.

Built from pickle-tools.conf at session start time.

--dangerously-skip-permissions

Disable interactive approval prompts. Normally, Claude Code asks for human approval before performing actions like writing files, running commands, or calling tools. In autonomous mode, there is no human to approve, so this flag disables all prompts.

This is why the tool allowlist, system prompt rules, and budget cap are critical — they replace human-in-the-loop approval with policy-based controls.

Warning: This flag means the agent can perform any allowed action without asking. Ensure your allowlist and system prompt are correctly configured before enabling this.

--model sonnet

Model selection. Specifies which Claude model to use. Options:

Model Use case Cost Speed
haiku Simple, fast responses Lowest Fastest
sonnet Balanced — recommended default Medium Fast
opus Complex reasoning, long research Highest Slowest

The default (sonnet) is recommended for most sessions. The heartbeat daemon might benefit from opus for complex tasks like blog writing, but the budget increase must be considered.

--max-budget-usd 0.50

Per-session spending cap. If the session's API costs exceed this amount, Claude Code terminates the session. This prevents runaway costs from complex prompts, infinite loops, or pathological inputs.

Budget recommendations:

  • Watcher: $0.50 — most messages need a simple response
  • Health: $0.50 — diagnostics and alerts are straightforward
  • Heartbeat: $0.75 — research and writing tasks need more room

Adapting for Your Own Use

Pickle Agent is designed to be forked and customized. The architecture separates identity (who the agent is) from infrastructure (how it runs), making it straightforward to adapt.

Step 1: Define your agent's identity

Replace CLAUDE.md with your agent's system prompt. This should define:

  • Who the agent is (name, role, organization)
  • What it can do (capabilities, integrations)
  • How it should behave (rules, constraints, tone)
  • What it knows about its environment (services, infrastructure)

Step 2: Define your agent's personality

Replace SOUL.md with your agent's personality definition:

  • Communication style (formal/informal, verbose/concise)
  • Tone (professional, friendly, technical)
  • Values (what the agent prioritizes)
  • Humor and personality traits

Step 3: Define autonomous tasks

Replace HEARTBEAT.md with your agent's task checklist. Consider:

  • What should happen every hour? (health checks, email triage)
  • What should happen every few hours? (research, data collection)
  • What should happen daily? (briefings, reports)
  • What should happen weekly? (summaries, content creation)

Step 4: Configure your integrations

Update mcp-config.json with your MCP servers. You might:

  • Keep iMessage (most universally useful)
  • Replace domain-specific servers (Zanda, ICD-11) with your own
  • Add new integrations (Slack, Discord, Jira, GitHub, etc.)

Step 5: Configure your contacts

Update contacts.conf with the phone numbers that should be able to reach your agent.

Step 6: Create domain-specific skills

Replace or add to the skills/ directory with procedures relevant to your domain. Examples:

  • A DevOps agent might have skills for incident response, deployment, monitoring
  • A personal assistant might have skills for scheduling, travel planning, expense tracking
  • A research agent might have skills for literature review, data analysis, report writing

Step 7: Tune environment variables

Adjust intervals, budgets, and thresholds:

  • High-urgency environments: lower PICKLE_POLL_INTERVAL, raise budgets
  • Cost-sensitive environments: raise PICKLE_HEALTH_INTERVAL, lower budgets
  • Complex tasks: raise PICKLE_HEARTBEAT_BUDGET, consider opus model

Step 8: Update infrastructure monitoring

Edit pickle-health.sh to check your specific services:

  • Replace Docker container names with your stack
  • Replace port numbers with your services
  • Adjust disk threshold for your storage situation
  • Add custom health checks (HTTP endpoints, database connectivity, etc.)

Troubleshooting

"Cannot access chat.db" / Permission denied

Cause: The process (Terminal, launchd job) does not have Full Disk Access.

Fix:

  1. Open System Settings → Privacy & Security → Full Disk Access
  2. Add Terminal.app (or iTerm, or whichever terminal app launchd runs under)
  3. If running via launchd, the parent process needs access. Try adding /usr/libexec/launchd or /usr/sbin/cron
  4. Restart the daemon after granting access

Verify:

sqlite3 ~/Library/Messages/chat.db "SELECT COUNT(*) FROM message;"

"Claude CLI not found" / "claude: command not found"

Cause: The claude binary is not in the PATH used by launchd.

Fix:

  1. Find where Claude is installed: which claude
  2. Add that directory to the PATH in your launchd plist:
    <key>EnvironmentVariables</key>
    <dict>
        <key>PATH</key>
        <string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:/Users/YOU/.nvm/versions/node/v20.0.0/bin</string>
    </dict>
  3. Unload and reload the plist

Messages not being detected

Possible causes and fixes:

  1. contacts.conf format wrong. Numbers must be in E.164 format with + prefix and country code. No spaces, no dashes. Example: +61400000000

  2. .last_rowid is ahead of current messages. If .last_rowid contains a number higher than the latest message ROWID, no messages will be processed. Delete the file and restart the watcher:

    rm .last_rowid
    launchctl unload ~/Library/LaunchAgents/com.pickle.watch.plist
    launchctl load ~/Library/LaunchAgents/com.pickle.watch.plist
  3. Messages are from a group chat. The watcher only processes individual (1:1) messages by default. Group chat messages have a different chat_identifier format.

  4. iMessage vs SMS. The agent reads from chat.db, which includes both iMessage and SMS. However, the sender phone format may differ. Check the raw database:

    sqlite3 ~/Library/Messages/chat.db "SELECT ROWID, text, handle_id FROM message ORDER BY ROWID DESC LIMIT 5;"

Health monitor not alerting

Possible causes:

  1. Alert deduplication. The health monitor only alerts on new issues. If the issue has been present since the last check, no alert fires. Check the state file for current LAST_ISSUES.

  2. Work hours. Outside Mon–Fri 8am–6pm AEST, the health monitor silently logs issues without sending iMessages. Check memory/incidents.jsonl for logged events.

  3. Interval too long. If PICKLE_HEALTH_INTERVAL is set very high, checks are infrequent. Reduce the interval for testing.

  4. Boot wait. After system restart, the health monitor waits PICKLE_BOOT_WAIT seconds before its first check. Check logs for "waiting for boot" messages.

Launchd not starting daemons

Diagnose:

# Check if loaded
launchctl list | grep pickle

# Check for load errors
launchctl load -w ~/Library/LaunchAgents/com.pickle.watch.plist 2>&1

# Check daemon logs
cat ~/pickle-agent/logs/pickle-watch.log

Common plist issues:

  • Wrong paths. Every path in the plist must be absolute and correct. No ~ expansion — use /Users/yourname/...
  • Missing directories. The log directory must exist before the daemon starts. Create it: mkdir -p logs/claude-sessions
  • File permissions. The scripts must be executable: chmod +x pickle-*.sh
  • Plist syntax error. Validate: plutil -lint ~/Library/LaunchAgents/com.pickle.watch.plist

Sessions running but not responding

Check session logs:

ls -la logs/claude-sessions/
tail -50 logs/claude-sessions/watch-latest.log

Common causes:

  • Budget exhausted. The session hit --max-budget-usd before completing. Increase the budget.
  • MCP server not starting. Check that MCP server paths in mcp-config.json are correct and dependencies are installed.
  • API key expired/invalid. Check MCP server logs for authentication errors.
  • Model unavailable. If using a specific model (e.g., opus), ensure your API key has access.

High costs

Diagnose: Check memory/incidents.jsonl for session frequency and types. Look at Claude Code's built-in cost tracking.

Reduce costs:

  • Lower PICKLE_HEARTBEAT_BUDGET and PICKLE_MAX_BUDGET
  • Increase PICKLE_HEALTH_INTERVAL and PICKLE_HEARTBEAT_INTERVAL
  • Use haiku model for simple tasks
  • Review skills for unnecessary verbosity
  • Ensure alert deduplication is working (not spawning sessions for repeated issues)

License

This project is released under the MIT License.

MIT License

Copyright (c) 2026 Pickle Agent Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Built with Claude Code by Anthropic.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages