Ghost

August 26, 2026 · View on GitHub

Ghost

MCP memory server for Claude Code, Cursor, and any MCP client. Pure Go. Single binary. No external services required.

Your agent's memory, on your disk — no cloud, no accounts, no subscription. One SQLite file you own.

CI Release Go License


Ghost beats every published competitor on LongMemEval-S (500-question blended, retrieve → generate → judge):

SystemScoreGeneratorSource
Ghost (hybrid)96.2%DeepSeek V4 ProThis repo
Mem094.4%Not specifiedmem0.ai/research — "managed platform, proprietary optimizations not in OSS SDK"
Hindsight91.4%Gemini-3 Proarxiv 2512.12818 — independently validated by Virginia Tech + Washington Post
Supermemory85.2%Gemini-3supermemory.ai/research — self-reported

Read carefully: these numbers are not directly comparable across rows — each uses a different generator and judge. Within the same generator+judge pair, differences are meaningful; across pairs, they're directional only. Full methodology and retrieval-only benchmarks further down in Benchmarks.


Quick start

Two commands. No accounts, no keys, no docker-compose, no vector database.

go install github.com/wcatz/ghost/cmd/ghost@latest
ghost mcp init

ghost mcp init registers Ghost as an MCP server, installs the session-start hook, migrates your existing Claude Code memories — projects Ghost already knows are imported at init, the rest auto-import on their first ghost_project_context call (read-only, nothing is lost) — and disables the built-in file memory so the two don't fight. It's idempotent and non-destructive — safe to re-run anytime, and --dry-run previews every change.

Then start a session. Ghost injects your project's context automatically and starts remembering.

Using opencode (with Ollama)? Skip the Claude Code init entirely — register Ghost as an MCP server:

ghost mcp init --client opencode
# optional but recommended — enables hybrid vector search:
ollama pull nomic-embed-text:v1.5

This installs a single lifecycle plugin to ~/.config/opencode/plugins/ghost-opencode.ts that does everything: it registers the ghost MCP server (via opencode's plugin config hook) and bridges session-idle events to ghost's stop hook (memory reflection, resolve, supersede). Your opencode.json is never touched. Restart opencode — Ghost's ghost_* tools and lifecycle processing go live automatically. Verify with ghost mcp status --client opencode.

No Go toolchain? Grab a prebuilt binary from Releases — linux, macOS, and Windows, amd64 and arm64, with checksums.txt. Building from source needs Go 1.26+ (older toolchains fetch it automatically via GOTOOLCHAIN=auto).

On Windows? Skip the manual download/unzip/PATH steps with the one-command installer:

irm https://github.com/wcatz/ghost/releases/latest/download/install.ps1 | iex

This downloads the latest release, verifies its checksum, installs ghost.exe to %LOCALAPPDATA%\ghost\bin, and adds that directory to your user PATH. Open a new terminal afterward, then run ghost mcp init.

Re-running the command upgrades an existing install in place.

Using Cursor, Goose, or another MCP client? Ghost speaks standard MCP over stdio — point any client at the binary:

{ "mcpServers": { "ghost": { "type": "stdio", "command": "ghost", "args": ["mcp"] } } }

Using opencode? Don't edit any config — ghost mcp init --client opencode installs a single plugin (~/.config/opencode/plugins/ghost-opencode.ts) that registers the MCP server itself (via opencode's plugin config hook) and bridges session-idle events to ghost's stop hook:

ghost mcp init --client opencode   # installs the plugin; restart opencode after

Using codex? ghost mcp init --client codex merges [mcp_servers.ghost] into ~/.codex/config.toml (textually — your comments survive) and wires SessionStart/Stop/SessionEnd into ~/.codex/hooks.json. Then run /hooks inside codex once and approve the ghost entries — codex silently skips untrusted hooks.

ghost mcp init --client codex   # then /hooks in codex to trust the entries

Using goose? ghost mcp init --client goose installs an Agent Plugins package at ~/.agents/plugins/ghost/ (plugin.json, mcp.json, and Open Plugins hooks). Goose's native payload fields (event, working_dir) are aliased onto the contract internally, so no shim scripts are involved:

ghost mcp init --client goose

Docker (multi-arch, amd64 + arm64):

docker run -i -e XDG_DATA_HOME=/data -v ghost-data:/data ghcr.io/wcatz/ghost:latest

