[poppy: remember what matters]
Poppy is a developer memory CLI and MCP server. Your AI coding agent stores
decisions, preferences, and lessons in a local SQLite store, then recalls
them when they're relevant. No cloud, no API keys, no per-query cost.
Then wire up your agent, and restart it:
The package on PyPI is poppy-memory; the command it installs is poppy.
No pipx? See Install for the other ways in, and for the one PATH
check that saves most first-day trouble.
First ten minutes
The order a new user walks, each step linking to its section:
- Install Poppy with pipx, uv, or the one-line installer. See
Install.
- Check that
poppy is on your PATH: command -v poppy should print a
path. See Check your PATH.
- Wire up your agent with
poppy setup <client>. See
Supported clients.
- Restart the agent so it loads the MCP server and hooks. See
Restart the agent after setup.
- Store and recall once with
poppy remember and poppy recall. See
What it does.
- Pair with Trags (optional) with
poppy setup trags, and approve it in
the browser within five minutes. See
Sync to Trags.
- Install the daemon (optional) with
poppy daemon install. See
MCP daemon.
- Run
poppy doctor to check engine, storage, MCP config, and hooks. See
Commands.
Install
Use the first option your machine already has:
- pipx:
pipx install poppy-memory. Needs pipx and Python 3.11 or newer.
- uv:
uv tool install poppy-memory. Needs only uv, which fetches its own
Python. A fresh Mac has neither pipx nor uv.
- One-line installer:
curl -fsSL https://trags.ai/install/poppy | sh.
Needs Python 3.11 or newer. It uses pipx or uv when it finds one and
otherwise installs into its own virtualenv.
Check your PATH
pipx and uv put the poppy command in ~/.local/bin. A fresh Mac often does
not have that folder on its PATH, and the install still succeeds: uv only
prints a warning. Check:
If it prints nothing, add the folder to your shell profile and open a new
terminal (pipx ensurepath or uv tool update-shell do the same):
This matters for your agent too. The hooks call poppy by name, so an agent
that cannot find it fails every hook with poppy: command not found. An agent
keeps the PATH it started with, so restart it after you fix PATH. poppy doctor
checks only the PATH of the terminal you run it in, so it can report the hooks
as fine while your agent still cannot find poppy.
Restart the agent after setup
poppy setup <client> ends with Restart <client> to activate. Do that.
Agents read their MCP servers and hooks only when they start, so a session
that was already open does not see Poppy until you quit and reopen it.
What it does
remember stores a fact, decision, preference, or lesson.
recall pulls the few memories most relevant to a query.
- MCP server: agents call the tools in MCP tools
directly. Works with Claude Code, Claude Desktop, Cursor, VS Code, Windsurf,
Codex, Gemini CLI, Copilot CLI, Pi, and Goose. Hermes Agent gets a memory
provider plugin instead.
- Hooks: Claude Code, Cursor, and Codex hooks surface relevant memories and
capture new ones. Claude Code and Cursor use end-of-session and compaction
backstops; Codex uses cadence-only capture.
- Zero-touch capture: with your one-time consent, Poppy extracts
durable memories from your sessions automatically, locally, using the
coding-agent CLI you already have. See
Zero-touch capture.
- Interchangeable engines: retrieval runs on a swappable local engine;
the default,
bloom, is a hybrid two-stage engine, with a zero-dependency
full-text fallback (seed). See Engines.
Supported clients
poppy setup <client> wires Poppy into a coding agent. Eleven clients have an
integration today:
- MCP server plus zero-touch capture hooks: Claude Code, Cursor, Codex.
- MCP server: Claude Desktop, VS Code, Windsurf, Gemini CLI, GitHub Copilot
CLI, Pi, Goose. Most also get a short primer file so the agent knows the
memory tools are there.
- Memory provider plugin that calls the Poppy CLI: Hermes Agent.
What supported means here: each integration is verified against its client at
release time, and maintained best-effort between releases. These clients change
on their own schedule, so when one of them breaks Poppy, that is a bug worth
reporting as a
GitHub issue rather
than something that holds the next release back.
poppy doctor checks the integrations installed on your machine.
Setup treats any hook command of the form <path>/poppy hook <event> (including
a versioned executable name such as poppy-0.3.3) as Poppy's own. It rewrites
these commands to the current executable, so a deliberate pin to a development
install is overwritten on the next setup and removed by --remove. Before
downgrading, run poppy setup <client> --remove with the current version, then
set up again with the older version. Older versions do not recognise
absolute-path hook entries and would add duplicates.
Claude Code plugin
Claude Code can also load Poppy as a plugin, in place of
poppy setup claude-code. Install poppy-memory first, as above. The plugin
only connects Claude Code to the poppy command you already have.
Restart Claude Code afterwards.
The plugin gives Claude Code the same MCP server and the same five hooks that
setup writes. It looks for poppy on the PATH and then in the folders pipx,
uv and pip install --user install into, so it works when ~/.local/bin is
not on Claude Code's PATH. In place of the CLAUDE.md block it ships a memory
skill with the same text. Claude loads the skill when memory is relevant, and
you can run it yourself with /poppy:memory. The plugin needs macOS or Linux.
On Windows, use poppy setup claude-code.
Use the plugin or setup, not both. With the plugin installed, poppy setup claude-code does not add the MCP server or hooks and says so. Pass
--claude-md to add the CLAUDE.md block next to the skill, so the primer loads
in every session. If you ran setup before installing the plugin, run
poppy setup claude-code --remove to remove setup's entries, or every hook runs
twice. poppy doctor warns when it finds both, and fails when the plugin
cannot find poppy.
The plugin does not ask for capture consent the way setup does. Answer the
notice at session start, or run poppy autocapture on --global.
Uninstall
Remove client integrations before uninstalling the CLI, so leftover hooks do
not fail with poppy: command not found on every session.
-
Run the following for each client you set up:
This removes Poppy's MCP entry, hooks and managed primer blocks while
preserving your other settings. If you installed the Claude Code plugin,
also run claude plugin uninstall poppy@trags-garden.
Other clients still need their Poppy entry and primer removed by hand:
Claude Desktop's claude_desktop_config.json, VS Code's user-profile
mcp.json, Windsurf, Gemini CLI, GitHub Copilot CLI
($COPILOT_HOME/mcp-config.json and copilot-instructions.md, default
~/.copilot), Pi ($PI_CODING_AGENT_DIR/mcp.json and AGENTS.md, default
~/.pi/agent), Goose, and Hermes Agent (plugin and SOUL.md guidance).
-
If you installed the daemon service, run poppy daemon uninstall.
-
Run pipx uninstall poppy-memory, or uv tool uninstall poppy-memory if
you installed it with uv.
-
Optionally run rm -rf ~/.poppy. This permanently deletes your memories
and local Poppy settings. If you use a custom POPPY_DIR, your data lives
there instead.
--remove leaves your memory store, daemon service, Trags key and capture
consent untouched. It also keeps the .pre-poppy.bak backups that setup and
removal write next to each client file; delete those too if you are removing
Poppy for good, since a backup can hold the daemon token. Use it on its own, without installation flags such as
--daemon, --hooks, --claude-md or --yes.
Zero-touch capture
With the Claude Code, Cursor, or Codex hooks installed, Poppy can also extract durable
memories from your sessions automatically: decisions, preferences, and
lessons that would help a future session, written one sentence each.
Nothing is captured without your consent. Capture ships enabled by
default but stays inert until you record a one-time consent. If you already
had consolidate-enabled true set from an earlier version, that setting
counts as your recorded consent and capture stays active with no new prompt.
Setting POPPY_CONSOLIDATE=1 overrides the consent check entirely and turns
capture on regardless. The unified command reports the resolved effect plus
the project and global layers:
poppy setup claude-code asks once on a TTY (pass --yes for
non-interactive installs). Until you decide, every session start shows a
short notice and nothing runs. Opting out persists permanently; a later
default change can never silently re-enable capture you turned off.
What fires when, once automatic capture is enabled globally:
- Every 3rd prompt, a detached background worker reads only the
transcript turns added since the last capture and extracts memories
from that window. It never blocks your prompt, captures no turn twice,
and stops at a per-session soft cap in Claude Code and Cursor. Codex has no
soft cap because it has no end-of-session flush.
- At session end in Claude Code and Cursor, a backstop pass flushes whatever
the mid-session loop has not captured yet. Codex
Stop fires after each turn,
so Poppy uses it only as a liveness signal and never as a flush.
- Around context compaction, Claude Code consolidates the compact summary;
Cursor flushes the live transcript at
preCompact before compaction begins.
Extraction runs through the coding-agent CLI you already use (claude,
cursor-agent, codex, or gemini), on your existing login. That CLI sends
the transcript excerpt to the model provider you already use, so no new third
party sees your conversation. The store, your memories, and retrieval stay on
your machine; retrieval runs on local ONNX models. Poppy sends nothing to Trags
unless you turn sync on. It never auto-spends on a paid remote model: if only an
API-key backend is configured, capture stays off and tells you why.
Before anything is written, a reconciler dedups each candidate against
the store: clear duplicates are skipped, clear updates supersede the old
memory (tombstoned and restorable for 7 days), and everything uncertain
is added rather than merged.
Turning capture off for one project
Consent is global and one-time, but you can keep a single sensitive repo
(employer or client code) out of memory without turning capture off
everywhere:
The project is auto-detected from the current directory (a repo with a
.git, CLAUDE.md, pyproject.toml, package.json, ...); pass
--project if you're outside it. Disabled projects are stored as a
deny-list in ~/.poppy/config.json (disabled_projects), and the switch
sits above consent: a disabled project stays out of capture no matter
what the global consent says. Only automatic capture stops. Recall keeps
working in that project, and poppy remember still writes there, so you
stay in control of what gets stored. Session start shows a one-line
reminder that capture is off for the project.
Secret redaction
An extraction model can copy a credential out of your transcript into a
memory ("the staging DB is postgres://admin:hunter2@db"). Before a
captured candidate becomes a memory, Poppy masks secret-shaped tokens in
place with [REDACTED]. This runs at the earliest seam shared by all
three capture paths (mid-session, session end, post-compact), so the raw
value never reaches the store, the capture journal, or Trags sync.
Masked today: private-key blocks (-----BEGIN ... PRIVATE KEY-----),
passwords in connection-string URLs, Bearer tokens, AWS access key ids
(AKIA... / ASIA...), OpenAI-style sk- keys, and GitHub tokens
(ghp_, gho_, ghu_, ghs_, ghr_). The memory itself is always
kept, with only the secret masked, so a lesson does not get thrown away
over one token.
Customizing redaction
Add exact values or environment-variable names when your project uses secrets
that do not match the built-in shapes:
Literal entries are case-sensitive and match anywhere in captured content.
Environment-variable entries look up the variable's current value when Poppy
redacts a capture; poppy redaction list shows only the variable name, never
its value. Configured literals are secrets themselves, so list hides their
values too unless you pass --show. Literals and resolved environment values
must be at least four characters long. The six built-in pattern families always stay on, and custom
matches are masked in place with the same [REDACTED] marker.
This is a deliberately narrow deny-list of high-signal shapes, not a
general secret scanner: it will not catch every credential your session
touches, so treat it as a safety net rather than a guarantee. If a repo
is sensitive enough that this matters, turn capture off for that project
(above). Redaction applies to automatic capture only; a memory you write
yourself with poppy remember is stored exactly as you typed it.
What is stored, all locally under ~/.poppy/:
- the extracted memories themselves, in
memories.db, each tagged with
the source app and session id;
- a capture journal (
capture_journal.jsonl, owner-only mode 0600) recording
types and counts for the session banner and poppy doctor, without memory
text. Rewriting the journal removes older previews. Forgetting a memory also
attempts this cleanup after deletion; a journal error logs a warning and
does not prevent the deletion;
- per-session progress state (
capture_state.json). The journal and
state files are per-device and are never synced anywhere, not even to
Trags.
Every session start prints a one-line status banner (active with counts,
a loud INACTIVE when something is broken, or an autocapture consent nudge), and
poppy doctor reports the full capture state. POPPY_CONSOLIDATE=1/0
remains an explicit environment override in both directions; setting it to
1 bypasses the consent check entirely.
Engines
Retrieval runs on one of two engines. Switch any time with
poppy engines use <name> (--migrate re-embeds your existing memories for
the new engine):
bloom is where retrieval work lands, so its architecture can change between
releases; seed is a stable anchor that doesn't change under you. No engine
changes silently: every change ships in a release with notes, and poppy stats
shows the exact architecture you're running as engine vX.Y.Z.
Because a release can change the embedding model, your stored vectors may need
refreshing: run poppy migrate-engine after switching engines, or if recall
quality dips after a bloom upgrade. Memories whose vectors don't match the
active engine stay searchable by keyword in the meantime. Nothing is lost, and
poppy doctor tells you how many are waiting.
Commands
MCP remember also rejects blank content. MCP remember and edit accept only
fact, decision, preference, lesson, summary, or context as
memory_type; invalid inputs return tool errors. Unknown memory types received
from cloud sync become fact. SessionStart renders each stored note on one line,
collapsing whitespace in its content and type.
Cursor hooks are installed globally at ~/.cursor/hooks.json by
poppy setup cursor. Capture starts immediately once automatic capture is
enabled; Cursor has no hook trust step. Hook execution also depends on Cursor's
server-side enable_execute_hook_exec account flag. If hooks are installed but
capture stays silent, poppy doctor reports that flag as a possible cause.
Cursor resumes append to the same transcript and keep the same session id. A
pure Q&A resume that runs no tool can emit no hooks, including sessionEnd; a
future idle-session sweep will cover that accepted tail-loss edge case. Cursor
transcripts omit tool outputs, so automatic capture sees the user and assistant
text but cannot use command or tool results as evidence. In headless
cursor-agent -p runs, beforeSubmitPrompt never fires, so capture is
backstop-only: there is no every-third-prompt cadence or prompt-time behavior,
but sessionEnd still flushes the remaining transcript.
Codex capture has a one-time trust gate. After poppy setup codex, run Codex
interactively in the TUI once and approve the Poppy hooks. Headless codex exec
silently skips untrusted hooks. Hook trust is tied to the command hash, so editing
a hook command later requires approval again. Poppy never adds a trust-bypass
flag to Codex commands.
Run poppy COMMAND --help for full flag descriptions.
MCP tools
The MCP server (poppy serve) exposes these tools:
remember, forget, and edit are explicit writes by the agent, so they are
not gated by the automatic-capture consent described under
Zero-touch capture.
MCP daemon
By default each client starts its own poppy serve process over stdio.
poppy setup <client> --daemon instead points the client at one shared HTTP
server at http://127.0.0.1:7679/mcp, so every client talks to the same
process and the same loaded engine.
Setup installs and starts the daemon first, then writes its URL and a bearer
token into the client config. A plain stdio poppy serve also forwards to the
daemon when one is running or installed; pass --no-daemon to keep it in
process.
The daemon keeps its files in ~/.poppy: daemon.token holds the bearer
token and is readable only by you, and daemon.lock holds the PID of the
daemon that last held the lock. On macOS, and when no service is installed,
output goes to ~/.poppy/logs/daemon.log. On Linux, the service logs to the
journal: run journalctl --user -u poppy-daemon.service. If
poppy daemon start reports the daemon did not become reachable, check the
log.
The installed service saves POPPY_DAEMON_PORT and POPPY_DIR overrides set
when you run poppy daemon install. Reinstall and restart the service to change them.
Storage
Client setup saves an explicit POPPY_DIR override in MCP registrations and
prefixes installed hook commands with a shell-quoted POPPY_DIR assignment on
macOS and Linux so clients launched outside your shell use the same store. Run
setup again to change or remove that saved override.
Memories live at ~/.poppy/memories.db (set POPPY_DIR to override). The
schema is a single SQLite file with FTS5 plus an embeddings table; back it
up the same way you'd back up any SQLite DB.
Set POPPY_MODEL_IDLE_S to change the 600-second model idle-unload timeout; zero or an invalid value disables unloading.
Environment variables
POPPY_MODEL_IDLE_S, POPPY_DB_KEY, POPPY_TRAGS_API_KEY, POPPY_MCP_SOURCE,
POPPY_TELEMETRY_OFF, and POPPY_UPDATE_CHECK_OFF are described in their own
sections.
Encryption at rest (optional)
By default the local store is a plaintext SQLite file, protected only by file
permissions (~/.poppy is 0700). You can encrypt it at rest with a key held
in your operating system's keychain:
enable generates a random 256-bit key, stores it in the OS keychain (macOS
Keychain, Linux Secret Service, or Windows Credential Locker), and rewrites the
store as a SQLCipher database. Every page
is encrypted, including the FTS5 search index and the embedding vectors, and the
-wal sidecar's frame contents are encrypted too (the -shm sidecar is a
plaintext wal-index that holds no database content). Search and recall keep
working locally with no change to the CLI, MCP server, or web UI. Existing
plaintext stores are migrated the first time you run enable; new writes go
straight to the encrypted store. Stop poppy serve, poppy ui, and any capture
or sync workers before migrating; enable/disable refuse if they detect a
live writer rather than risk losing a write.
What this protects against: theft of the disk or of an unencrypted backup, and
casual inspection of the store file. What it does not do: this is local
encryption at rest, not zero-knowledge or end-to-end encryption. The key lives
in your keychain, so anything running as your user account that can read the
keychain can decrypt the store. Cloud sync is unaffected and is a separate
concern (memories synced to Trags are covered by that service's own encryption).
If the keychain entry is lost, the store cannot be recovered, so keep a backup
strategy that accounts for the key. On a headless host with no keychain, set the
POPPY_DB_KEY environment variable to a 64-hex-character key instead; note that
a key in an environment variable is weaker than one in the keychain, because it
is visible to anything that can read the process environment.
Sync to Trags (optional)
If you want your memories searchable across machines and inside the Trags
web app, run poppy setup trags to onboard. Sync is opt-in and additive;
your local store remains the source of truth.
poppy setup trags opens a browser page and shows a one-time code, including
how long it is valid. If it expires while you are in a terminal, Poppy offers a
fresh code, for up to three codes in total. In a non-interactive session, run
poppy setup trags again for a fresh code.
Once your Trags key is set up, by default each local change (a new memory, an
edit, a forget, a restore) starts a background sync: Poppy pulls changes from
Trags, then pushes yours. poppy sync run does the same by hand. A push sends
the memories that changed since the last push, and an upload that fails is
tried again on the next push. When two devices edit the same memory, the newer
edit wins.
On a converged store, poppy sync run and poppy sync pull may still report
pull: 1 live on every run. The pull watermark is inclusive, so when the newest
row on the server is a live memory, the pull serves it again. A push that sends
0 live and 0 tombstones shows that this machine has nothing left to upload. It
still counts the memories it skipped, and it does not prove the server agrees:
a deletion the server refused is not sent again.
Forgetting a memory is a soft delete: Trash keeps the record for at least
7 days and poppy restore brings it back, unless the memory's own expiry has
passed. When sync is configured, an expired deletion stays in Trash until the
current sync server has confirmed the deletion, so it can remain there past 7
days, until a sync succeeds. Two kinds of expired deletion leave Trash without
that confirmation: one the current server refused for good, and, when a sync
cleans up Trash, one for a memory the current server is not known to hold.
Poppy then keeps a small record, without the memory's content, so a pull does
not bring the memory back. A deletion goes to Trags only when
Trags is known to hold that memory, because this machine uploaded it or a pull
saw it there, so a memory that never left your machine does not reach the
server just because you deleted it. A deletion carries the whole memory, which
lets another device restore it from its own Trash.
Three limits apply:
- If an upload reaches Trags but the reply is lost, Poppy does not count that
memory as on Trags until a later push or pull confirms it. A deletion before
then stays local and the copy on Trags remains. Pulls only fetch rows changed
since the previous pull, so a later pull may not catch it.
- A deletion sends this machine's copy of the memory, including edits that
were never pushed.
- If sync was already set up when you upgraded to the release that added this
check, every memory that existed then counts as already on Trags, and
deleting one sends it even if it never got there. The full guarantee covers
memories created after the upgrade, and stores that had no Trags remote at
upgrade time.
After pairing, the Trags API key normally lives in your operating system's
keychain, and config.json holds no key. On macOS that is the login keychain,
under the service poppy-memory, with an account name that starts with
trags-api-key:. Check that it is there without printing the key:
The key goes into the keychain only when Poppy can write it and read it back
in the same process; otherwise it stays in plaintext in config.json (which
stays 0600), and setup tells you which one happened. Keys are resolved in
this order: the POPPY_TRAGS_API_KEY environment variable, then a plaintext
key in config.json, then the OS keychain. An existing plaintext key is moved
into the keychain automatically and scrubbed from the file the first time a
Poppy process can write it and read it back from the keychain.
On macOS, the daemon that poppy daemon install sets up is a LaunchAgent in
~/Library/LaunchAgents that runs in your login session. It reads the key
from the keychain, also after you log out and back in, so do not set
POPPY_TRAGS_API_KEY for it. Processes started outside a login session, such
as cron jobs, SSH sessions, and editor background jobs, can often write the
login keychain but not read it. Set POPPY_TRAGS_API_KEY for those; captures
they make are still pushed by the next sync a login-session process triggers,
such as the daemon. This is local secret hygiene, not zero-knowledge: anything
running as your user that can read the keychain can read the key.
Browse memories
Telemetry
Telemetry is off until you turn it on. The first time you run Poppy in a
terminal it asks once whether to send anonymous usage events; the default is
no, so pressing Enter declines. Until you answer yes, Poppy sends nothing.
Saying yes sends a small number of events to PostHog (EU region) so we can see
which features get used. The distinct ID is a random UUID generated on your
machine then and stored in ~/.poppy/analytics.json; it is not derived from
hardware, accounts, or anything identifying. Decline and no ID is generated at
all.
This is the complete list of events and the properties they carry. Every event
Poppy can emit is in this table; there are no others.
The last five are one-time funnel milestones: each fires at most once per
machine, ever, and then never again. The first four can fire repeatedly.
Never sent: memory content, recall query text, project names, file paths,
session ids, config values, API keys, or anything else you type into Poppy.
Properties are counts, lengths, version strings, and fixed enum values only.
If you onboard to Trags with poppy setup trags, the random device UUID is
shared with the Trags server so it can link this machine's telemetry to your
account.
The question is asked on stderr, and only when someone can actually answer
it: stdin, stdout and stderr all have to be a terminal, and Poppy must see no
sign of a coding agent or a CI runner driving that terminal. Hooks, the MCP
server (poppy serve), daemon commands, detached workers, --json output,
--help and any piped or redirected run are never interrupted by it, and
nothing is sent from any of them while the question is unanswered.
So if you only ever run Poppy through a coding agent, you never see the
question: the agent's terminal counts as a program, not a person. Telemetry
then stays off, and poppy telemetry status shows off (not answered yet).
Run a command such as poppy stats in a plain terminal to be asked, or answer
directly with poppy telemetry on or poppy telemetry off.
Your answer persists in ~/.poppy/config.json, and either answer counts, so
you are asked once. Change your mind at any time, or answer without waiting
to be asked:
Setting POPPY_TELEMETRY_OFF=1 in the environment also turns telemetry off
and overrides everything else. When telemetry is off, Poppy makes no
telemetry network calls at all.
Update check
poppy doctor may send one anonymous HTTP request to PyPI at most once every
24 hours to check the latest poppy-memory version. The request contains only
the installed version in its User-Agent and no identifiers. Normal commands
never make this request; they can only show a once-per-version hint already
stored in the local cache. Disable the check with POPPY_UPDATE_CHECK_OFF=1 or
poppy config set update_check off. Turning telemetry off, with
poppy telemetry off or POPPY_TELEMETRY_OFF=1, also turns it off.
So does leaving the telemetry question unanswered. A fresh install makes no
network request of any kind, to PyPI or to PostHog, until you have said yes.
Hacking on it
Clone the repo:
For a map of the code, see
docs/architecture.md.
For the exact source of a release, download it from PyPI instead:
License
Apache-2.0; the LICENSE file ships with the package. Poppy's source is
open, and you are free to read, run, self-host, fork, and modify it under
those terms. Poppy is maintained by the Trags team. We are not accepting pull
requests right now. Bug reports and feature requests are welcome as
GitHub issues, or by
email at [email protected]. For security vulnerabilities, email
[email protected] instead of filing publicly.