Threat model#
Pimpo acts for people: it reads their email, sends messages and changes things. This page says what it defends against, how, and what it does not promise. Each defense has a test that fails if it breaks.
Assets#
- Accounts: email, calendar, chats, services with their tokens.
- The house's data: memory, receipts, routines.
- The owner's intent: rules, approvals and budget.
Attackers#
| Attacker | Can |
|---|---|
| A stranger who writes to you | put text in an email, page, calendar invite or photo that Pimpo reads |
| A malicious routine or connector author | publish to the gallery, or offer a connector |
| Someone on your network | reach ports on your machine |
| Another person in the house | use Pimpo with their own role |
| A thief with your phone | open the paired app |
Defenses#
| Threat | Defense | Test |
|---|---|---|
| Prompt injection in content read | External content is data. Irreversible actions always need approval. Explorations only simulate changes. | TestGateInjectionLeaksNoSecret |
| Poisoned memory | Anything the agent notes is low trust and never reaches later instructions. Facts are single lines, and topics cannot write outside the memory. | TestGatePoisonedMemoryNeverBecomesInstruction (50 attacks) |
| A fact hidden from its source | Pimpo records where every fact came from (the conversation, task, routine, job, email, import or person that gave it), not the agent: the agent may add the email it read a fact in, and that is kept only when the sender appears in mail the run read, next to the run's own source. Forgetting a source removes every fact that came even partly from it; a low-trust copy of what the person typed never attaches a source that could remove it. Sources and forgetting are per person: only a fact's author sees its source's details and forgets it, and the history shows others' forgetting only as whose it was. | TestNotedFactsKeepTheirSources, TestSourcesArePrivate, TestForgetSourceRemovesExactlyItsFacts, TestEveryFactKeepsItsSources |
| A destructive request after context was lost | Rules live outside the model, and deletes go to the trash. | TestGateTwoHundredEmails |
| Runaway cost or loops | The budget is checked before every model call, with call limits per run. | TestGateLoopStopsAtBudget |
| One person running up the house's bill, or using models they were not given | Every cost is recorded for the person it was for (chats, routines, jobs, judgments and written text), and a paid call is refused before it starts when it could pass that person's own daily limit or the house's; a guest has a small limit by default. Every model call keeps to the models the house, the person and the assistant all allow, enforced where the model is called, not in the interface: the automatic choice and fallbacks stay inside, and a model chosen by hand outside is refused. Only the owner sets these; the owner sees whether a person reached their limit, never how much they spent, and each person sees their own spending. | TestAPersonsLimitBlocksOnlyThem, TestGuestsHaveASmallLimitOfTheirOwn, TestAllowedModelsAreTheIntersection, TestAutomaticChoiceStaysInsideTheAllowed, TestAnExplicitModelOutsideIsRefused, TestEachPersonHasTheirOwnLimit, TestJudgmentsAreThePersons |
Sites the agent drives (browser.*) | Off until the owner turns it on; Pimpo's own Chrome profile with Chrome's sandbox kept on; every request the page makes (redirects, images, scripts, fetches, form posts) is checked before Chrome sends it and refused off the hosts the run opened within its scope or on this computer or a private network, WebSockets and WebRTC are removed, and the page reached is checked again; pressing a button is irreversible and asks first; passwords are never typed; page text returned as the site's words (RFC 0002). | TestBrowseFillAndSubmit, TestBrowserOnlyWhenTurnedOnAndOnlyWhereAllowed, TestRequestsOutsideScopeNeverLeave, TestBrowserRefusesPrivateAddresses, TestBrowserOpenAddsNoHostFromABadAddress |
Reading the web for a routine (web.read, http.getJSON, rss.read, one-click unsubscribe) | The scope is the host Go really connects to (URLs with a user name are refused); every read and redirect stays on that host; connections to this computer, private networks, CGNAT/Tailscale and IPv6 forms that embed them are refused when dialling; an unsubscribe link follows no redirect. | TestHostIsWhereGoConnects, TestBlocked, TestGetJSONWithinScope, TestPrivateAddressesAreRefused, TestRSSAndAtom, TestOneClickUnsubscribeStaysOffPrivateAddresses |
| iMessage and the unofficial WhatsApp | Both answer only the paired owner. iMessage reads Messages' database read-only, only iMessages (never SMS, whose sender is easy to fake) that arrive after it starts, and sends with the text as an argument, never inside the script. The unofficial WhatsApp starts only when the owner chose it in Labs and never approves: numbered answers are ignored and notices say to choose elsewhere. | TestIMessageHandsOverNewMessagesOnly, TestUnofficialWhatsAppNeverApproves, TestUnofficialWhatsAppWaitsForLabs |
| Long jobs | A job runs only after the owner sees the plan and starts it, within a budget the owner sets (at most $20) and the day's limit. Each part is an exploration limited to the capabilities the plan gave it (unknown ones are dropped, none means none), so it reads for real and writes only as proposals. The parts' results reach the report as data. | TestJobPlansRunsPartsAndReports, TestJobStopsAtItsBudget, TestJobResumesAfterARestart |
| Voice conversation | The microphone is open only while the owner has the conversation on, and speech is transcribed on the owner's Pimpo (Whisper), never by the browser's cloud recognition. A spoken answer is a new message and never an approval: proposed actions wait for a tap, so someone talking near the device, or audio played to it, cannot approve anything. | ui/src/test/conversation.test.ts |
| The phone as a node | Each paired phone reports only what it shares, chosen on the phone itself; a report from a phone that does not share it is refused. Automations use a separate key that can report events and nothing else, and each phone's key and login are revoked with the phone. Positions are compared with the owner's places and dropped; photos stay on the computer and their text reaches routines as data, never as instructions. | TestPhoneArrivingHomeAndAPhotoOfABill |
| Learned preferences | Learned only from the owner's own requests and decisions, never from content the agent read (nor from suggestions, whose titles come from email metadata); shown to the agent as learned and not confirmed, so they shape answers but never pass a rule or approve an action; the owner confirms or removes each, and a removed one is never learned again. | TestLearnsOnlyFromTheOwnersOwnWords |
| Lessons (what Pimpo proposes to keep) | A lesson is only a proposal: learned preferences, notes the agent took (low trust, maybe read in an email), repeated tasks, repairs and suggestions are shown with their evidence and change nothing until the person accepts; accepting goes through the usual path (confirming the fact, compiling the exploration with its checks and audit, keeping the old version in the history), so a lesson never bypasses rules or approvals. Rejecting is remembered by a fingerprint of what it would keep and the lesson is never proposed or learned again. Lessons are per person: each sees and decides only their own, the owner included; suggestions are the owner's only. The weekly digest carries a count and a link, never buttons or numbers to answer, so a channel that can be taken over cannot accept a lesson. | TestLessonsAreProposedNeverApplied, TestAcceptEditAndRejectLessons, TestRejectingARepairDiscardsIt, TestLessonsArePerPerson, TestTheWeeklyDigestOnlyLinks |
| A company's showcase showing too much | The showcase is off until the company's creator turns it on, and only they change it, partners and generic updates of the company included. It shows the company's name, industry and mission and only the items they added: briefs that shipped, videos the company uploaded, https links. It names no one, escapes everything it shows, and is served with a strict content security policy. | TestTheShowcaseShowsOnlyWhatItsPersonPutThere |
| A company showing on someone else's dashboard | Company widgets are drawn for the person asking, only when the company is theirs or shared with them; anyone else does not see them listed and cannot draw them by id. | TestACompanyOnTheDashboardIsOnlyItsPeoples |
| An agent publishing in the company's name | Uploads to YouTube are always private and marked as made with AI, and only videos of the company that pass their checks go up; making one public (youtube.publish) and posting on LinkedIn are irreversible, so they reach whoever the rules and decision levels name. Every text a member publishes ends with the company's AI disclosure, which can be reworded but not removed. Networks Pimpo does not post to get a ready post on the person's phone, never an automated account. Social networks take only brand accounts without a warning. | TestYouTubeUploadsPrivateAndPublishesOnlyWhenAsked, TestWhatMembersPublishSaysItWasMadeWithAI, TestLinkedInPostsOnThePage |
| A member's media reaching what it should not | Videos are made on this computer with ffmpeg, from files of the company only: a scene names media by id, checked against the company's own, never a path. Screenshots come from the member's own browser, only of hosts the rules allowed for the call, and only with the browser turned on. Media files live in the company's folder and are served only to the people who may view the company. | TestAMemberMakesAVideoThatIsCheckedFirst, TestAScreenshotIsOfThePageAndItsSize, TestNobodySeesAnotherPersonsThings |
| A finance member moving money | Finance capabilities only read: costs.read, stripe.balance, stripe.charges (with a restricted key that reads Balance and Charges) and github.sponsors. A budget is a limit on what the company spends with Pimpo; a member only proposes one, and a person applies it, never past their own limit. | TestStripeIsReadOnly, TestABudgetIsProposedAndAPersonDecides, TestMembersCannotChangeTheirCompany |
| Autonomy earned past what a person meant | Pimpo only suggests: a person gives a member a kind of delivery to do alone, after the run of approvals the company set (10 by default, at most 100), counted only from answers a person gave, with any no starting the count again. The line it adds is marked as earned and decides like any autonomy line, so the decision levels, the administrator's rules, the protection list and what always asks stay above it; a no to that kind afterwards removes the line. | TestAutonomyIsEarnedAndTakenBack, TestMembersCannotChangeTheirCompany |
| A product owner's briefs misleading the CEO | Every claim in a brief cites a source and quotes it, and Jev, reading the quote as data, flags claims it does not support and those that quote nothing; the flags stay on the brief. Briefs only propose: a person accepts, rejects or marks them shipped. A shipped brief is checked against its own predictions on days 30 and 90, only by its author or the work asked to, and its author's accuracy is shown. Signals and their text reach members as data. | TestAProductOwnersBriefIsCheckedAfterItShips, TestABriefScoresAsTheRoadmapSays, TestTheSameSignalCountsOnce |
A company member's coding CLI on the host (code.workspace) | Only a company member's live work calls it, under the house's and the company's rules like any other action. The CLI (Claude Code, Codex or opencode, chosen per member) works in a git worktree of its own per task, under the company's folder, on a branch of its own; it gets its own file and command tools there and no Pimpo tools, no web search or fetch, none of the repository's own Claude Code settings or hooks, an environment with only what it needs to run and sign in plus the company's coding variables (no vault secret, no git config, so none of the person's git credentials), and a cost cap of at most $3 per call. Pimpo commits with the member's name, hooks and signing off, and pushes with the member's GitHub token, given to git as a header for that command only. Merging stays github.merge, irreversible. With the sandbox on (Claude Code or Codex), commands write only in the worktree; without it, they run as the person does, which the member's window says. Worktrees of finished tasks are removed, and a deleted company's clones with them. | TestAMemberCodesOnItsBranchAndOpensAPullRequest, TestCodingNeedsAMemberAndAWayItCanRun, TestClaudeCodesInItsFolderOnly, TestAMemberWorksOnItsOwnBranch, TestTheTokenTravelsAsAHeaderOnly |
Code the agent runs (code.run) | Off until the owner turns it on; a fresh Docker container per call with no network, a read-only root, all Linux capabilities dropped, no-new-privileges, an unprivileged user, no environment, and limits on time, memory, CPU, processes and output, from official images pinned by digest (RFC 0001). | TestEveryRunIsIsolated, TestImagesArePinnedByDigest, TestCodeRunsOnlyWhenTurnedOn, TestLive (with Docker) |
| A malicious skill (SKILL.md) | A skill is third-party text the agent loads as reference, never as the owner's words; once loaded, the rest of that exploration reaches only the capabilities the owner granted it (none means none), its scripts are never run, the protection list refuses known ones by hash (at preview and again every time one is loaded), and a skill changed on disk stops working. | TestInstallASkill, TestProtectionListRefusesASkill |
| A crafted email subject steering suggestions | Suggestions see metadata only, told it is data; they never act: accepting one shows the request and starts an ordinary exploration, which simulates changes. Each round is capped at $0.02 within the daily limit. | TestSuggestionsFromMetadataOnly |
| Leaking secrets | Secrets sit in an encrypted vault whose key is in the OS keychain. Models see capabilities, never credentials. Backups are sealed as a whole with a passphrase of 12+ characters (scrypt N=2^17); staged imports keep their secrets encrypted with the vault key until they are in the vault. A vault key that cannot be read is an error, never replaced by a new one over existing secrets. A secret kept in 1Password or HashiCorp Vault is stored as a reference and read only when a connector uses it, held in memory for five minutes and never written to disk; the op CLI runs with only PATH, HOME and its token, and a time limit; password manager addresses must be https (plain http only to this computer, for the house) and redirects are not followed. A person's references resolve only with that person's own credentials, never the house's, and the house's 1Password app is for the house alone. Checking a reference answers only whether it was found; errors name the reference, never a value; backups carry references, not values. | TestExportImportRoundTrip, TestStagedImportKeepsSecretsUntilTheyAreInTheVault, TestOSKeyOnlyCreatesForAnEmptyVault, TestFileKeyIsNeverReplaced, TestOnePasswordReferencesResolveWithACleanEnvironment, TestAReferenceThatFailsNamesTheReference, TestAPersonsReferencesUseOnlyTheirOwnCredentials, TestHashiCorpVaultReferences, TestPasswordManagersMustBeReachedOverHTTPS, TestOnePasswordConnectReferences, TestExportKeepsReferences, TestPasswordManagersArePerPersonAndNeverShowSecrets |
| A key typed where it does not belong | When a connector lacks a key or the service refuses it (HTTP 401), Pimpo's code, never a model, opens a request for the person the run acts for, naming only the connector and field; the notice on every channel carries only a link to a form in the app, which only that person can open or answer (the administrator included gets 404 for a member's). The form writes straight to the vault under the name Pimpo chose for that person's connector; the answer, the event and what the run and the model read back say only that it was saved, and the link works once and for 7 days. A house key (an external connector's) is asked only of the administrator; guests get no requests. Keys pasted into the app's chat, a new task or any channel message (Telegram, WhatsApp, Discord, Slack, Signal, email) are taken out by a detector of common formats and of values written after words like "password:" before the text reaches the model, the conversation, the store or the log; the person is warned and a message that was only a key goes no further. It redacts and never blocks. Limit: a key in a format it does not know, typed without a label, goes through. | TestAMissingKeyIsAskedForPrivately, TestAnExplorationAsksAndTheModelNeverSeesTheValue, TestCredentialRequestsBelongToTheirPerson, TestPastedKeysAreRemovedBeforeAnythingSeesThem, TestMissingAndRefusedKeys, TestRedactsKnownFormats, TestLeavesOrdinaryText |
| A routine doing more than it says | The runtime exposes only declared capabilities. The gallery signs content and audits each routine with every capability reachable. A routine that sends to other people, or reads the owner's data and can reach an outside host, is flagged and installs only after the owner confirms. The index is fetched over https only, and its list of authors (their ids, names and keys) must be signed by a gallery root key built into Pimpo, so whoever serves the index cannot add an author or swap one's key; an index without that signature is refused whole. The first key seen for each author is also remembered, and later installs and updates must be signed with it, by the same author, and never go back to an older version. | TestAuditFindsUndeclaredCalls, TestTamperingIsCaught, TestAuthorsMustBeSignedByARootKey, TestGalleryRefusesUnsignedAuthors, TestGallerySignAuthors, TestGalleryInstallAndPublish, TestGalleryUpdateKeepsTheOwnersSettings, TestPlainHTTPIndexIsRefused |
| A changed release or update | Each release's checksums.txt is signed with the release key (checksums.txt.sig); pimpo update checks the signature against the release public keys built into the binary before trusting any checksum, and refuses to install when the signature is missing or does not match. scripts/install.sh does the same when its openssl can verify Ed25519 (OpenSSL 3), and otherwise warns and checks the checksum only. The desktop app's updates are signed with their own key (Tauri). Limit: the first install trusts wherever it was downloaded from. | TestUpdateNeedsSignedChecksums, TestUpdateAndRollBack, TestReleaseSign |
| A connector doing more than it says | It runs as a separate process with a clean environment, exactly its declared tools, timeouts, and risk-based rules. An uploaded connector cannot take the name of one of Pimpo's own families (gmail, notify, web...). | TestConnectorsAreHeldToTheirManifest, TestInstallJSONConnector |
| Known exfiltration endpoints and patterns | A signed protection list is checked before every action, including other agents' actions through the Guard. Pimpo cannot undo other agents' actions, so through the Guard their writes (unless inside folders a rule allows), tools it cannot sort (names are normalized first; look-alike letters are not sorted) and the first visit to a new web host ask. | TestGuardBlocksListedThings, TestGuardForOtherAgents |
| Widgets and dashboards | widget.show only reaches the routine's own person; a snapshot has fixed kinds, capped sizes and https links only, and is always drawn as text. Dashboards and widgets belong to their person: a shared dashboard shows others only widgets marked shared, and nobody else can change it. Refresh now runs the routine under the same rules, and asks before a run that uses a model and costs. A floating widget's window shows only this computer's Pimpo, can only drag itself, and follows only /open links into the main window. A phone's home-screen widgets read the feed with a widget key (wk_) made on that phone, kept only as a hash, replaced each time the phone pins or unpins a widget: it reads only the widgets pinned to that phone, in its person's name, opens nothing else and dies with the device. The Android app stores the key in its private storage and takes the address from the page it shows, never from the page's words. | TestTheWidgetKeyReadsOnlyThePhonesPins, TestWidgetsCarryOnlyData, TestSharedDashboardsShowOnlySharedWidgets, TestRefreshNowAsksBeforeSpending, TestNobodySeesAnotherPersonsThings, floating_widgets_keep_their_place_and_only_take_widget_ids |
| Companies of agents | Off until the owner turns them on in Labs. A company belongs to the person who made it; partners reach it only with the grant they were given (view, approve, configure), only its person deletes it or changes who it is shared with, and anyone else, the administrator included, gets 404. A seat for a person goes only to a partner, the CEO seat always stays its person's at the top, and every change is checked whole (one top, no loops, roles and bosses that exist) before it is written. A company file never names a person. | TestACompanyIsOnlyItsPeoples, TestNobodySeesAnotherPersonsThings, TestTheTreeStaysWhole, TestTheCEOSeatKeepsItsPersonAndPlace |
| A company's rules loosening the house's | A member's call is decided by the house's rules first and then by its company's, and the stricter verdict wins, so a company can restrict but never allow past the administrator's rules, the protection list or the capabilities that always ask. Inside the company the most specific layer decides; a rule allowing what a broader one forbids is refused unless saved as an exception by someone who configures the company, and stays marked with what it overrides. A call for a member who is no longer in its company is blocked. | TestCompanyRulesSitOnTheHouses, TestTheMostSpecificRuleWins, TestABroaderRuleMakesExceptionsVisible |
| A company member acting for real | A member's work is the only live exploration: every call passes the house's rules, then the company's (the stricter wins), and only its role's tools are reachable; its approvals pause its clock instead of timing it out. Each piece has a cost cap (at most $5, inside the day's limit), runs one at a time per member and three per company, and stops when the member, its department or the company is paused. What company.wake or a person hands a member is given as data, never as instructions, and only a member's own routine can wake it. | TestAMemberWorksLiveWithinItsRole, TestPausingAMemberStopsItsWork, TestARoutineWakesItsMember, TestOnePieceOfWorkAtATimePerMember |
| Agents delegating without end, or a task drifting on the way down | Work goes only down the tree (or across a department when the company allows it), at most 4 levels, at most 10 open tasks per member, never the same open task twice and never back up to someone who handed it down. A handoff without an objective or a way to know it is done is refused. Each handed-down task is judged against its root; one that may have strayed waits for the assignee's boss. Content read outside travels marked as data in the task. A question waits for its answer without spending, and only the member it was asked of, or a person allowed to approve, answers it, once. | TestHandoffsAreRefusedWhenTheyWouldRunAway, TestGuardKeepsDelegationFromRunningAway, TestATaskThatStraysWaitsForTheBoss, TestAQuestionToTheCEOWaitsForThem |
| A member poisoning the company's memory | What a member keeps is marked as that member's words and given to the others as context, never as rules or instructions; only people delete notes. A note for the whole company waits, by default, for a person to approve it before any member reads it (each company may give that to another decider, let members write freely, or close a memory), a member's own notes reach only that member, and a task's reach only the work on it and on the tasks below it. Meetings only talk: no tools, a cost cap of at most $2, at most 8 agents and 4 rounds. | TestMembersRememberAndTheCompanyRemembersForThem, TestEachMemoryIsWrittenAsTheCompanySays, TestEachMemberReadsItsOwnMemories, TestAMeetingStopsAtItsCap |
| Autonomy stretched past what a person allowed | A decider other than a person answers only what the company's rules or the house's starting presets ask; a rule the administrator wrote, the protection list and the capabilities that always ask still reach a person. Simulated or refused calls are not decided. Deciders read the action's arguments as data, a model's vote costs at most $0.05, and every answer is kept. Members have no capability that changes their company, so none gives itself autonomy, money, tools or accounts. | TestWhoDecidesWhatAMemberAsks, TestSomeThingsAlwaysGoToAPerson, TestMembersCannotChangeTheirCompany |
| A decision kept from the CEO | The host classifies by triggers first, without a model, and a decision is at the highest level any trigger reaches; a classifier only adds to that, and when it is unsure or fails the decision goes to the CEO. A level only ever makes the decider more careful, and an action the rules allow still goes to whoever its level names. Opinions gathered on the way up decide nothing. | TestNoStrategicDecisionMissesTheCEO, TestAnActionAtTheCEOsLevelWaitsForThemWhateverTheAutonomy, TestTriggersPutADecisionAtItsLevel |
| A company spending past what its person allowed | Every company cost is booked for a member and checked against the member's salary, its department's and the company's limits before work starts, and each piece of work is capped at what is left; all of it stays inside the person's own limit and the house's, checked at every call, and a company is never given more than its person's daily limit. Subscription work is shown at its API-price equivalent and can be counted in the limits. | TestABudgetReachedHoldsTheWork, TestACompanyCannotOutspendItsPerson, TestSubscriptionWorkCountsWhenTheCompanySays |
| A member reaching its person's accounts or life | A member's call reads settings and secrets under the member's own names, so a connector finds only that member's accounts, or a company account it was granted (read only for a read grant). The person's Google calendar and mail are never used for a member, capabilities that reach the person's phone, Mac apps, house, Google sheets, music, notes and reminders are blocked for members, live member work gets no tools on the person's memory, and each member browses with its own profile. | TestAMemberUsesOnlyItsOwnAccounts, TestASharedAccountGoesAsFarAsItsGrant, TestMembersStayOutOfTheirPersonsLife |
| Someone at an unlocked computer | The desktop app opens as the administrator without asking. When the owner turns on the lock, the window, the floating Pimpo and floating widgets stay hidden until Touch ID, the account password or Windows Hello confirms, at start and after five minutes closed; turning it on requires passing the check once, so nobody locks themselves out. It guards the app's window, not the data folder or a browser session. | device_auth::tests, manual on each system |
| Another person in the house | Every device and session belongs to one person and every request acts only for them; every route has an access rule and routes without one are the owner's. Routines, conversations, memory, activity, approvals, recordings, jobs and phone reports belong to their person, and nobody, the owner included, reads another's through Pimpo. Others' events reach the live stream only as a type, and Needs you merges only the caller's own approvals, questions, routines and jobs. Guests' changes wait for their responsible person; a member answers only for themselves and the owner cannot answer a member's own approvals. A guest keeps facts only for themselves; a member's house facts are shown to agents as that member's words, never the owner's, and are limited in number. Memory tidying, restoring a memory version, usage reports, suggestion rounds and queued emails each act only on one person's own things, and a queued email leaves from the account of whoever queued it. Once a member has signed in, only they change their mail and calendar. Removing someone deletes their accounts, memory, chats, explorations, routines, jobs, reminders and queued emails, and their id is never given to anyone again; role changes are recorded where the person sees them. Passkeys (WebAuthn) sign each person in as themselves; Pimpo keeps only public keys, checks the signature and the address the passkey belongs to, and a passkey of someone removed opens nothing; a passkey whose signature count goes backwards (a copy) is refused and the person is told, and removing a passkey signs out its sessions. A link the owner makes for someone else is an invite: it opens nothing by itself, works once within 15 minutes and opens a session with a token of its own, which the person sees in their account and activity and can sign out; each person lists and signs out their own devices and sessions, and nobody else's. Devices unused for 180 days and passkey sessions unused for 30 are signed out, phone reports included, and repeated wrong sign-ins and passkey sign-ins started from one address are slowed and refused (requests without a credential do not count). Limit: the owner can still open an invite before the person does; the person then sees a device they did not add. Limit: whoever administers the computer Pimpo runs on can read its files; the privacy is enforced by Pimpo, not by encryption between people. | TestGateFamilyIsolation, TestEveryRouteHasAnAccessRule, TestNobodySeesAnotherPersonsThings, TestMembersCannotAdministerTheHouse, TestPasskeySignsInAsItsPerson, TestIdleDevicesAreSignedOut, TestWrongSignInsAreLimited, TestAnInviteWorksOnceForItsPerson, TestAnUnusedInviteExpires, TestPeopleSeeAndRevokeTheirOwnDevices, TestRemovingAPasskeySignsOutItsSessions, TestACopiedPasskeyDoesNotSignIn, TestPasskeySignInsAreLimited, TestIdlePhonesCannotReport, TestSharedFactsAreTheirAuthors, TestOrganizeIsPerPerson, TestRestoreForLeavesOthersFactsAlone, TestReportIsOnlyTheirsOwn, TestSuggestionsSeeOnlyTheOwnersRoutines, TestSendsAsWhoeverQueued, TestOnlyTheResponsibleAnswers, TestOnlyGuestsHaveAResponsible, TestIDsAreNeverReused, TestRemovingSomeoneDeletesTheirThings, TestTheOwnerCannotReplaceASignedInMembersAccounts, TestRoleChangesAreShownToThePerson, TestNeedsArePerPerson |
| Messages to third parties and physical actions | whatsapp.send_to and ha.critical always ask, whatever the rules say or anyone approved before. | TestWhatsAppToOthersAlwaysAsks, TestAlwaysAskIgnoresGrants |
| An "approve for this routine" answer stretched to other actions | The answer is bound to the operation: the person who answered (only the person the routine works for; never for a guest's request or an exploration), that routine at the code version of the run that asked, the capability and scope, and every argument except free text (bodies, subjects, messages); recipients compare as a set, a URL by its host, and an amount may only be at most the approved one (or the limit the person set, never below it). Anything different asks as usual; a new version of the routine, or the routine now working for someone else, voids it. It only turns an "ask" into "allow": rules that block still block, and whatsapp.send_to and ha.critical ignore it. Each person lists and revokes only their own (/api/grants), and every call it allows is recorded with grant:<id> as its rule. It is offered by number on Discord, Slack and Signal, unlike "always": a mistaken number can only repeat that exact operation, which the person already approved once, and it is revocable in one place. | TestGrantMatchesOnlyTheSameOperation, TestGrantHostsAndAmounts, TestGrantsBelongToWhoAnswersAndSkipAlwaysAsk, TestApproveForThisRoutine, TestRevokingAGrantAsksAgain, TestGrantsArePerPerson |
| Network exposure | The server listens on loopback. Home mode listens only on a private address (never a public one) and is off by default. Remote access uses the Tailscale built into Pimpo: Funnel ends TLS on this machine, so Tailscale never sees the traffic in the clear. Every way in needs a per-device token that can be revoked; the owner's login link is printed only when made and pimpo token rotate replaces it. A browser's cookie changes nothing unless the request comes from Pimpo's own page (Origin or Sec-Fetch-Site must match), so another page, such as one on another port of this computer, cannot act through it; JSON bodies must be declared as JSON. Every answer forbids framing and allows only Pimpo's own scripts (Content-Security-Policy), except that Telegram Web may frame the Mini App page, which may load Telegram's script. | TestDevicePairingAndRevocation, TestLANUsesOnlyHomeAddresses, TestPhoneLinksWithTailscaleAndHome, TestCrossOriginWritesAreRefused, TestPhoneReportsNeedPimposOwnPage, TestSecurityHeaders, TestSetToken |
| Backups in someone else's storage | Every export, cloud or not, is encrypted and authenticated as a whole on this machine (scrypt + AES-GCM, with a SHA-256 manifest of every file), so S3 or Google cannot read or change the database, memory, connectors or secrets; import and cloud restore refuse a file that is not sealed. S3 endpoints must use https (except on loopback) and bucket names are checked. Google Drive access is limited to files Pimpo created (drive.file). Storage keys and the passphrase stay in the vault and are never sent back to the interface. Retention deletes only files named like Pimpo backups. | TestTamperedBackupsAreRefused, TestCloudBackupsToS3, TestS3SignsEveryRequest, TestS3RefusesPlainHTTPAndOddBuckets |
| A third-party MCP server | Only the owner can add one. Pimpo lists its tools and the owner sets each tool's risk; every tool starts as irreversible and asks first, and the server's own hints are shown only as its claim. Packages are pinned to a version, local servers get a clean environment with only the variables given, remote servers need https (plain http only on this computer, judged by the exact host) and are never followed to another host, and keys live in the vault. Tools added later stay hidden; a tool that vanishes, or whose description or parameters change, blocks the connector until it is reviewed again. | TestAddMCPServerFromTheRegistry, TestImportedRemoteConnector, TestProbeReportsTheServersClaims, TestChangedToolIsStopped, TestRemoteURLNeedsHTTPSOrThisComputer, TestRemoteServerRedirectKeepsTheKeyHome |
| A chat talked into acting | Chat messages run as explorations: reads are real, changes are simulated and shown. Confirming performs only the recorded actions of that one answer, once, through the policy and approvals like a routine; the agent cannot add actions at confirm time. A conversation is visible only to the person who started it, and searching conversations looks only in the caller's own. | TestChatRehearsesThenDoesExactlyWhatItShowed, TestSearchingConversations |
| An assistant stepping outside its role | An assistant's tools are a list the host enforces: it is shown only those tools and any other call is blocked and recorded. Its instructions go to the agent as a job description, never as a way around rules. Only the owner creates assistants. | TestAssistantOnlyUsesItsCapabilities |
| Someone opening the Telegram Mini App as another person | The Mini App signs in only with Telegram's signed initData: HMAC-SHA256 under a key derived from the bot token, over every field but the hash, sorted, compared in constant time; data signed more than an hour ago, a field given twice, or data for another bot opens nothing, and nothing in it is read before the signature checks out. The Telegram account id maps only to the person who paired it in a private chat; an unpaired account gets nothing. The session belongs to that person, lasts an hour, is kept in the page's memory and sent as a bearer token (not a cookie), and opens only the Mini App's routes on top of the person's usual access rules, the owner's included. Only /tg/app may be framed, and only by https://web.telegram.org; only it may load Telegram's script. Wrong initData counts as a wrong sign-in. Limit: Telegram's script runs in that page and could read the session while it lasts, so its routes are few; a copy of someone's initData taken within the hour opens their session. | TestVerifyInitData, TestMiniAppSignsInAsThePairedPerson, TestMiniAppMenuButtonNeedsHTTPS, TestNoticesOfferTheMiniApp, TestSecurityHeaders, TestNarrowedTokens |
| Someone else messaging the Discord, Slack, Signal, Telegram or WhatsApp bot | A channel answers only the account that paired with a pairing code: the owner's (10 characters, valid 15 minutes, used once on one channel) or an invite (10 characters, valid 24 hours, used once). Wrong codes get no answer; a sender with 3 wrong codes is ignored for an hour, and 10 wrong codes from anyone change the owner's code. Telegram pairs and listens only in private chats. Numbered answers resolve only the choices of the latest notice to the owner, once, within 10 minutes, unless they reply to an older notice (Discord, Slack, Signal), which answers exactly that one, once, and a reply naming a message Pimpo does not have answers nothing; a number sent just as a newer notice arrived shows its choices again, and "always" (a lasting rule) is never offered by number; "for this routine", bound to the exact operation, is. An answer to a routine's question typed in words is taken only when it matches one of its options unambiguously and the question is the sender's own; a reply that matches none shows the options again, and words sent without replying answer only the sender's latest question, asked in the last 10 minutes with nothing said on that channel since, and only when they are exactly an option. Notices for other people never reach these channels. | TestSignalLikeChannelPairsAndAnswersByNumber, TestLinkPairingGivesGuessersNothing, TestWhatsAppPairingGivesGuessersNothing, TestPairingCodeResistsGuessing, TestGroupsAndChannelsAreNeverAWayIn, TestInvitesExpireAndRenew, TestNumberedAnswersAreBoundToTheLatestNotice, TestLinkNumbersNeverMakeRules, TestAReplyAnswersItsNotice, TestLinkAnswersInWords, TestTypedAnswerInTheApp, TestWhatsAppQuestionAsAList |
| An email forging the owner's address | The email channel takes a message only when the receiving provider's own Authentication-Results (the top one) show DKIM or DMARC passing for the owner's domain; without that header nothing is taken. Earlier answers reach the next turn as JSON strings labelled as Pimpo's words, so quoted mail cannot pose as the owner. | TestEmailChannelOnlyHearsTheOwner, TestHistoryKeepsEarlierTurnsAsData |
| An API model running up a bill | An API model runs only with a price set by the owner; its tokens are counted into the daily budget on every turn and the loop stops at the call's cost limit. Keys stay in the vault. Its tool calls go through Pimpo's MCP server, so rules and approvals apply exactly as with Claude Code. | TestExploreWithAnAPIModel, TestAnthropicAgentLoop |
| A provider's model list | Lists are read with the key from the vault, which is never logged or cached (answers are cached under a hash of the headers). A newly listed model gets no price from the list itself, except OpenRouter's own prices, and still needs one from the owner before it runs. Only a list the provider returned retires a model. Server-side compaction's summary tokens are counted in the budget. Members see only the names of retired models, never prices or keys. | TestLiveCatalogMarksNewAndRetired, TestMergeMarksNewAndRetired, TestAnthropicCompaction |
| An email that tries to steer a routine's written text | A routine's write step only returns text: the item is passed as data with a system prompt that forbids following it, the output is capped in length and recorded, and anything the routine does with it goes through the policy and approvals like any other call. Each text is checked against the budget first and a run may ask for at most 20. | TestWriteStep, TestWriteIsBudgetedAndRecorded, TestWriteStepPrompt |
| Text in a desktop notification | Titles and bodies (from email subjects or model output) reach the system notifier as arguments or environment variables, never inside a PowerShell or AppleScript script. The desktop app obeys /desktop/* only for addresses with exactly the local Pimpo's scheme, host and port. | TestNotifyKeepsTextOutOfScripts, only_this_computers_pimpo_is_local |
| Forged webhooks | WhatsApp checks Meta's signature and handles each message id once (a replayed body repeats nothing), dropping messages older than a day. The generic channel signs what it sends; it is the owner's, so it receives only the owner's notices and acts for nobody else. | TestWhatsAppChannel, TestWhatsAppMessagesAreHandledOnce, TestGenericChannel |
| Damaged database | Pimpo checks the database (SQLite quick_check and the newest 500 events of the hash chain) at start, read-only, and before every snapshot. A damaged one is never written over or deleted: it is moved to quarantine/ and Pimpo serves only a recovery page to the owner (the owner's link or signed-in devices read back from the damaged file, a pinned PIMPO_TOKEN, or a fresh recovery link printed at start); members and guests get nothing. A damaged snapshot is never taken, offered or restored. Going back to a copy keeps who may sign in from the damaged file when it can be read; when it cannot, the copy's own devices and login link come back. | TestDamagedDatabaseStartsRecovery, TestQuarantineAndRecoverKeepTheDamagedFile, TestSnapshotRefusesADamagedDatabase, TestDamagedSnapshotsAreMarkedAndSkipped, TestCheckFindsADamagedFile |
Forged pushes to public endpoints (/push/gmail, /github-hook/…) | A Gmail push is taken only with a Google-signed ID token (RS256, checked against Google's published keys) that has not expired, was issued by Google, names the configured audience and the configured service account, with a verified email; the push names only a mailbox and a history id, and Pimpo then asks Gmail itself, with that person's own token, what reached the inbox, so a forged or replayed push reads nothing and runs nothing by itself; a history id that is not newer changes nothing. Each person's watch is for their own mailbox and wakes only their routines. A GitHub delivery is read only after its HMAC-SHA256 over the raw body matches the routine's own secret (constant time); a wrong signature and a routine without a GitHub webhook look alike; each delivery id starts the routine once, at most 30 a minute and 1 MB each; the secret is shown once and kept in the vault. Slack events come only over the owner's own Socket Mode connection and are kept only while one of the owner's routines watches them. Everything these events carry reaches routines as data. | TestPubSubTokenVerification, TestGmailPushFiresTheMailboxOwnersWatchersOnce, TestGmailWatchRenewal, TestGitHubSignature, TestGitHubWebhook, TestSlackEventsReachAWatchingRoutine |
| Tampered history | Events are chained by hash; an import with a broken chain is refused. A snapshot restore keeps the current devices, passkeys, login link and people, so it does not bring back access that was revoked, and snapshots do not follow symlinks. | event store tests, TestBrokenHistoryIsRefused, TestRestoreKeepsWhoMaySignIn, TestSnapshotsSkipSymlinks |
| Settings changed without the owner noticing, or secrets kept in the history | Every change to rules, the safety level, the budget, connections, models, people and settings is recorded in the hash-chained event log with who made it and when. A secret's value is never recorded, not even encrypted or hashed: vault entries, fields named like a key, token, secret or password, and values holding one (such as a link with a password) only say added, replaced or removed, and undo never brings a secret back. Undo puts back only what is still as the change left it. The house's history is the owner's; a member's own mail and calendar changes go only to that member's history, and the owner sees neither them nor their undo. Limit: the event log cannot forget, so a removed member's history (their email address and mail server, never passwords) stays in it and in snapshots and exports, unreachable through Pimpo. | TestHistoryRecordsEveryArea, TestHistoryNeverKeepsSecrets, TestHistoryUndo, TestHistoryIsEachPersonsOwn |
| Someone else's progress | Progress records carry their person: /api/progress returns only the asker's, and on the live stream another person's update arrives as its type alone. Labels are Pimpo's own step names (a capability, or a part's title from the plan the person approved), capped at 60 characters, never what a step read or wrote; a followed job's message goes only to that person's own channels. | TestProgressIsEachPersonsOwn |
| A lost phone | Revoke the device in Settings; the owner's session is unaffected. | TestDevicePairingAndRevocation |
Not promised#
- A compromised computer. Anyone who controls the machine Pimpo runs on controls Pimpo.
- A model's judgment. Judgments are probabilities; anything irreversible waits for a person.
- A person who approves something harmful. The receipts show it, and undo works where the service allows.
- Services' own security (Google, Meta, Telegram).
This page is built from the repository's documentation. Improve it on GitHub.