-i matters — MCP speaks over stdio, and XDG_DATA_HOME=/data is what makes the volume actually hold ghost.db. For consolidation, run reflect against the same volume (the MCP server itself never uses the API key):

docker run -i -e XDG_DATA_HOME=/data -v ghost-data:/data \
  -e ANTHROPIC_API_KEY=sk-ant-... ghcr.io/wcatz/ghost:latest reflect myproject --apply

Why not just use built-in memory?

ChatGPT, Claude, Gemini, and Copilot all ship native memory now — but each one is walled off inside its own product. Nothing you teach ChatGPT carries over to Claude Code, and nothing Claude Code learns carries over to Cursor or Goose. Ghost's bet isn't "better than any single one of those" — it's one memory, across every MCP client, that lives on your own disk: a local SQLite file you can query, back up, and delete, instead of a separate silo per product. See Why Ghost? below for the specific comparison against Claude Code's built-in memory.

Why Ghost?

Coding agents forget everything between sessions. You re-explain your architecture, your conventions, and that one gotcha with the staging database — every single day.

Claude Code's built-in memory is a markdown file with a limited load window (~200 lines). No search, no categories, no dedup, and memory is siloed per repository. Ghost replaces it with a real memory system:

Claude Code built-inGhost
StorageFlat .md files, limited load windowSQLite + FTS5, unlimited
SearchNone (linear load)Full-text + optional local vector search
CategorizationNone8 categories with importance scores
DedupNone (appends forever)FTS-based upsert — merges on save
ConsolidationNone (Dreams, managed)Anthropic API, CLI fallback, or local Jaccard
Time decayNone (stale facts persist equally)Category-aware: conventions never decay, gotchas fade
Cross-projectNone (siloed per repository)ghost_search_all + _global project
Memory graphNoneAuto-linked related memories, graph view in Obsidian
ClientsClaude Code onlyAny MCP client

Switching migrates your existing Claude Code memories into Ghost — at init or on first contact. Nothing is lost.

How it stacks up

Ghost's bet: a memory system should be smaller than the thing it remembers. The alternatives make you choose between cloud memory services (your codebase's context on someone else's server, metered per request) and self-hosted stacks (Postgres plus a vector DB before you've saved a single memory).

As far as we know, Ghost is the only memory system that packs local hybrid vector + full-text search, on-demand consolidation, time-decay scoring, memory lifecycle management, and a memory graph into a single zero-infrastructure binary. The field as of August 2026 — corrections welcome, open an issue:

What you installVector searchConsolidationTime decayMemory lifecycleAny MCP client
Ghostone static Go binarylocal (Ollama, optional)yesyesresolve + supersede + demote + linkyes
Mem0 (self-hosted)FastAPI + Postgres + Qdrant/Neo4jserver-sideyesnoextraction onlyvia OpenMemory (Docker)
Zep (self-hosted)Graphiti + Neo4j/FalkorDBserver-sidenotemporal graphtemporal fact chainsyes (MCP)
Supermemoryself-hosted binary or cloudhybridyestemporaldual-profileyes (MCP)
Claude Codebuilt-in to CLIfile-based indexDreams (managed)nononeClaude Code only
Engramone Go binaryno (FTS only)nononoyes

Mem0 and Zep are excellent products, but self-hosting them means running a service stack (Postgres, Neo4j, Qdrant). Supermemory offers a self-hosted binary or cloud option. Ghost ships as a single binary with zero infrastructure. If all you want is full-text search in a single binary, Engram is a fine, simpler choice.

The questions you should be asking

Where does my data go?

One SQLite file under ~/.local/share/ghost (or $XDG_DATA_HOME/ghost) — this path is the same on every OS, including Windows (i.e. %USERPROFILE%\.local\share\ghost, not %AppData%); only the config file follows the OS-native convention (see Configuration below). Ghost makes no network calls in normal operation, with three exceptions you control: localhost Ollama for embeddings (optional), the Anthropic API when ANTHROPIC_API_KEY is set and no CLI binary (claude/opencode/codex/goose) is available for consolidation, resolve, or supersede, and the GitHub API only if you run ghost upgrade. That's the complete list.

What exactly gets injected into my agent's context?

A bounded digest, and you can inspect it yourself. The session-start hook emits: project name, top memories, learned context, open tasks, and active decisions. Global memories (the _global project) are injected even when the cwd matches no known project. See precisely what your agent sees:

