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.
pipx install poppy-memory
Then wire up your agent, and restart it:
poppy setup claude-code # or: claude-desktop, cursor, vscode, windsurf, codex,
# gemini, copilot-cli, pi, goose, hermes-agent
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
poppyis on your PATH:command -v poppyshould 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 rememberandpoppy 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 doctorto 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:
command -v poppy # prints a path such as /Users/you/.local/bin/poppy
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):
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
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
rememberstores a fact, decision, preference, or lesson.recallpulls the few memories most relevant to a query.- MCP server: agents call
remember/recall/forget/consolidatedirectly. 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.
poppy remember "we always use uv for python deps" --type preference
poppy recall "python package manager"
# we always use uv for python deps
# preference | 2026-05-25 | score: 0.91 | mem_3f9a1c2b7d4e
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.
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.
claude plugin marketplace add trags-garden/poppy
claude plugin install poppy@trags-garden
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, remove
Poppy's MCP entry and hooks from your Claude Code settings, 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.
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 autocapture # show resolved, project, and global status
poppy autocapture on --global # turn automatic capture on everywhere
poppy autocapture off --global # opt out everywhere (persists across upgrades)
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
Stopfires 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
preCompactbefore 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:
poppy autocapture off # in the repo: choose this project or everywhere
poppy autocapture on # capture this project again
poppy autocapture status # show the resolved, project, and global state
poppy autocapture off --project NAME # target a project by name instead
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:
poppy redaction add "company-specific-secret"
poppy redaction add --env INTERNAL_API_TOKEN
poppy redaction list
poppy redaction remove "company-specific-secret"
poppy redaction remove --env INTERNAL_API_TOKEN
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 andpoppy 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):
| Name | What it is |
|---|---|
bloom |
Default. Hybrid (FTS5 + bge-small embeddings + RRF) into a cross-encoder rerank. Runs on local ONNX models, so a plain pip install poppy-memory has everything it needs. |
seed |
FTS5 only. No ML deps, no model downloads. The floor, and the automatic fallback when bloom cannot start. |
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
| Command | Flags | What it does |
|---|---|---|
poppy remember CONTENT |
--type, --project, --ttl, --expires-at, --supersedes, --check-conflicts, --auto-supersede |
Store a memory; rejects empty or whitespace-only content. |
poppy recall QUERY |
--project, --type, --since, --limit, --json, --include-expired |
Search memories, ranked by relevance to the query. |
poppy list |
--project, --type, --since, --limit, --json, --include-expired |
List all memories, newest first. |
poppy edit MEMORY_ID |
--content, --type, --project, --no-project, --ttl, --expires-at, --no-expiry |
Edit a memory in place. |
poppy forget MEMORY_ID |
--yes |
Soft delete a memory by ID, restorable for 7 days. |
poppy restore MEMORY_ID |
Restore a forgotten memory from Trash. Trash keeps it for at least 7 days; a memory whose own expiry has passed is not restored. | |
poppy expire |
--yes |
List memories whose TTL has passed; --yes purges them. |
poppy stats |
Show memory stats. | |
poppy engines |
List the engine catalog with the active engine starred. | |
poppy engines use NAME |
--migrate |
Switch the active retrieval engine. |
poppy migrate-engine |
--project, --memory-type, --since, --all, --dry-run |
Re-embed memories so their vectors match the active engine. |
poppy config set KEY VALUE |
Set a config value. | |
poppy autocapture |
status, on, off, --global, --project, --yes |
Show or change automatic capture at project or global scope. See Zero-touch capture. |
poppy consent |
--enable, --disable, --status |
Legacy visible consent command; still works while callers migrate to poppy autocapture. |
poppy capture |
--off, --on, --status, --project |
Deprecated hidden alias for the per-project controls; still works while callers migrate. |
poppy telemetry |
status, on, off |
Show or change anonymous usage telemetry. See Telemetry. |
poppy encrypt |
status, enable, disable, repair |
Turn on encryption at rest for the local store. See Encryption at rest. |
poppy serve |
Start the Poppy MCP server (stdio). | |
poppy daemon |
status, install, uninstall, start, stop, restart, run |
Manage the shared MCP daemon that poppy setup --daemon connects clients to. See MCP daemon. |
poppy ui |
--host, --port, --no-open, --allow-remote |
Browse and manage memories in a local web UI. --allow-remote permits a non-loopback --host; the UI has no authentication, so this exposes its delete and edit API to your network. |
poppy setup claude-code |
--hooks/--no-hooks, --claude-md/--no-claude-md, --yes, --daemon |
Install Poppy into Claude Code (MCP + hooks + CLAUDE.md primer). Adds only the CLAUDE.md block, and only with an explicit --claude-md, when the Claude Code plugin is installed. |
poppy setup claude-desktop |
--print-instructions, --print-import-prompt |
Register the Poppy MCP server in the Claude desktop app. |
poppy setup cursor |
--hooks/--no-hooks, --daemon |
Install Poppy into Cursor (MCP + zero-touch capture hooks). Honors $CURSOR_HOME. |
poppy setup vscode |
--daemon |
Install Poppy into VS Code (MCP only). |
poppy setup windsurf |
Install Poppy into Windsurf (MCP only). | |
poppy setup codex |
--hooks/--no-hooks, --daemon |
Install Poppy into Codex (MCP + cadence capture hooks + AGENTS.md primer). Honors $CODEX_HOME. |
poppy setup gemini |
--daemon |
Install Poppy into Gemini CLI (MCP only). |
poppy setup copilot-cli |
Install Poppy into GitHub Copilot CLI (MCP via $COPILOT_HOME/mcp-config.json + $COPILOT_HOME/copilot-instructions.md primer; defaults to ~/.copilot). |
|
poppy setup pi |
Install Poppy into Pi (MCP via pi-mcp-adapter + AGENTS.md primer in $PI_CODING_AGENT_DIR; defaults to ~/.pi/agent). |
|
poppy setup goose |
Install Poppy into Goose (MCP extension + .goosehints primer). | |
poppy setup hermes-agent |
Install Poppy as a Hermes Agent memory provider plugin with SOUL.md guidance. | |
poppy setup trags |
--api-url |
One-command device-code onboarding for Trags cloud sync. |
poppy sync push |
--dry-run |
Send local memories + tombstones to Trags. |
poppy sync pull |
--dry-run |
Apply Trags rows newer than the last pull watermark. |
poppy sync run |
--dry-run |
Pull then push, full bidirectional sync. |
poppy sync status |
Show watermarks and last-sync time. | |
poppy import claude-memories |
--dry-run, --projects-dir |
Import auto-memory files from ~/.claude/projects/<slug>/memory/. |
poppy import hermes-memories |
--dry-run, --memories-dir |
Import paragraphs from ~/.hermes/memories/{MEMORY,USER}.md. |
poppy hook ... |
Claude Code, Cursor, and Codex hook entrypoints (session-start, user-prompt-submit, pre-tool-use, post-compact, session-end, stop, replay-compact, replay-session-end). Invoked by Poppy-installed hooks. |
|
poppy doctor |
Verify the installation: engine, storage, MCP config, hooks, and the Claude Code plugin. |
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 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.
| Subcommand | What it does |
|---|---|
poppy daemon status |
Show whether the service is installed, whether the daemon holds its lock, whether it answers over HTTP, and its version. |
poppy daemon install |
Install and load a per-user service: a launchd agent on macOS, a systemd user unit on Linux. Other platforms are not supported. |
poppy daemon uninstall |
Unload and remove that service. |
poppy daemon start |
Start the daemon through the installed service, or as a detached background process when no service is installed. |
poppy daemon stop |
Stop the daemon, through the service when one is installed. On Linux, the next client that starts poppy serve starts the service again; use uninstall to keep it off. |
poppy daemon restart |
Stop a running daemon, then start it again. Run it after upgrading Poppy so clients reach the new version. |
poppy daemon run |
Run the daemon in the foreground. This is what the service runs. |
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.
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:
# pipx installs poppy in its own virtualenv, so inject the extra dependency there:
pipx inject poppy-memory sqlcipher3
# (plain pip installs instead: pip install 'poppy-memory[encryption]')
poppy encrypt enable # migrates the existing store in place
poppy encrypt status # show state
poppy encrypt disable # decrypt back to plaintext
poppy encrypt repair # reconcile an inconsistent state, if ever needed
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. Approve it
within five minutes: the code expires after that, and the command stops
waiting at the same time. If you see Setup code expired or already used or
Timed out waiting for authorization, 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:
security find-generic-password -s poppy-memory # shows attributes only
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
poppy ui # opens a local web UI at http://127.0.0.1:7800
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.
| Event | When | Properties |
|---|---|---|
cli_install |
once, before the first event from a machine | version (Poppy version), python_version, platform (darwin, linux, win32) |
memory_write |
each memory stored by poppy remember or the MCP remember and consolidate tools; automatic capture, imports, and memories edited or superseded in poppy ui do not send it |
memory_type (fact, decision, preference, lesson, summary, context), has_project (true/false, never the project name), source (manual for poppy remember, agent for an MCP client that does not name itself, a known client such as claude-code or cursor, or other for any client Poppy does not know; the connecting client's own name is slugified before comparison, so Claude Memory becomes claude-memory; ui, claude-memory and hermes-memory appear only when the MCP server's --source setting or the connecting client's own name is one of them; a --source or POPPY_MCP_SOURCE value is only trimmed and lowercased, so Claude Memory there becomes claude memory and is reported as other; POPPY_MCP_SOURCE sets the same value as --source, and both apply only when the MCP server runs in process, so a poppy serve that forwards to the daemon ignores them) |
recall_call |
poppy recall |
query_length (character count, never the query text), result_count, engine (engine name) |
agent_setup |
every poppy setup <client> |
agent (client name, for example claude-code), and via (plugin) when the Claude Code plugin was already installed |
setup_completed |
once per machine, on the first poppy setup <client> |
agent (client name), and via (plugin) when the Claude Code plugin was already installed |
consent_granted |
once per machine, when you turn automatic capture on | via (consent_cmd, setup_yes, or setup_prompt) |
consent_pending_shown |
once per machine, when the consent nudge is first shown | channel (session_start or setup) |
first_autocapture_stored |
once per machine, the first time automatic capture stores a memory | trigger (mid_session, post_compact, or session_end) |
closed_loop |
once per machine, the first time a memory captured in an earlier session is recalled back into a session | channel (session_start, recall_prompt, or recall_file), injected_count, loop_count (counts only) |
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:
poppy telemetry status # on, off, or not answered yet
poppy telemetry on
poppy telemetry off
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:
git clone https://github.com/trags-garden/poppy
cd poppy
uv sync
uv run pytest -v
uv run poppy --help
For a map of the code, see docs/architecture.md.
For the exact source of a release, download it from PyPI instead:
pip download poppy-memory --no-binary :all: --no-deps -d .
tar xzf poppy_memory-*.tar.gz && cd poppy_memory-*/
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 feedback@trags.ai. For security vulnerabilities, email
security@trags.ai instead of filing publicly.
Metadata
Release files for poppy-memory 0.3.6
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| poppy_memory-0.3.6.tar.gz | 1.1 MB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| poppy_memory-0.3.6-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.9 MB
Release files / poppy_memory-0.3.6.tar.gz
| Download URL | poppy_memory-0.3.6.tar.gz |
|---|---|
| Size | 1.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5227047725ec935f5ee6bbeed3aecb8b3353764b55f4b7933272726ccebea4d9
|
|
BLAKE2b-256 checksum How to use checksums |
e48b5621948843583a17afb7df8b35e359c65667a911597913958775e9281b8e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logRelease files / poppy_memory-0.3.6-py3-none-any.whl
| Download URL | poppy_memory-0.3.6-py3-none-any.whl |
|---|---|
| Size | 767.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
bf26216c17f83fc444879b573332bbb94647b3868a464502f635fb544feaebab
|
|
BLAKE2b-256 checksum How to use checksums |
d30069506b0e1fef9e7a12e76c44830f308f1c66d8915a00f44cb77e616c4196
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logSecurity for poppy-memory 0.3.6
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for poppy_memory-0.3.6.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 25, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.