Pimpo
Documentation

Configuration#

This page is the reference for configuring Pimpo, for people who run it themselves or want to know where each setting lives. Most people never need it: the desktop app and the web app cover everything a day-to-day user changes, and the user guide explains those screens. Here you find the command line, the environment variables, the data folder, every stored setting, the model backends, the channels, network access, local models, backups and the settings that matter for safety. The source is at github.com/turbine-dev/pimpo.

Contents#

Running Pimpo#

pimpo with no command runs serve. Flags come after the command and before any other argument (pimpo routines import --active FOLDER, not pimpo routines import FOLDER --active).

CommandFlagsWhat it does
pimpo serve--addr (default 127.0.0.1:7788), --data DIR, --demoRuns the server, the web app and the channels. Prints the login link the first time.
pimpo tokenrotate, --data DIRPrints the owner's login link; rotate replaces it, signing out every browser that used the old one (a running Pimpo follows within seconds, unless PIMPO_TOKEN fixes the token).
pimpo update--check, --beta, --version vX.Y.Z, --rollbackReplaces this binary with the latest release after checking it against the release's checksums, whose signature must match a release key built into Pimpo, keeping the old one as pimpo.previous; --rollback puts it back. Restart Pimpo afterwards: its first start keeps a snapshot of the data (before-VERSION). The desktop app updates itself instead.
pimpo versionPrints the version.
pimpo export FILE.pimpo--data DIRWrites everything to one file, sealed as a whole with a passphrase. Refuses to overwrite an existing file.
pimpo import FILE.pimpo--data DIR, --unsealedReplaces the data with a backup. Pimpo must be stopped. What was there is kept aside. --unsealed also takes a file exported before format 2, whose contents cannot be checked.
pimpo snapshots--data DIRLists the local snapshots, with date and size.
pimpo snapshot [LABEL…]--data DIRTakes a snapshot now.
pimpo restore NAME--data DIRGoes back to a snapshot. Pimpo must be stopped. The current state is snapshotted first.
pimpo routines import FOLDER--data DIR, --activeInstalls routines from a folder in the repository layout (routines/<id>/routine.json) after checking their manifest, tests and audit. They arrive paused unless --active.
pimpo report--data DIR, --days N (default 21), --jsonHow the routines did in real use: runs, failures, silent failures (scheduled runs that never happened while Pimpo ran), late runs, times Pimpo was off, repairs, approvals and cost. Also GET /api/report?days=N. See Validating on real accounts.
pimpo local list--data DIRLists the local model catalog; ✓ marks what is installed.
pimpo local install ID…--data DIRDownloads catalog items, with the same checks as the app.
pimpo local remove ID…--data DIRRemoves installed items.
pimpo connector check DIRLoads a connector, lists its capabilities and runs its contract. Keys come from environment variables named as the connector's env.
pimpo connector openapi SPEC [NAME DIR]--only op1,op2 or --only get, --header NAMEReads an OpenAPI description (file or URL). Without --only and NAME DIR, lists the operations; with them, writes DIR/connector.json.
pimpo migrate openclaw|hermes--home DIR, --data DIR, --apply, --secrets, --trustShows what would come over from OpenClaw or Hermes; --apply imports it (Pimpo must be stopped), --secrets also brings the Telegram token and mail password, --trust treats imported memories and rules as your own words. Takes a snapshot first.
pimpo gallery keygen--out KEYFILECreates a signing key for publishing routines.
pimpo gallery build DIR--key KEYFILE, --author ID, --name NAMESigns DIR/routines/*.json into DIR/index.json, then verifies it.
pimpo gallery sign-authors DIR--key KEYFILEMaintainers: signs the authors in DIR/index.json with the gallery root key.
pimpo gallery verify INDEXVerifies a gallery index (file or URL), its authors' signature included. Fails on any problem.
pimpo protect suggest--domain D or --pattern REGEX, --capability NAME, --reason TEXTPrints a protection-list entry to propose by pull request.
pimpo protect sign DIR--key KEYFILEMaintainers: signs DIR/entries.json into DIR/list.json.
pimpo release sign FILE--key KEYFILE or --key-env NAME, --out SIGFILEMaintainers: writes FILE.sig, the Ed25519 signature the release uses for checksums.txt (Releases and support).
pimpo protect verify FILEChecks a signed protection list.

serve does a few things before it listens: it finishes an import started in the web app, applies a snapshot restore chosen in the web app, and takes a snapshot when the version changed since the last start. While it runs it takes a snapshot every 24 hours.

# Start with the defaults: ~/.pimpo, loopback only, port 7788
pimpo serve

# Try it with a sample mailbox and calendar, no accounts, no model costs
pimpo serve --demo

# A second, separate install
pimpo serve --data ~/pimpo-test --addr 127.0.0.1:7799
# Move to another computer
pimpo export ~/Desktop/pimpo.pimpo
pimpo import ~/Desktop/pimpo.pimpo

# Go back to an automatic snapshot
pimpo snapshots
pimpo restore 20260928-101500-daily
# Local models from the terminal
pimpo local list
pimpo local install kokoro-multi

# Routines from a folder, paused until you review them
pimpo routines import ~/src/my-routines
# Check a connector before copying it into ~/.pimpo/connectors/
EXAMPLE_KEY=... pimpo connector check examples/connectors/hnsearch
pimpo connector openapi --only get https://api.example.com/openapi.json example ./example

--demo without --data uses a separate folder (~/.pimpo-demo), so a demo never touches your real data. The demo does not start Tailscale or the home-network address.

Environment variables#

Pimpo reads only these. None of them is needed for normal use.

VariableRead byWhat it does
PIMPO_HOMEevery commandThe data folder, when --data is not given.
ZODIM_HOMEevery commandSame as PIMPO_HOME, from when Pimpo was called Zodim. PIMPO_HOME wins.
PIMPO_BACKUP_PASSPHRASEexport, importThe backup passphrase, instead of asking on the terminal. At least 12 characters for a new export.
PIMPO_TOKENserveFixes the login token instead of the stored one, and stores it. The desktop app and browser tests use it.
PIMPO_TELEGRAM_APIserveThe address of a self-hosted Telegram Bot API server. Empty uses Telegram's.
PIMPO_DESKTOP_NOTIFYserveAny value: show notices as system notifications, and complete PATH from the usual install folders and the login shell (so the claude, codex and opencode CLIs are found when started from the Finder). Set by the desktop app.
PIMPO_EXIT_WITH_PARENTserveAny value: stop when the process that started Pimpo exits. Set by the desktop app.
OPENCLAW_STATE_DIR, OPENCLAW_CONFIG_PATH, OPENCLAW_WORKSPACE_DIRmigrate openclawWhere OpenClaw keeps its data, config and workspace, when not the defaults (~/.openclaw).
HERMES_HOMEmigrate hermesWhere Hermes keeps its data, when not ~/.hermes.
Any name a connector declaresconnector checkThe connector's keys for the contract run.

PATH, HOME and SHELL are used as usual to find programs. An external connector process gets only PATH, HOME, LANG and the keys it declares; nothing else from Pimpo's environment reaches it (see Writing a connector).

The desktop app starts pimpo serve --addr 127.0.0.1:<a free port> itself, with a fresh PIMPO_TOKEN, PIMPO_EXIT_WITH_PARENT=1 and PIMPO_DESKTOP_NOTIFY=1.

The data folder#

Everything Pimpo keeps is in one folder: ~/.pimpo by default. Change it with --data DIR on any command, or with PIMPO_HOME. The folder is created with owner-only permissions (0700).

Pimpo was called Zodim, and Vigia before that. When ~/.pimpo does not exist yet and neither --data nor PIMPO_HOME is set, Pimpo moves ~/.zodim (or else ~/.vigia) to ~/.pimpo and renames its database files, snapshots included. It refuses while the old app is still running, and asks you to quit it first.

PathWhat it holds
pimpo.db (with -wal, -shm)The SQLite database: the event log, settings, rules, routines and their runs, people, paired devices, receipts, costs, and the vault's encrypted secrets.
vault.keyThe vault key, only when the system keychain is not available. See Safety settings.
memory/The memory: Markdown files, one per topic, versioned with git.
connectors/<name>/Installed external connectors, each with its connector.json. Connectors added from Connections (JSON, OpenAPI, MCP) are written here too.
local/Downloaded voices, the voice engine and speech-to-text models. A catalog.json here replaces the built-in catalog (see Local models).
models/ggml-base.binThe whisper.cpp model used for voice notes when no local speech-to-text model is installed. You put it there yourself.
media/Recent audio Pimpo made for you (the last 60).
cache/speech/Cached audio for Listen in the chat (the last 400).
phone/photos/Photos taken in Phone on a paired phone, for routines to read; kept on this machine and not included in exports.
browser/profile/Pimpo's own Chrome profile, with the sessions you signed in to for the browser; it is not included in exports.
skills/Installed skills, one folder each (skills/<id>/SKILL.md and its files); .staging/ holds skills being reviewed.
tailscale/The state of the built-in Tailscale node, when From anywhere is on.
snapshots/Local snapshots: a copy of pimpo.db and memory/ each. The last 10 are kept.
restore-pendingA snapshot chosen in the web app, applied at the next start.
quarantine/Databases that failed their check at start, each in a folder named for the time, with its journal and the reason. Pimpo never deletes them.
recovery.jsonPresent while Pimpo is in recovery: the quarantine folder, the reason and the recovery link's token.
import-pending/A backup imported from the web app, applied at the next start. Its secrets wait there only until then.
before-import-<date>/What was here before an import. Delete it when you no longer need it.
pimpo.pidThe running server's process id. import, restore and migrate --apply use it to refuse while Pimpo runs.

Routines are not files in the data folder: they live in the database. To keep them as files, use Routines › Repository (a folder you choose) or see How routines work.

Outside the data folder, the desktop app caches the shell's PATH in the user cache folder (pimpo/shell-path), and the vault key is in the system keychain under the service pimpo.

Settings#

Settings are stored in the database and changed in Settings in the app. Almost every change takes effect at once, without a restart. The web app reads and writes them with GET /api/settings and PUT /api/settings; a PUT changes only the fields it carries, so a client can send just what changed. Both need a login (see Network and access).

Settings has these sections: General, Phone, Models, Notifications, Backup, Privacy, Labs and Bring over from another agent. Preferences wait in a bar at the bottom until you press save; cards with their own button (models, backups, pairing) save at once.

Language and time#

SettingDefaultWhereWhat it does
localept-BRSettings › General › Language for the app and messagesThe language of the app, the channels, approvals and what routines write. Also the language for voice-note transcription.
zonethe computer's time zoneSettings › General › Time zoneAn IANA time zone (America/Sao_Paulo). Schedules, "today" in the daily limit and dates use it. An unknown zone is refused.

Daily spending limit#

SettingDefaultWhereWhat it does
Daily limit, in US dollars1Settings › General › Daily spending limit, and the welcome screenChecked before every paid model call and every paid voice. A call that could pass it is refused. 0 means no limit.

The limit is stored apart from the other settings and changed with PUT /api/budget {"daily_usd": 5}. The day follows zone. Calls paid by a subscription (below) do not count. After 80% of the limit, the automatic choice stops using the strong model and the high thinking level.

Models and spending per person#

SettingDefaultWhereWhat it does
A person's modelsall of the house'sPeople › Models and spendingThe models the person may use. Every call for them (chat, routine, job, judgment, written text) uses only a model that the house, the person and the assistant answering all allow; the automatic choice falls back to the cheapest of them, and a model chosen by hand outside them is refused.
A person's daily limit, in US dollarsnone; 0.25 for a guestPeople › Models and spendingChecked before every paid call made for that person, together with the house's limit. It must be at most the house's. 0 (empty) means none of their own; for a guest it means the guest default.
An assistant's modelsall of themAssistants › Models it may useNarrows further what a conversation with that assistant may use.

They are stored with the person (people in the key-value table) and the assistant (assistants), and changed with PUT /api/people/{id}/limits {"models": ["haiku"], "daily_usd": 0.5} (owner only) and the assistant's models. GET /api/me/limits gives each person their own: models, limit and today's spending. Every cost.recorded event names the person it was for; costs from before people existed are the owner's. The owner's limits are the house's.

Models per job#

The welcome screen sets all three at once (Which model thinks for Pimpo): it offers what this computer already has (Claude Code, Codex, opencode, Ollama or LM Studio models) or one API key. With a key, Pimpo lists the provider's models, picks its current main model for exploring and compiling and a light one for judgments, requires a known price for both, tests the main one with a single word, and only then saves. The same happens with POST /api/setup/model and {"kind": "provider", "provider": "openai", "key": "…"} (or kind claude_code, codex, opencode, ollama, lmstudio with a model). A job left on Claude Code on a computer without it says so and points to Settings › Models.

Pimpo has three jobs, each with its own model. A model is named in one of these ways: sonnet, opus, haiku or claude-… (Claude Code); codex or codex:<model> (Codex); opencode:<provider>/<model> (opencode); or <provider>:<model> for a provider's API (anthropic:…, openrouter:…, ollama:…). See Models.

SettingDefaultLabel in Settings › Models › Models in useUsed for
explore_modelsonnet, or what the welcome screen choseDoing tasks and chattingExplorations (doing a task the first time) and chats.
compile_modelsonnetWriting routinesTurning a task into a routine with tests.
judge_modelhaikuJudgments and short textsThe routines' yes-or-no questions and short texts, when the judgment backend is Your model or as the last step of the others.
modelsnoneProviders and On this computerYour API models, each with its price in USD per million tokens in and out (0 to 1000). A provider model must be in this list, with its price, before any job may use it. opencode models carry no price: opencode reports each call's cost.
fallbacksnoneIf it fails: next to each jobUp to four models per job (explore, compile, judge), tried in order when the model fails for a provider reason: a refused key, no credits, a rate or usage limit, an outage, a missing model or CLI. Your own daily limit and a cancelled task are never retried. You are told once when a job falls back and once when its model answers again.
effortsthe model's ownThinking next to each jobThe default thinking level per job: low, medium, high or max.

A routine can override the judgment model and thinking level for itself, in its own settings (Model for judgments and texts).

Automatic model choice in chats#

SettingDefaultWhereWhat it does
auto_offfalseSettings › Models › Automatic choice in chatsWhen on, every chat request stays on explore_model.
auto_lightClaude Code's haiku, or your cheapest priced modelLight modelWhere simple requests go.
auto_strongClaude Code's opus, or your dearest priced modelStrong modelWhere hard requests go, while under 80% of the daily limit.

Each request is weighed as simple, normal or hard. With a Jev key, Jev weighs it and only a probability of 60% or more moves it; otherwise a few plain rules do (short plain questions are simple, long or planning requests are hard). Normal requests, and any doubt, stay on explore_model. The Haiku and Opus defaults apply only when explore_model is a Claude Code model and the claude CLI is installed. A model equal to explore_model is not used as light or strong.

The thinking level follows the same weighing: low for simple requests, high for hard ones (not after 80% of the limit), and the job's efforts otherwise. In a chat, the pill under the message box fixes a model or a level for that conversation. On channels, /model and /think do the same; see the user guide.

Thinking levels#

low, medium, high and max, or empty for the model's own default. Each backend maps them to its own scale: Codex calls max "xhigh"; the OpenAI-compatible providers stop at high, so max is sent as high; OpenRouter gets it as reasoning.effort; DeepSeek, Mistral and Qwen (DashScope) get no level, because they choose thinking by model. If a provider refuses the level, the call is repeated without it.

Judgments#

SettingDefaultWhereWhat it does
judge_backendlocalSettings › Models › Who answers judgmentslocal (Local model), laya (Laya on this computer), jev (Jev) or llm (Your model).
laya_urlemptyLaya's addressA Laya server on this computer (pip install "laya[serve]" && laya-serve, port 8000). With it set, the local judge asks Laya first; laya needs it (or uses http://127.0.0.1:8000). A server started with LAYA_API_KEY needs that key in the vault as laya.key.
local_judge_urlhttp://127.0.0.1:11500Local model addressPimpo's small judgment model (tools/judge/serve.py), or any server speaking the Judge API.
ollama_modelqwen3:1.7bnot in the appThe Ollama model the local backend tries when the local judge does not answer.

With local, the local judge answers first (then Ollama, at ollama_url); answers it is unsure about go on to Jev, when its key is set, and then to your model. With jev or llm, that backend goes first and the others follow. The Jev key is set in Connections (Jev (calibrated judgments, optional)).

Voices#

SettingWhereWhat it does
voiceSettings › Models › Voice for audio › RoutinesWhat reads the routines' audio: auto (a downloaded voice for the language, else the system's), local (a downloaded voice only), system, openai or elevenlabs.
voice_model, voice_namesameFor OpenAI, the model (tts-1 or tts-1-hd) and the voice; for ElevenLabs, the voice.
chat_voiceSettings › Models › Voice for audio › Listen in chatWhat reads answers aloud with Listen. Empty uses the routines' voice. It also accepts browser, the browser's own voice, which only the chat may use.
chat_voice_model, chat_voice_namesameAs above, for the chat.

The OpenAI voice uses your OpenAI provider key and is billed per character, checked against the daily limit. ElevenLabs uses its own key, set on the same card. Local voices are downloaded in Settings › Models › Download models.

Notifications, email requests, labs and more#

SettingDefaultWhereWhat it does
suggest_offfalseSettings › Notifications › SuggestionsStops the daily suggestions. When on, once a day after 9:00 (only on Mondays after three rounds nobody took up) the judgment model sees metadata only (senders and subjects of the last 21 days' email, titles of the next 14 days' events, the routines and those that failed) and may offer up to two routines, within the daily limit and at most $0.02 a round.
learn_offfalseSettings › Notifications › Learn my preferencesStops learning. When on, once a week the judgment model sees only the owner's own requests and decisions of the last seven days (approvals denied or made permanent, suggestions taken or declined), never what the agent read, and may add up to five preferences to memory as learned, at most $0.03 a round. A preference the owner removes is not learned again. POST /api/learn runs a round at once.
lesson_digest_offfalseSettings › Notifications › Weekly lessons digestStops the weekly notice of lessons waiting. When on, after 10:00 and at most once every seven days, each person with lessons waiting gets a notice with how many and, when Tailscale or the home network is on, a link to Lessons. It has no buttons and no numbers to answer: lessons are decided only on the page. It is a task notice, so muting tasks silences it too.
labs_onnoneSettings › Labs › New powersFeatures that give the agent more reach and start off: code_sandbox (Run code in isolation: the code.run capability, which needs Docker; see RFC 0001) browser (Use a browser) and companies (Companies of agents: see RFC 0004).
mutenoneSettings › NotificationsKinds of notices kept off Telegram, WhatsApp and system notifications: task (Task results), failure (Routines that failed), backup (Backup problems). They stay in Needs you. Approval requests cannot be silenced.
email_channelfalseSettings › Notifications › Ask by emailChecks your mailbox every minute for messages you sent to yourself with Pimpo: in the subject, and treats each as a request. Only your own address counts.
labs_offnoneSettings › LabsTurns off newer features: memory_organize (Organize memory every night), meaning_search (Search memory by meaning), mcp_registry (Explore the MCP registry). All are on by default.
protection_networktrueSettings › Privacy › Download the protection listDownloads the signed community protection list every day. Only the list is downloaded; nothing of yours is sent.
protection_urlthe project's listnot in the appWhere the protection list comes from.
gallery_urlthe project's indexnot in the appThe routine gallery index. A local path works too.
ollama_url, lmstudio_urlhttp://127.0.0.1:11434, http://127.0.0.1:1234Settings › Models › Local server addressesWhere Ollama and LM Studio answer, without /v1.
custom_urlnoneProviders › OpenAI-compatible serverThe address of your own OpenAI-compatible server, ending in /v1.

Models#

Settings › Models (also reached from Connections › Intelligence) finds what is on this computer and lets you add providers. Every model answers one real word, capped at one cent, before it is added.

On this computer#

BackendHow Pimpo finds itModel names
Claude Codethe claude command on PATHsonnet, opus, haiku, claude-…
Codexcodex on PATH, or inside the ChatGPT app (/Applications/ChatGPT.app or ~/Applications/ChatGPT.app)codex, codex:<model>
opencodeopencode on PATH, or ~/.opencode/bin/opencodeopencode:<provider>/<model>, chosen under opencode › Choose
Ollamaa server at ollama_urlollama:<model>
LM Studioa server at lmstudio_urllmstudio:<model>

Claude Code is the default for every job. Each CLI runs with its own tools off, so the model reaches the world only through Pimpo's tools, where rules and approvals apply. Codex runs with your Codex config ignored and a read-only sandbox. opencode runs with your project config, Claude Code integration, external skills, auto-update and sharing turned off, every opencode tool denied, and its session deleted after each run. The user guide says what was checked against each real CLI.

Providers#

Paste a key in Settings › Models › Providers and choose among the provider's models. Keys are kept in the vault.

ProviderIdAddress
Anthropicanthropichttps://api.anthropic.com/v1 (Messages API)
OpenAIopenaihttps://api.openai.com/v1
Google Geminigooglehttps://generativelanguage.googleapis.com/v1beta/openai
OpenRouteropenrouterhttps://openrouter.ai/api/v1
Qwen (Alibaba DashScope)dashscopehttps://dashscope-intl.aliyuncs.com/compatible-mode/v1
DeepSeekdeepseekhttps://api.deepseek.com/v1
Groqgroqhttps://api.groq.com/openai/v1
Mistralmistralhttps://api.mistral.ai/v1
xAI Grokxaihttps://api.x.ai/v1
Ollamaollamaollama_url, no key
LM Studiolmstudiolmstudio_url, no key
OpenAI-compatiblecustomcustom_url; a key only if the server asks

All but Anthropic use OpenAI's chat completions API. Prices come from OpenRouter's public catalog of list prices. A model the catalog does not know needs its price typed in: without a price the model does not run, because the daily limit counts every call. A call also stops if it would cost more than it is allowed.

The list of models comes from each provider's own GET /models (Anthropic's in pages; OpenRouter's includes prices), asked with the key in the vault. It is kept for a day (a minute for Ollama and LM Studio), asked again with If-None-Match / If-Modified-Since so an unchanged list costs a 304, and Look again in the provider's list (or GET /api/models/catalog/{provider}?fresh=1) asks at once. Once a day Pimpo also looks at the providers you have models from. Your own models keep the price you set. A model a provider starts listing after the first look is marked New for two weeks (New · price unknown when no catalog price is found; it still needs your price before it runs). One of your models a provider stops listing is marked Retired; jobs, chats and routines that use it suggest switching, and you are told once if a job uses it. GET /api/models/retired lists them. Only a list the provider actually returned retires a model; a provider that cannot be reached changes nothing.

Anthropic compaction#

SettingDefaultWhereWhat it does
compact_offfalseSettings › Models › Summarize long conversationsStops Anthropic's server-side compaction.
compact_at0 (150000)Starts at (tokens)Input size where Anthropic summarizes the older turns; 50000 to 1000000.

With an Anthropic API model, once a conversation or job has grown to half of compact_at, requests carry the compact-2026-01-12 beta and context_management with a compact_20260112 edit triggered at compact_at input tokens. Anthropic then replaces the older turns with a summary block, which Pimpo sends back as it came. The summary's own tokens (usage.iterations of type compaction, which the top-level usage leaves out) are counted at the model's prices. A model that refuses the beta answers without it.

Subscription or money#

Some backends are paid by a subscription you already have. Pimpo asks each CLI how it is signed in, at most every ten minutes:

A subscription call spends no money and does not count toward the daily limit. What the same work would cost on the API is recorded apart and shown under Cost › Through your subscription. Everything else, including Claude Code signed in with an API key, is real spending.

Channels#

Channels are set up in Connections; each card says what to create and what to paste. Tokens go to the vault. This is only what matters for configuration; the user guide covers daily use.

ChannelWhat Pimpo needsNotes
Telegrama bot token from @BotFatherChecked with Telegram before it is saved; takes effect without a restart. PIMPO_TELEGRAM_API points at a self-hosted Bot API server. More bots for routine notices: Connections › Extra Telegram bots.
WhatsAppthe Cloud API access token, the phone number id and the app secretMeta calls Pimpo at /webhook/whatsapp, so Pimpo needs a public https address (for example Tailscale with Funnel, below). Pimpo makes the verify token; every call must carry Meta's signature.
Email requeststhe mail connection (Gmail or IMAP) and email_channel onSee Settings.
Slacka bot token (xoxb-) and an app token (xapp-), Socket Mode onDirect messages. Pair by sending pimpo and the pairing code shown in Connections.
Discorda bot tokenPrivate messages, paired the same way.
Signalthe address of a running signal-cli daemon (http://127.0.0.1:8080) and, optionally, its numberPaired the same way. Messages are end-to-end encrypted up to the daemon.
Slack or Discord notices onlyan incoming webhook of a channel of your ownMessages to you only; no conversation.
Your own bridgethe channel APINotices go out as signed webhooks; requests and button taps come back through the API. See Extending Pimpo.

Only the paired owner is answered on Slack, Discord and Signal; strangers get nothing. A channel that keeps failing for three minutes is reported on the others.

Push triggers#

A routine that watches Gmail or Slack can hear of new items as they happen instead of checking every few minutes (its trigger settings: Hear of it as it happens). A GitHub repository can start a routine through its own webhook. While a push is live the routine is still checked once an hour, in case a push was lost; when push is not set up, or a Gmail watch lapses, it goes back to checking at its own interval.

Gmail (Google Pub/Sub)#

Gmail push works for a mailbox signed in with Google in Connections (today, the administrator's; mailboxes connected with an IMAP password keep polling). Each person's watch is for their own mailbox and wakes only their routines. The administrator sets up Google Cloud once, in the same project as the OAuth client used to sign in:

  1. Enable the Gmail API and the Cloud Pub/Sub API in the project.
  2. Create a topic: gcloud pubsub topics create pimpo-gmail.
  3. Let Gmail publish to it: gcloud pubsub topics add-iam-policy-binding pimpo-gmail --member=serviceAccount:gmail-api-push@system.gserviceaccount.com --role=roles/pubsub.publisher.
  4. Create a service account for the push to sign as: gcloud iam service-accounts create pimpo-push.
  5. Give Pimpo a public https address: Tailscale with Funnel in Settings › Phone. The push address is that address followed by /push/gmail (shown in the routine's settings).
  6. Create the push subscription with authentication: gcloud pubsub subscriptions create pimpo-gmail-push --topic=pimpo-gmail --push-endpoint=https://YOUR-NAME.ts.net/push/gmail --push-auth-service-account=pimpo-push@PROJECT.iam.gserviceaccount.com. Leave the audience at its default (the push address).
  7. In Pimpo, on a routine that watches Gmail, turn on Hear of it as it happens and enter the topic (projects/PROJECT/topics/pimpo-gmail) and the service account (pimpo-push@PROJECT.iam.gserviceaccount.com).

The setup is kept as push.gmail (topic, account and an optional audience, if the subscription uses a custom one) and changed with PUT /api/push/gmail, the administrator's only. Pimpo starts a watch for each person who has a routine with push on, renews it every day (Gmail's last 7 days) and stops it when no routine wants it. Every push must carry a Google-signed ID token for that audience and service account; the push only names a mailbox, and Pimpo asks Gmail itself (with that person's token) what reached the inbox.

Slack#

The Slack link is the administrator's (see Channels). For routines that react to Slack, also subscribe the app to message.channels (and message.groups for private channels) and app_mention, give the bot channels:history (groups:history) and app_mentions:read, and add the app to the channels. Messages arrive over the Socket Mode connection already open; they are kept (14 days) only while a routine watches slack.messages, and only the administrator's routines read them.

GitHub#

On the routine's page, turn on Start from GitHub: Pimpo shows the address (/github-hook/<routine> on the public address) and a secret, once. In the repository's Settings › Webhooks, add the address with content type application/json and the secret, and choose the events. New secret replaces it at once. Deliveries must be signed with the secret (X-Hub-Signature-256), carry at most 1 MB, and each delivery id starts the routine once.

Network and access#

For a server (Docker or systemd) and reaching it safely from elsewhere, see Running on a server.

Listening address#

pimpo serve listens on 127.0.0.1:7788, this computer only. --addr changes it. The server speaks plain http, so an address other than loopback sends your login token in the clear over the network; use the two ways below instead of --addr 0.0.0.0:….

Logging in#

The first time it starts, Pimpo prints a link: http://127.0.0.1:7788/auth?token=…; pimpo token prints it again and pimpo token rotate replaces it. Opening it sets an http-only, same-site session cookie for a year. The token is created once and kept in the database, so the link stays valid across restarts until rotated; PIMPO_TOKEN replaces it. A link the owner makes for someone else is an invite: it works once, within 15 minutes, and opens a session with a token of its own; the person sees the device in Account and in their activity, and can sign it out there. Requests that change something with the browser's cookie must come from Pimpo's own page (the Origin, or Sec-Fetch-Site, must match the address), and JSON bodies must say Content-Type: application/json; clients that send Authorization: Bearer are exempt. Every answer carries a Content-Security-Policy that allows only Pimpo's own scripts and forbids framing (the Telegram Mini App page is the one exception, below). Every API call needs the cookie or Authorization: Bearer <token>, with the session token or a device token. The web app's files, /api/health and routes that check their own credential (the WhatsApp webhook, routine webhooks at /hook/…, the Google and Spotify sign-in callbacks, the per-exploration MCP endpoint, the Telegram Mini App sign-in) are the only ones reachable without it.

Phones and other devices#

Settings › Phone (Open on your phone) turns on one or both ways in. Both are remembered across restarts.

Generate code pairs a device: it gets its own token, shown once as a link and QR code, and kept only as a hash. Choose whose device it is: it signs in as that person and acts only for them. Revoke one device with the trash icon next to it; the others keep working. Device tokens also authenticate bridges and scripts that use the API.

Sign-in and sessions#

Local models#

Settings › Models › Download models downloads voices, speech-to-text models and, through Ollama, language models; pimpo local does the same from the terminal (see Running Pimpo).

Without a local speech-to-text model, voice notes use whisper.cpp (whisper-cli or whisper-cpp on PATH), ffmpeg, and the model at ~/.pimpo/models/ggml-base.bin.

Backups and moving#

There are three kinds of copies. The user guide has the details.

Keep the passphrase outside Pimpo: without it no one can open a backup.

Safety settings#

The threat model says what Pimpo defends against. These are the settings that shape it.

The vault#

Secrets (tokens, API keys, passwords, the backup passphrase) are encrypted in the database. The key is 32 random bytes kept in the system keychain (service pimpo, account data-key); keys kept under the old names zodim and vigia are carried over. On a system without a keychain, the key is a vault.key file in the data folder with owner-only permissions: then anyone who can read the data folder can read the secrets, so protect that folder. Code outside the vault holds only a secret's name; the value is read when a connector makes a request. An export removes the encrypted secrets from its copy of the database and carries them re-sealed with your passphrase instead, so the file never depends on this computer's vault key.

Outside password managers#

A secret stored as op://Vault/Item/field (or op://Vault/Item/section/field) or vault://<mount>/data/<path>#<field> is a reference: the vault keeps the reference, and reads the value when a connector asks for the secret. Values read stay in memory only, for five minutes (a failure for 30 seconds), and are forgotten when the password manager's settings change. Backups and exports carry the reference, not the value.

The credentials are secrets in the vault too: pm for the house (set by the owner in Connections › Password managers, GET/PUT/DELETE /api/password-managers/{onepassword|hashicorp}, POST /api/password-managers/{kind}/test) and person.<id>.pm for each person (set in Account, with the same routes). A secret named person.<id>.… resolves only with that person's credentials; everything else resolves with the house's. POST /api/secrets/check {"reference": "…"} resolves a reference once with the caller's credentials and answers only {"found": true} or an error naming the reference.

ManagerHow Pimpo readsNotes
1Password, service accountop read --no-newline <ref> with OP_SERVICE_ACCOUNT_TOKENNeeds the op CLI on the PATH. It runs with only PATH, HOME and the token in its environment, and a 15-second limit.
1Password, app on this computerop read signed in through the desktop appHouse only. Turn on Settings › Developer › Integrate with 1Password CLI in the 1Password app.
1Password ConnectGET /v1/vaults, /items and /items/{id} with the Connect tokenThe vault and item are found by name; the field by label or id, within the section when the reference names one.
HashiCorp VaultGET /v1/<path> with X-Vault-Token (and X-Vault-Namespace when set), reading data.data.<field>Token, or AppRole (POST /v1/auth/<mount>/login, mount approle by default); an AppRole token is kept in memory while it lasts, at most 30 minutes.

Addresses must be https; plain http is allowed only to this computer (localhost, 127.0.0.1, ::1) and only for the house's managers. Redirects are not followed, so a token is never sent to another address.

Rules and approvals#

Spending#

The daily limit (Settings › General) is checked before every paid call, so it cannot be passed by more than one call. Fallbacks never work around it. See Daily spending limit.

What leaves the computer#

This page is built from the repository's documentation. Improve it on GitHub.