echo '{"contract":{"version":1,"source":"claude-code","transcript_format":"claude-jsonl"},"hook_event_name":"SessionStart","cwd":"'"$PWD"'"}' | ghost hook session-start --source claude-code

No mystery blob in your system prompt. Save-time dedup keeps the digest from bloating, and time-decay scoring weights ghost_project_context and resource reads toward what's still true. Subagent sessions get nothing (they inherit context in-band from the parent); resume skips injection; compact emits a one-line pointer instead of re-dumping. Full details in docs/architecture.md.

What's the exit story?

Your memories are a plain SQLite database in one file. Open it with sqlite3, query it with any tool, back it up with cp. No proprietary format, no export request form. The schema is a readable Go string constant in internal/memory/schema.go. If you stop using Ghost tomorrow, your memories are sitting there in a format that will outlive all of us.

Switching in is just as easy: ghost mcp init imports Claude Code memories, and even without running init, Ghost auto-imports (read-only) on the first ghost_project_context call for a project with zero memories.

Where's the off switch?

  • Per client: remove the ghost entry from your MCP config. Ghost only runs when your client spawns it over stdio — there is no daemon.
  • Embeddings: set embedding.enabled: false in your config file (see Configuration for the OS-specific path — %AppData%\ghost\config.yaml on Windows, ~/.config/ghost/config.yaml elsewhere).
  • Consolidation: never runs unless you invoke ghost reflect — and that's a dry run unless you pass --apply.
  • Everything: delete $XDG_DATA_HOME/ghost, or ~/.local/share/ghost when XDG_DATA_HOME is unset. There is nothing else.

What does it cost to run?

$0/month. No metered API in the hot path. When ANTHROPIC_API_KEY is set and no CLI binary is available, consolidate/resolve/supersede use the Anthropic API (cost scales with memory count — roughly $0.001 for a typical project); otherwise they fall back to a free CLI call (claude, opencode, codex, or goose) or the fully offline SQLite tier.

How it works

Ghost is a memory pipeline: Save → Embed → Link → Search → Consolidate → Decay.

8 memory categories

architecture · decision · pattern · convention · gotcha · dependency · preference · fact — enforced by a SQLite CHECK constraint, not vibes. Saving a near-duplicate strengthens the existing memory instead of piling up copies (FTS-overlap dedup, same category).

Full-text (FTS5) and vector results are fused with Reciprocal Rank Fusion (k=60), weighted 70% vector / 30% FTS. A background worker links similar memories (cosine ≥ 0.70) into a graph, which powers the Obsidian mirror's graph view and future link-aware features; links self-heal after consolidation rewrites memories. An experimental graph-expansion ranking bonus exists but ships disabled — our own benchmark sweep (ghost bench --sweep) showed it demoting exact matches, so it stays off until a redesign beats that measurement (methodology).

Vectors come from a local Ollama instance (nomic-embed-text:v1.5, 768 dims) if one is running. No Ollama? No error, no setup step — Ghost is fully functional with FTS5-only search and quietly upgrades to hybrid the moment Ollama appears:

ollama pull nomic-embed-text:v1.5

Time-decay scoring

Facts about your stack shouldn't expire. Last month's debugging detour should. The score multiplier is max(floor, 1 / (1 + age_days / scale)):

CategoryDecayFloor
preference, convention, factnever
architecture, pattern45-day scale0.3
decision, gotcha, dependency30-day scale0.15

Pinned memories are fully exempt from decay — they score at raw importance regardless of age or category.

Consolidation you can undo

ghost reflect merges duplicates, prunes noise, and promotes cross-project knowledge to global scope. Tiered: Anthropic API (Haiku) first, then a CLI tier (claude, opencode, codex, or goose — whichever is on PATH), falling back to a fully offline SQLite tier (Jaccard >= 0.5, same-category merges). When --source is set (e.g. --source opencode), the API tier is skipped entirely and the matching CLI binary is used directly.

Because an LLM rewriting your memory store is scary, the guardrails are layered:

  • Dry run by default — see the diff before --apply
  • Auto-snapshot before every replace, keeping the 3 most recent per project; ghost reflect --restore is the undo button
  • Empty-set refusal — the store layer will not replace your memories with nothing, ever
  • Quality gate — in auto mode, output shrinking below 30% of input is rejected and the next tier is tried (when input ≥ 6 memories)
  • Manually saved memories are always preserved

Tasks, decisions, and global memory

