Skip to main content
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.

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:

  1. Install Poppy with pipx, uv, or the one-line installer. See Install.
  2. Check that poppy is on your PATH: command -v poppy should print a path. See Check your PATH.
  3. Wire up your agent with poppy setup <client>. See Supported clients.
  4. Restart the agent so it loads the MCP server and hooks. See Restart the agent after setup.
  5. Store and recall once with poppy remember and poppy recall. See What it does.
  6. Pair with Trags (optional) with poppy setup trags, and approve it in the browser within five minutes. See Sync to Trags.
  7. Install the daemon (optional) with poppy daemon install. See MCP daemon.
  8. 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:

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

  • remember stores a fact, decision, preference, or lesson.
  • recall pulls the few memories most relevant to a query.
  • MCP server: agents call remember/recall/forget/consolidate 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.
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 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:

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 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):

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)

Source distribution for poppy-memory 0.3.6
File Size Uploaded Provenance
poppy_memory-0.3.6.tar.gz 1.1 MB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for poppy-memory 0.3.6
File Interpreter ABI Platform
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 log

Release 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 log

Security 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.

Select from 2 files

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 log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page