Cursor IDE Hooks
Three hooks for the Cursor IDE that save memories automatically and inject recall context at session start. No manual "save" commands needed.
These are additive to the existing Claude Code + Codex hooks. You can run both — they share the same ~/.mempalace/hook_state/ directory and the same kill switches.
Pair this with the Cursor plugin
The hooks here only handle the auto-save side. To also get MemPalace's MCP server, slash commands (/mempalace-search, etc.), and the guided mempalace skill, install the bundled Cursor plugin — it's the .cursor-plugin/ folder at the repo root, dropped into ~/.cursor/plugins/local/mempalace. The plugin and the hooks are orthogonal: install whichever you want, in any order. The plugin deliberately does not wire hooks itself because Cursor's hooks system is configured per-user/per-project (in ~/.cursor/hooks.json), not per-plugin.
Three layers of recall
The sessionStart wake hook is one of three orthogonal ways MemPalace gets the agent to read the palace before answering. Install any combination — they reinforce each other and all reference the same canonical protocol in integrations/shared/recall-protocol.md.
| Layer | Fires | Scope | Get it from |
|---|---|---|---|
sessionStart hook | Once per new conversation | Injects wing-scoped recall context up front | The hooks on this page |
mempalace-recall skill | When a request matches its description, or when attached | Full search-before-answer protocol | The Cursor plugin (skills/) |
| Recall rule | When Cursor's matcher judges the turn recall-relevant | A short nudge to search first | The plugin (rules/mempalace-recall.mdc, alwaysApply: false) or examples/cursor/rules/ |
The hook is the only layer that fires automatically and exactly once per chat. The skill and rule are demand-driven: they kick in when the user actually asks about past work, people, or prior decisions, and stay out of the way on greenfield coding. For recall forced into every conversation, copy the alwaysApply: true variant from examples/cursor/rules/ into ~/.cursor/rules/ — a heavier, deliberate opt-in.
What They Do
| Hook | When It Fires | What Happens |
|---|---|---|
| Wake Hook | sessionStart — when a new Cursor conversation opens | Returns additional_context telling the agent to recall scoped to the wing inferred from the workspace root. Cursor-only — Claude Code has no equivalent. |
| Save Hook | stop — after every agent turn | Counts stop invocations per conversation. Every 15 (default), emits a followup_message telling the agent to file the session into MemPalace and write a diary entry. |
| PreCompact Hook | preCompact — right before context compaction | Runs mempalace mine synchronously on the transcript before compaction summarises it. Drops a pending-save marker so the next stop forces a save followup. |
Two-layer capture: the save and precompact hooks both mine the JSONL transcript directly into the palace (capturing verbatim tool output — Shell results, search findings, build errors). The save hook also nudges the AI to write structured drawers and a diary entry. Belt-and-suspenders.
Install — Cursor
The fastest path is the installer that ships in the repo.
Preview the change first (writes nothing, just prints the would-be JSON):
hooks/cursor/install.sh --scope user --dry-runUser scope — applies globally, writes ~/.cursor/hooks.json:
hooks/cursor/install.sh --scope userOr project scope — only this repo, writes <repo>/.cursor/hooks.json:
hooks/cursor/install.sh --scope project --target /path/to/your/repoThe installer copies the three hook scripts to ~/.mempalace/hooks/cursor/, merges the entries into your hooks.json, and preserves any unrelated hooks already in that file. Re-running is idempotent. Pass --variant minimal for the stop-only setup, or --uninstall to remove the MemPalace entries (leaves other hooks intact).
Manual install — ~/.cursor/hooks.json (user scope)
{
"version": 1,
"hooks": {
"sessionStart": [
{ "command": "/absolute/path/to/hooks/cursor/mempal_wake_hook_cursor.sh" }
],
"stop": [
{
"command": "/absolute/path/to/hooks/cursor/mempal_save_hook_cursor.sh",
"loop_limit": 1
}
],
"preCompact": [
{ "command": "/absolute/path/to/hooks/cursor/mempal_precompact_hook_cursor.sh" }
]
}
}Manual install — .cursor/hooks.json (project scope)
Identical content. Project hooks load in any trusted workspace and are checked into version control with the project. Cloud agents also load project hooks.
Make the scripts executable once:
chmod +x hooks/cursor/mempal_save_hook_cursor.sh \
hooks/cursor/mempal_precompact_hook_cursor.sh \
hooks/cursor/mempal_wake_hook_cursor.shCursor watches hooks.json and reloads automatically after a save. If hooks still do not fire, restart Cursor and check the Hooks panel in Settings → Hooks.
Configuration
All knobs are environment variables. Defaults match the Claude Code hooks where they overlap.
MEMPAL_SAVE_INTERVAL=15— number ofstopevents between save followups. Lower = more frequent saves, higher = less interruption.MEMPAL_CURSOR_SILENT=1— suppress thefollowup_messageentirely (the hook still runs its best-effort background mine and keeps its counters).MEMPAL_VERBOSE=false/0/nois equivalent. Note the followup is on by default for Cursor — see "Why the followup is on by default" below.MEMPAL_STATE_DIR— where the hook keeps counter files, the pending-save marker, andcursor_hook.log. Defaults to~/.mempalace/hook_state/.MEMPAL_STATE_TTL_DAYS=30— age after which stalecursor_*.count/cursor_*.pendingfiles are swept. The hooks run a daily-throttled garbage collection so per-conversation state can't grow unbounded; only Cursor's own state is touched.MEMPAL_DIR— optional project directory (code, notes, docs) to also mine on each save trigger, with--mode projects. The transcript is always mined regardless —MEMPAL_DIRis purely additive.MEMPAL_PYTHON— path to a Python 3 interpreter. The hook's own JSON parsing and the install script's JSON merge use this. Resolution order:$MEMPAL_PYTHON→command -v python3→ barepython3. Set this when Cursor is launched from a GUI on macOS and the inherited PATH lacks the Python where you installed MemPalace.MEMPAL_DISABLE_HOOK=1— emergency kill switch. Disables all three hooks; they emit{}and exit 0.MEMPALACE_HOOKS_AUTO_SAVE=false— same effect asMEMPAL_DISABLE_HOOK=1. Also honoured via~/.mempalace/config.json:json{ "hooks": { "auto_save": false } }
How It Works
Wake Hook (sessionStart)
Cursor opens new conversation → sessionStart fires
↓
Hook reads workspace_roots[0]
↓
Infers wing = basename(workspace_root)
↓
{"additional_context": "scope recall to wing=<...>"}
↓
Agent reads additional_context before first turn
↓
Agent calls mempalace_search + mempalace_diary_read
wing-scoped on the first relevant questionCursor's sessionStart is fire-and-forget — the agent loop does not wait for a blocking response and does not consume continue / user_message. But it does honour additional_context, and that is the only field MemPalace emits.
Save Hook (stop event)
User sends message → agent responds → Cursor fires stop hook
↓
Hook reads loop_count from stdin
↓
┌─── loop_count > 0 (our own followup running) ──→ echo "{}"
│
└─── loop_count == 0
↓
Check pending-save marker from preCompact
↓
┌── marker present ──→ delete + emit followup_message
│
└── no marker
↓
Atomic counter++ for this conversation_id
↓
┌── counter % SAVE_INTERVAL != 0 ──→ echo "{}"
│
└── counter % SAVE_INTERVAL == 0
↓
Background: mempalace mine <transcript_dir>
↓
Emit {"followup_message": "save key topics..."}
↓
Cursor auto-submits followup as next user turn
↓
Agent files drawers + writes diary
↓
Agent stops; stop fires again with loop_count = 1
↓
Hook sees loop_count > 0 → echo "{}" → agent stopsThe loop_count > 0 short-circuit prevents infinite loops: emit once → agent saves → stops → we see loop_count = 1 → we let it through. This is the Cursor equivalent of Claude Code's stop_hook_active flag. The loop_limit: 1 in hooks.json is defense-in-depth on top.
PreCompact Hook
Context window near full → Cursor fires preCompact (observational)
↓
Synchronously: mempalace mine <transcript_dir>
↓
Drop pending-save marker for this conversation_id
↓
{"user_message": "transcript snapshotted..."}
↓
Compaction proceeds (we cannot block it)
↓
Next stop event picks up the marker → forces saveCursor's preCompact is documented as observational only — its only output field is user_message, with no followup_message and no way to block. That is fundamentally different from Claude Code's PreCompact which can block until the AI has saved. We work around the limitation by mining the verbatim transcript synchronously (zero LLM cost) and queueing a save nudge for the next agent turn.
Why synchronous (and what happens on a slow mine)
The pre-compaction mine runs synchronously on purpose: compaction is irreversible, so we must finish ingesting before the hook returns — background mining would race the compaction. On a very large transcript this can exceed Cursor's per-hook timeout, in which case Cursor kills the mine mid-run. That is safe: mempalace mine is incremental and append-only, so a killed mine resumes cleanly on the next invocation rather than corrupting the palace, and the pending-save marker still forces a re-mine plus a verbatim save nudge on the next stop.
Cursor-only extras
The features below are not available in the Claude Code or Codex hooks because their hook surfaces do not expose the necessary events.
- Session-start recall via
sessionStart. The wake hook injects wing-scoped recall guidance into the conversation's initial system context, so the agent searches the palace before answering anything that touches past work. Verified output field — see the Cursor hooks reference section "sessionStart". - Per-script
loop_limit. Cursor'sloop_limit(default 5, configurable per script) is a hard cap on how many auto-followups Cursor will issue. MemPalace sets it to1in the examplehooks.jsonas defense-in-depth on top of its ownloop_countcheck. - Inferred wing from
workspace_roots. Both the wake hook and the save hook usebasename(workspace_roots[0])to scope memory operations. A user with multiple Cursor workspaces gets per-project wings without any manual configuration.
Debugging
cat ~/.mempalace/hook_state/cursor_hook.logExample output (ISO-8601 timestamps, event + conversation id, message):
[2026-05-27T02:16:01Z] [event=sessionStart] [conv=abc123] workspace=/Users/me/proj wing=proj
[2026-05-27T02:21:33Z] [event=stop] [conv=abc123] counter 0 -> 1 (interval=15)
[2026-05-27T02:42:09Z] [event=stop] [conv=abc123] counter 14 -> 15 (interval=15)
[2026-05-27T02:42:09Z] [event=stop] [conv=abc123] TRIGGERING SAVE at counter=15
[2026-05-27T02:42:11Z] [event=stop] [conv=abc123] loop_count>0; letting agent stop
[2026-05-27T03:05:44Z] [event=preCompact] [conv=abc123] trigger=auto transcript=/Users/me/.cursor/.../transcript.txt
[2026-05-27T03:05:46Z] [event=stop] [conv=abc123] consumed pending-save marker (post-compaction)When a hook can't parse its stdin (corrupt payload, future Cursor schema change), the raw input — capped at 4096 bytes, mode 0600 — lands at:
~/.mempalace/hook_state/cursor_last_input.log
~/.mempalace/hook_state/cursor_last_python_err.logBoth are overwritten on each failure, never appended, so a repeating misconfiguration cannot grow disk usage.
Cost
Zero extra tokens spent by the hooks themselves. The hooks are bash scripts that run locally. They do not call any API. The followup_message the save hook emits is a normal user turn — it counts the same as any other user message and does not invoke any extra LLM call beyond the one the user would otherwise make. To suppress it entirely, set MEMPAL_CURSOR_SILENT=1.
Why the followup is on by default
The Claude Code hook is silent by default: its background mempalace mine --mode convos captures the verbatim transcript on its own (because normalize.py has a Claude Code JSONL parser), and the LLM-driven diary nudge is opt-in behind MEMPAL_VERBOSE.
Cursor is different. Cursor's transcript format is undocumented and normalize.py has no Cursor parser, so the background mine is best-effort only and does not yet yield clean verbatim drawers. That makes the followup_message — which drives the agent to file its own in-context verbatim quotes via mempalace_add_drawer / mempalace_diary_write — the load-bearing verbatim-capture path for Cursor. Turning it off by default would leave a default install capturing nothing, so it is on by default.
If you want the Claude-style "zero tokens in the chat window" behaviour and accept the reduced capture, set MEMPAL_CURSOR_SILENT=1 (or MEMPAL_VERBOSE=false). The proper long-term fix is a Cursor transcript parser in normalize.py (tracked follow-up); once that works, this default flips to silent to match Claude.
Known limitations
- Hooks load at session start. Cursor watches
hooks.jsonand reloads the wiring when the file changes, but for the freshly-loaded hook scripts to take effect on an existing conversation you usually have to start a new conversation. This matches the behaviour of Claude Code's hook lifecycle. preCompactcannot block. See the diagram above. The pending-save marker is the workaround.- Transcript file format is opaque. Cursor does not document the schema of the file at
transcript_path, andmempalace/normalize.pyhas no Cursor parser yet, so the backgroundmempalace mine --mode convosis best-effort for Cursor — it does not yet produce clean verbatim conversation drawers. Thefollowup_messageis the load-bearing capture path (see below). Adding a Cursor parser tonormalize.pyis tracked follow-up work; once it lands, the followup can default to silent like the Claude hook.
Related
- Auto-Save Hooks (Claude Code + Codex) — the analogous feature for those tools.
hooks/cursor/STDIN_SHAPE.md— per-event JSON schema with citations.- Claude Code Retention — broader setup checklist if you mix Cursor with Claude Code.