Beyond memories: tasks (pending/active/done/blocked), decision records with rationale and alternatives (active/superseded/revisit), and a _global project whose memories are included in every project's context. Projects resolve by longest path-prefix match with a basename fallback, so worktrees and moved checkouts still find their memory.

Memory lifecycle

Saving a memory is the beginning, not the end. Ghost tracks what happened after you saved — which facts got replaced, which findings turned out to be intermediate, which memories are near-duplicates of each other — and uses that to keep search results honest:

  • Resolve (ghost resolve) — marks resolved-evidence memories (changelog entries, cost estimates, closed experiment notes) with resolved_at, dropping them from ranked injection while keeping them searchable. Uses MCP Sampling for zero-credit classification in live sessions; the CLI path uses an API key.
  • Supersede (ghost supersede) — creates directed supersedes links between memories (newer replaces older). A single LLM call classifies each candidate pair as SUPERSEDES / CAUSES / NEITHER. Re-runnable and self-healing after consolidation rewrites memories.
  • Demote — when a superseded memory and its replacement both appear in search results, the older one is sunk below every present superseder. Targeted demotion on genuine replacement pairs only (a blanket age-only recency prior destroys old-but-correct retrieval — measured, published, ship-off). Flips staleness fresh-wins from 0.083 to 1.000 while leaving unrelated retrieval untouched (see staleness suite).
  • Link — a background worker auto-links related memories (cosine ≥ 0.70) into a graph. Links power the Obsidian mirror's graph view, supersedes ranking, and near-duplicate demotion at injection time.

Most memory systems extract a fact and forget about it. Ghost's lifecycle is the pipeline that keeps facts true over time.

Obsidian vault mirror

Your memories are yours to browse. ghost obsidian export mirrors memories, decisions, and tasks into plain Markdown notes — one folder per project — that Obsidian opens as a vault, with memory links rendered as wikilinks so the graph view maps your knowledge. ghost obsidian sync keeps the mirror fresh by polling for database changes; --project scopes the mirror to a single project (plus Global). The mirror is strictly one-way: it reads the database read-only (safe alongside a live MCP server), and it only ever prunes stale notes inside a directory carrying the .ghost-vault marker — backing off entirely when a listing might be incomplete, so stale extras beat silent deletions.

ghost obsidian export --out ~/Documents/GhostVault   # one-shot mirror
ghost obsidian sync --interval 30s                   # keep it fresh

Set obsidian.auto_sync: true in config to have the session-start hook spawn ghost obsidian sync in the background automatically instead of running it by hand — off by default, so nobody gets a vault directory or a background process without asking for it.

Using the vault

Every note carries YAML frontmatter (category, importance, pinned, project, tags, created, updated, source) and an aliases entry — a short single-line preview of the content — so the graph view and Quick Switcher show a readable label instead of the id-suffixed filename. That structure turns the mirror into a queryable knowledge base with no extra tooling:

  • Graph view already maps your memories: each ## Related wikilink is an edge, so the auto-linked graph renders natively. Colour nodes by folder (project) or tag to see clusters.

  • Dataview queries over the frontmatter give live tables without touching Ghost — e.g. pinned gotchas, stale memories, or decisions by status:

    ```dataview
    TABLE importance, updated FROM "Ghost"
    WHERE type = "memory" AND pinned = true AND category = "gotcha"
    SORT importance DESC
    ```
    
  • Backlinks show what links into a memory; the tag pane browses tags:; local graph and Canvas explore or arrange nodes spatially.

Because the mirror is one-way, edits inside the vault are informational only and are not synced back to Ghost — and if sync is running it will overwrite hand-edits on the next database change. Change memories through the MCP tools (or ghost CLI), not by editing notes.

MCP surface

20 tools, 4 resources, 2 prompts:

GroupTools
Memoryghost_memory_save ghost_memory_search ghost_search_all ghost_memories_list ghost_memory_update ghost_memory_delete ghost_memory_pin ghost_memory_promote ghost_save_global ghost_resolve ghost_project_delete
Contextghost_project_context ghost_list_projects ghost_health
Tasksghost_task_create ghost_task_list ghost_task_update ghost_task_complete
Decisionsghost_decision_record ghost_decisions_list

ghost_resolve scans a project's memories for resolved-evidence notes (intermediate findings, changelog entries, superseded experiments) using the calling session's own model via MCP sampling when the client supports it, falling back to a subscription-billed claude -p call otherwise (dry-run only on that fallback) — no Anthropic API credits spent either way. Args: project (required), apply (default false: dry-run preview only; pass true to stamp resolved_at on confirmed memories).

