Hey Pepper

Your AI chief of staffDocs

Hey Pepper

How Pepper keeps your data safe

Pepper reads the chats you connect, so it holds text that is deeply private to you. Read this page before you run Pepper for real.

If you find a hole, email hello@heypepper.ai with "security" in the subject rather than posting it anywhere public.

  • Pepper reads the chats you connect through Beeper, the app that brings your chat accounts together. It never writes to them: the Beeper token is made with "Allow sensitive actions" off, so Beeper itself refuses every attempt to send, edit, delete, archive or mark a message as read.
  • On Telegram, Pepper talks only to you. Its bot accepts messages from one numeric user id in one private chat; the code has no way to accept a recipient.
  • Pepper can also answer calls on a phone line, and it speaks to whoever calls. When contacts you list or strangers call, Pepper takes a message without reading your data; the one thing it uses is the names of people it knows, to transcribe the message more accurately.
  • Routines are your own programs. Pepper starts them on a schedule and reads their status. Your code controls anything they send, and each routine declares what it sends in its ROUTINE.md.
  • Pepper drafts, you send. When it suggests a reply, you copy it into your own app.

Three things together make an assistant dangerous: it holds private data, it reads text written by people who are not you, and it has a way to get data out. Pepper has the first two by design. Its safety rests on having no way to get data out except to you and the services listed under Where your data goes, and on treating all text from outside as data rather than instructions.

The people who can attack Pepper are:

  • Anyone who can message you. Their words land in the archive of your messages, in the brief (the summary Pepper writes three times a day) and in what Ask, where you put questions to Pepper, pulls up for the model. They will try to make Pepper follow instructions, invent links, repeat one-time codes, or put things on your task list.
  • Anyone who can call your phone line. Caller ID can be faked.
  • Any other process or user on the Mac that runs Pepper. It can read the data folder if the permissions let it, and it can connect to Pepper's local ports.

Prompt injection, where someone's message tries to give instructions to the model behind Pepper, is not a solved problem, for Pepper or for anyone. So the real boundary is what Pepper is able to do at all, something the model cannot talk its way around, and filters are a second layer on top.

Boundaries (the model cannot cross these):

  • The Beeper token can only read, and Beeper itself enforces that.
  • The Telegram adapter has a fixed recipient: you.
  • The model's tools can only read the SQLite database, and each argument has a fixed type and a limit. No shell, no files, no HTTP, and no network by default (one exception: exact meeting quotes, in the table below). At most 12 tool calls per question by default (a setting can raise it to 20), 20 rows per search, 40 per thread, 6,000 characters per result.
  • Helper processes (speech, embeddings, routines) get only an allowed list of environment variables, with none of Pepper's keys. Routines can still read .env, because they run as the same macOS user.
  • /halt in Telegram pauses Pepper until /resume: briefs, nudges, backlog offers, extraction, and new questions on Telegram, the dashboard and the phone. It does not stop an answer already underway, or a command you run on the Mac yourself. New messages still arrive in the archive on the Mac, but no model reads them. Routines keep running; pause each one separately. If you are on a call with Pepper, it ends at the next exchange.

The second layer, filters (helps, but is not the boundary):

  • Pepper labels every tool result and transcript as information from an outside source, names the source, removes invisible characters, and prevents formatting in the text from hiding that label.
  • One-time codes, verification links and URLs containing secrets are masked in text before the model sees it, and again in answers before they are sent. Photos are not masked: the model is told not to repeat what it can read in them. A voice note's transcript and a caller's message are sent to you as heard.
  • Links in an answer are removed unless they appeared verbatim in something the model was shown. Telegram link previews are off. Answers are plain text, never Markdown.
  • Messages you forward to the bot are content, never commands and never tasks. The same goes for forwarded voice notes and photos.
  • Facts and decisions require your words or your confirmation. A clear direct request to you in a one-to-one chat can become a task automatically; less certain items wait in Review, the dashboard's list of things to confirm. Claims from other people stay marked as proposed, with their source, until you say otherwise.
  • Tests with planted injections assume the model is fooled and check that the defenses around it still hold: no code or unknown link gets through to you, a tool refuses anything outside the list, and the planted message is still shown as content.

Most of these live in .env, at mode 600, which means only your user can read the file.

WhatWhere it livesWhat it can doHow to replace it
Beeper access token.env, mode 600Read chats and messages. Nothing else; created read-only.Beeper Desktop, Settings, Integrations
Anthropic API key.envSpend your credit.platform.claude.com
OpenAI API key (optional).envSpend your OpenAI credit; transcribe audio if you chose OpenAI for speech.platform.openai.com
Telegram bot token.envAct as the bot. The bot only ever talks to your id.@BotFather, /revoke
Your Telegram id.envControls who may use the bot.Not a secret, but keep it exact.
Tactiq sign-in (optional)data/connections/tactiq.json, mode 600Read your meeting transcripts.npm run pepper -- connect tactiq again, or --remove
Phone passphrasedata/phone/passphrase, scrypt hash, mode 600Opens the archive by voice from your number.npm run phone -- passphrase
Phone socket tokendata/phone/token, mode 600Lets FreeSWITCH, the call server on the Mac, hand calls to Pepper.Delete the file and re-run the phone setup
Dashboard sessionIn memory onlyOne browser session, tied to your Tailscale login.Restart Pepper

When Pepper starts, it makes the data folder and database readable only by your user, and npm run doctor fails if .env, the data folder or the database is still readable by anyone but you.