Resources: project context, global memories, project decisions, project tasks — pin them in clients that support it to survive context compaction.

Prompts: recall_project (injects project context into the conversation), record_decision (guides structured decision recording).

The server ships with embedded instructions that teach the agent when to save, which categories to use, and how to leverage cross-project search — it works proactively without configuration. Full architecture notes in docs/architecture.md.

CLI

ghost mcp                         # Run MCP server on stdio (used by your MCP client)
ghost mcp init [--client claude|opencode|codex|goose|all] [--dry-run]  # Configure MCP client (auto-detects when --client omitted)
ghost mcp status [--client claude|opencode|codex|goose]                # Deep health checks (incl. Ollama reachability, model presence)
ghost hook <event> --source <host>   # Contract-v1 lifecycle hook (session-start, stop, session-end)
ghost reflect <project> [flags]      # Memory consolidation (dry-run by default; --apply, --restore, --tier, --source)
ghost resolve <project> [flags]      # De-weight resolved-evidence memories (dry-run by default; --apply, --source)
ghost supersede <project> [flags]    # Link superseded memories (dry-run by default; --apply, --threshold, --source)
ghost project delete <name> [flags]  # Permanently delete a project (dry-run by default; --apply + name re-type to confirm)
ghost project merge <old> <new>      # Merge one project into another; child records move to the survivor
ghost context [--cwd <dir>]          # Print the passive session-start context block (for opencode)
ghost bench [--sweep]                # Retrieval-quality benchmark on the built-in dataset
ghost obsidian export                # Mirror memories to an Obsidian vault (one-way; --out, --project)
ghost obsidian sync                  # Keep the vault mirror fresh (--interval; polls for DB changes)
ghost upgrade                        # Self-update from GitHub Releases (linux/macOS; Windows: re-download)
ghost version                        # Print version

ghost mcp init auto-detects which MCP clients are on PATH and installs for all of them. When only one is found, it installs for that one; when multiple are found, it installs for all; when none are found, it errors with install instructions. Use --client to override and target a specific client (e.g. --client opencode installs a single lifecycle plugin to ~/.config/opencode/plugins/ghost-opencode.ts that self-registers the MCP server and bridges stop events — opencode's own config file is never modified; re-running init repairs an outdated plugin in place).

When reflection.auto_resolve is enabled in config (default off), the stop hook also spawns ghost resolve <project> --apply as a detached background process after each session, so resolved-evidence memories get marked automatically without waiting for a manual run. This never blocks the hook itself — the spawn is fire-and-forget, logged to resolve.log in the ghost data directory. If the Anthropic API is out of credit at spawn time, the spawned process fails and logs the failure; it does not degrade to a lower-quality answer.

Configuration

Ghost works with zero config. When you want to change something, layers are (later wins):

  1. Compiled defaults
  2. /etc/ghost/config.yaml
  3. ~/.config/ghost/config.yaml (honors $XDG_CONFIG_HOME when set; on Windows, absent an XDG_CONFIG_HOME override, this resolves to %AppData%\ghost\config.yaml)
  4. GHOST_* environment variables, plus ANTHROPIC_API_KEY (used by reflect/resolve/supersede when no CLI binary is available)
embedding:
  enabled: true                          # default; degrades gracefully without Ollama
  ollama_url: "http://localhost:11434"
  model: "nomic-embed-text:v1.5"
linking:
  enabled: true                          # on by default when embedding is enabled
  threshold: 0.70                        # min cosine similarity to auto-link memories

Note: env-var names map underscores to config dots, so keys that themselves contain underscores (e.g. embedding.ollama_url, obsidian.vault_dir) need an explicit shortcut rather than the generic GHOST_* mapping; GHOST_OLLAMA_URL and GHOST_OBSIDIAN_VAULT_DIR are provided for exactly that. Running Ghost inside a VM with Ollama on the host? Point it at the host's gateway IP instead of hand-editing the config file, e.g. GHOST_OLLAMA_URL=http://10.0.2.2:11434 (the default host-gateway address for UTM/QEMU on macOS; other hypervisors use their own convention, such as host.docker.internal for Docker Desktop).

OpenCode integration: set GHOST_OPENCODE_MODEL to pin the model for opencode-backed tiers (e.g. GHOST_OPENCODE_MODEL=big-pickle). The opencode backend runs opencode run --pure --title "[ghost]" per LLM call — headless sessions are titled [ghost] so they're filterable in opencode's session search. Authentication is handled by opencode's own provider config; Ghost does not need OPENCODE_API_KEY directly.

Benchmarks

Every Ghost number below is reproducible with the in-repo harnesses, and shipped with per-question logs — the competitor figures in the comparison table further down are externally sourced and not reproducible from this repo. Retrieval-only metrics are deterministic given the embedding cache; end-to-end scores are recorded runs (model-pinned, single-run — rerun variance is possible but small at temperature 0). Full methodology in docs/benchmarks.md.

LongMemEval-S (the consensus long-term-memory benchmark; cleaned variant, session-level retrieval against the official evidence labels, all 470 answerable questions, no LLM judge):

condition   R@1     R@5     R@10    MRR@10  NDCG@10   wall clock (470 questions)
fts-only    0.429   0.751   0.832   0.758   0.738     44s
vector      0.558   0.926   0.968   0.911   0.909     ~1m (warm embedding cache)
hybrid      0.532   0.930   0.973   0.901   0.903     one-time cold embedding ~12h on ARM64 CPU

Hybrid session Recall@5 93.0%, Recall@10 97.3% — in the band of the best-reported hybrid retrieval results on this benchmark, using nothing but the single Ghost binary and local Ollama embeddings. Harness + per-question logs: bench/longmemeval/. Honest nuances: retrieval-only numbers are not comparable to end-to-end answer-accuracy percentages (those depend mostly on the generator model); and on this chat-style data vector-only ties hybrid — the keyword leg earns its keep on exact identifiers (ports, versions, hostnames), which is what the next table shows. These are full-run wall-clock totals, not per-query latency — Ghost's harness doesn't instrument per-query p50/p95 yet, so unlike some competitor benchmarks, there's no per-query latency figure to publish here honestly.

ghost bench — the in-repo dev-facts dataset, runs in seconds, regression-guarded in CI:

$ ghost bench
condition          R@1     R@5    R@10   MRR@10  NDCG@10
fts-only         0.786   0.964   1.000    0.964    0.965
vector-only      0.786   0.929   0.964    0.952    0.946
hybrid           0.857   0.964   1.000    1.000    0.989

14 graded queries, 22 memories. Retrieval-only, no LLM judge.
  • Hybrid fusion beats both single legs here (NDCG@10 0.989 vs 0.965 full-text, 0.946 vector) — CI asserts that relationship on every PR. Across both benchmarks, fusion is the robustness play: vectors win conversational recall, keywords win exact identifiers.
  • We ran the ablations, found our own regression, and removed it. An additive graph-expansion ranking bonus hurt retrieval — a public LongMemEval-S kill experiment showed its recoveries were a strict subset of a deeper vector-k's, with no headroom at production depth — so it was removed entirely rather than kept disabled. The link graph itself is retained for the Obsidian mirror and supersedes ranking. ghost bench --sweep grid-searches the fusion parameters if you want to check our tuning.
  • The staleness suite ("prod ran Postgres 14, we migrated to 16" — does search rank the fresh fact first?) runs report-only in CI. A recency-trap fixture (older memory is the correct answer) proved a blanket age-only prior can't be the default: it's a cliff, every weight that fixes staleness destroys old-but-still-correct retrieval. The fix that survives it is category-aware: search now applies the time-decay factor (pinned / preference / convention / fact never decay; pattern/architecture τ=45; decision/gotcha/dependency τ=30) to reorder the result window, so the staleness suite's updated-deployment facts (dependency category) flip fresh-wins 0.083 → 1.000 while the trap's fact memories stay flat at 0.929 — the free lunch the blanket prior couldn't achieve. Decay is ordering-only (it never drops a relevant memory). Both halves ship: ghost supersede creates supersedes links (cosine proposes, Haiku confirms — 8/8 on a labeled set), and DefaultSearchParams ships DecayEnabled: true + SupersedeDemote: true, so production search (ghost_memory_search, ghost_search_all) is time-aware and consumes supersedes links by default. Link creation stays opt-in: the demote is a hard no-op until you run ghost supersede --apply. Publishing the negative result, the reason, and the fix that survives it is the point.

End-to-end LongMemEval-S (retrieve → generate → judge — DeepSeek v4 Pro as both generator and judge, 500 questions including 30 abstention, topk_context=5):

condition   blended(500)  non-abstention(470)  abstention(30)
hybrid      96.2%         96.8%                86.7%
fts-only    83.4%         83.6%                80.0%

Per-category, hybrid vs FTS-only (the delta shows where vector search earns its keep):

Question typeHybridFTS-onlyDelta
single-session-user (64)100.0%98.4%+1.6pp
single-session-assistant (56)98.2%67.9%+30.3pp
single-session-preference (30)96.7%80.0%+16.7pp
multi-session (121)92.6%72.7%+19.9pp
temporal-reasoning (127)97.6%88.2%+9.4pp
knowledge-update (72)98.6%94.4%+4.2pp

The biggest lifts land on vocabulary-mismatch classes — single-session-assistant (+30pp) and multi-session (+20pp) — exactly where embeddings fix what FTS misses. Not leaderboard-comparable (DeepSeek v4 Pro, not GPT-4o), but the retrieval → answer pipeline is identical to the official harness.

Competitor comparison (500-question blended, all systems):

SystemScoreGeneratorSource
Ghost (hybrid)96.2%DeepSeek V4 ProThis repo
Mem094.4%Not specifiedmem0.ai/research — "managed platform, proprietary optimizations not in OSS SDK"
Hindsight91.4%Gemini-3 Proarxiv 2512.12818 — independently validated by Virginia Tech + Washington Post
Supermemory85.2%Gemini-3supermemory.ai/research — self-reported

Read carefully: These numbers are not directly comparable across rows — each uses a different generator and judge. Within the same generator+judge pair, differences are meaningful; across pairs, they're directional only.

Reproduce: see bench/longmemeval/phase4/. Full methodology, the ghost bench parameter sweep, and the staleness-suite deep dive: docs/benchmarks.md.

Skipped deliberately: LOCOMO (publicly audited answer-key and judge problems) and DMR.

Works well with Superpowers

Superpowers structures how agent work gets done (brainstorm-first planning, TDD, subagent execution); Ghost remembers what was learned. A workflow pattern that works well: load ghost_project_context before planning, ghost_memory_search before touching a component, ghost_decision_record when an architectural choice is made, ghost_memory_save when a phase completes.

Project status

Ghost is a solo project, built because I wanted my own agents to stop forgetting, and used daily on real infrastructure work. What you can verify rather than trust:

  • Pure Go, CGO_ENABLED=0, 8 direct dependencies (SQLite via modernc.org/sqlite — no C toolchain anywhere); a static binary around 12.5 MB
  • ~1:1 test-to-code ratio; CI runs go vet, golangci-lint, and race-enabled tests on every PR and push to main
  • Releases for 6 OS/arch targets built by GoReleaser with checksums, plus a multi-arch Docker image

Small enough to read the whole thing in an afternoon. That's on purpose — and because the exit story is one SQLite file, the cost of trying Ghost and walking away is a go install and an rm.

Contributing

Issues and PRs welcome. go test ./... and go vet ./... must pass; feature branches only.

License

Apache License 2.0 — see LICENSE.

Review pipeline

PRs are reviewed automatically on every push. The pipeline uses PR-Agent self-hosted on GitHub Actions, powered by Big Pickle via the opencode zen endpoint (https://opencode.ai/zen/v1), with DeepSeek V4 Flash as a fallback.

What it does:

  • /review posts a persistent review comment with score, effort estimate, and up to 5 findings (inline on diff lines)
  • /improve posts committable code suggestions as GitHub suggestion blocks (top 4 per run)
  • Reviews are anchored to the default branch via apply_repo_settings (fetches .pr_agent.toml and context files)

What it doesn't do:

  • Ticket compliance analysis is disabled (require_ticket_analysis_review = false) — the native grading mislabeled clean PRs and the merge gate is conversation resolution
  • The intro line ("Here are some key observations...") is disabled (enable_intro_text = false)
  • Auto-describe is disabled — findings live in the review, not in the PR description

Extra instructions enforce 3 lenses beyond diff-vs-issue matching: invariant parity (cross-checking guard clauses against sibling mutators), protected resources (_global project, DB rows, subprocess env), and behavior preservation at modified call sites.

Review identity: Reviews post as the Review Loop GitHub App (review-sweeper) when the app token is available; falls back to github-actions when secrets are absent.

Concurrency: one agent run per PR per event type. Bot comments fire issue_comment runs; a shared group with event-type splitting prevents the bot from cancelling its own in-flight review.