DataStoredLeaves the MacDeleted by
Message text and sendersdata/pepper.sqlite, mode 600, in a 700 folderRelevant messages go to Anthropic to write the brief, extract follow-ups and answer Ask questions, and Anthropic's retention terms apply. Briefs and answers also pass through Telegram./forget <person> removes their messages, tracked follow-ups, facts, decisions, briefs that mention them, and their chat; they are not re-imported. Group meetings they were in stay, with their name removed. Nothing is deleted from your apps, from Anthropic, or from backups already taken.
Photos you send to the botAs image content in the turns tableTo Anthropic, unmasked; the model is told not to repeat codes or credentials it can read in them./new closes the thread; /forget scrubs threads that mention the person
Voice notes and call audioA private temp folder during transcription, then deletedNowhere with the default local speech-to-text. To OpenAI, or to Codex (experimental), only if you put them in PEPPER_STT. Phone audio uses local or OpenAI only, and the passphrase is always transcribed locally.Automatically
Spoken repliesOnly Pepper's fixed prompts are cached as audio. Answers are synthesised and deleted.Made on the Mac, then sent to you on Telegram or played into the call.Automatically
Your questions and Pepper's answersthreads and turns tablesTo Anthropic as the conversation, and through Telegram when you use it. If you turn on exact meeting quotes, a meeting question also goes to Tactiq, the meeting transcript service you connected./new closes a thread; /forget scrubs threads that mention the person
Routine outputstatus.json and up to 30 logs per routine, mode 600Pepper sends nothing but a one-line status (name, state, what it needs) in the brief, which reaches Telegram and the model that writes the brief's headline. Models never see routine logs.Logs pruned to 30; status.json stays
Spend and healthllm_calls, healthNowhere, except the totals that appear in the brief.Kept

Pepper makes no automatic backup copies. npm run backup creates a local copy, normally in ~/pepper-backups, unencrypted and including .env. Pepper deletes the backups it made once they are 30 days old. If you use Time Machine and haven't excluded the data folder, it is backed up there too, and Pepper never deletes those copies.

Telegram. Edited messages, channel posts, inline queries and group events are never even requested from Telegram. Buttons carry single-use random tokens that expire.

The Today page, Pepper's dashboard. It is published only with tailscale serve, which puts it on your private Tailscale network and nowhere else. A request must carry the Tailscale login header that Serve sets, and that login must be in PEPPER_DASH_USERS. The Host must match PEPPER_DASH_ORIGIN or the Mac's own loopback address. Every change needs a session cookie and a per-session token, and must come from the same origin. The page escapes every string, so each one shows as text rather than running as code, and runs with a strict content security policy. Never use tailscale funnel: it would put the page on the public internet.

The phone. Three tries per call, and after two failed calls in an hour your access by phone is locked for an hour (npm run phone -- unlock). When a caller passes these checks, you get a Telegram message right away, so you can stop a call that isn't from you with /halt. Speech from contacts and strangers never reaches Ask; it does reach the transcription model, which is OpenAI if you chose that.

The local ports. 7777 (dashboard) and 8084 (phone) listen on 127.0.0.1 only, and the dashboard refuses any connection that doesn't come from the Mac itself. The phone socket needs a token that only the FreeSWITCH dialplan and Pepper's own data folder hold.

Pepper runs as an ordinary user. Any admin on that Mac can read what that user can read. So:

  • Give Pepper a separate macOS user and limit the Mac's accounts to that user and yours. If you add a second user for iMessage or anything else, know that it can reach Pepper's local ports and, through adb (the USB tool that controls the phone), the bridge phone that carries the phone line.
  • Turn on FileVault. The database is plaintext on disk.
  • SSH with keys only, and only over Tailscale. No public ports.
  • Keep the phone line on Wi-Fi you trust. The bridge carries call audio over your home network, and the spoken passphrase travels with it.
  • Never create the Beeper token with "Allow sensitive actions" on.
  • Never set TELEGRAM_API_ROOT or point BEEPER_BASE_URL anywhere but localhost. Pepper refuses both.
  • Never expose port 7777 with anything but tailscale serve.
  • Never commit .env, data/ or routines/. They are in .gitignore.
  • Never say your phone passphrase where it can be recorded, and never reuse it anywhere.
  • Never run Pepper on a Mac where people you do not trust have accounts.

These are the gaps we know about and haven't closed, so you can decide whether to run Pepper with them.

  • The phone passphrase is a replayable secret. Someone who records you saying it, and fakes your caller ID, gets a voice session until you /halt. A second factor that cannot be replayed is planned.
  • Local processes are trusted. Any program on the Mac, under any account, that knows an allowed login can forge the dashboard's login header on the loopback port, and a program running as Pepper's user can read the database. A Unix socket behind Tailscale Serve is planned.
  • The filters are rules of thumb. The filters that mask codes and remove links catch the common patterns and are tested against a collection of examples, but a determined attacker can find a pattern they miss.
  • Routines are trusted code. Only run routines you wrote or read.
  • npm run check:no-send scans src/ for any write to Beeper, any send to Telegram outside the one adapter, any network or process call outside the allow-list, any process started without an explicit list of environment variables, and any place where browser code inserts HTML into a page. It runs on every push.
  • The injection examples in test/fixtures/injection.json run on every push through tests of the wrappers, filters, extraction and rendering that assume the model was fooled, and npm run eval:injection runs it against the real model.
  • gitleaks, which scans the code for anything that looks like a secret, runs on every push, and a test checks that every secret in .env.example is blank and no value in it looks like a credential.
  • npm run doctor checks the token's scope and that every part loaded, and runs whatever checks each part declares. It does not prove that a part works.

A part is code you add to Pepper (see Make it yours). It runs inside Pepper and has the same access to your messages. Pepper's checks stop the ordinary mistakes: a part can't import another part or code that sends, and its migrations can only create tables with its own prefix. Running code still has the whole database. They cannot stop deliberately harmful code, so only add code you asked for or trust. Routines are different: they run outside Pepper, as the same macOS user, so they can read the database and .env too.