Files
wehub-resource-sync c3749daf48
Tests / test-linux (3.13) (push) Failing after 0s
Tests / test-linux (3.11) (push) Failing after 1s
Tests / lint (push) Failing after 0s
Tests / test-linux (3.9) (push) Failing after 1s
Docker / build (push) Failing after 1s
Docker / build-gpu (push) Failing after 2s
Tests / test-windows (push) Has been cancelled
Tests / test-macos (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:03:03 +08:00

368 lines
17 KiB
Markdown

# Cursor IDE Hooks
Three hooks for the [Cursor](https://cursor.com) 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](/guide/hooks).
You can run both — they share the same `~/.mempalace/hook_state/`
directory and the same kill switches.
::: tip 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](https://github.com/MemPalace/mempalace/blob/main/.cursor-plugin/README.md) —
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`](https://github.com/MemPalace/mempalace/blob/develop/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](https://github.com/MemPalace/mempalace/blob/main/.cursor-plugin/README.md) (`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/`](https://github.com/MemPalace/mempalace/blob/develop/examples/cursor/rules/README.md) |
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):
```bash
hooks/cursor/install.sh --scope user --dry-run
```
User scope — applies globally, writes `~/.cursor/hooks.json`:
```bash
hooks/cursor/install.sh --scope user
```
Or project scope — only this repo, writes `<repo>/.cursor/hooks.json`:
```bash
hooks/cursor/install.sh --scope project --target /path/to/your/repo
```
The 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)
```json
{
"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:
```bash
chmod +x hooks/cursor/mempal_save_hook_cursor.sh \
hooks/cursor/mempal_precompact_hook_cursor.sh \
hooks/cursor/mempal_wake_hook_cursor.sh
```
Cursor 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 of `stop` events between save
followups. Lower = more frequent saves, higher = less interruption.
- **`MEMPAL_CURSOR_SILENT=1`** — suppress the `followup_message` entirely
(the hook still runs its best-effort background mine and keeps its
counters). `MEMPAL_VERBOSE=false`/`0`/`no` is 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, and `cursor_hook.log`. Defaults to
`~/.mempalace/hook_state/`.
- **`MEMPAL_STATE_TTL_DAYS=30`** — age after which stale
`cursor_*.count` / `cursor_*.pending` files 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_DIR` is 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` → bare `python3`. 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 as
`MEMPAL_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 question
```
Cursor'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 stops
```
The `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 save
```
Cursor'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.
::: tip 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](https://cursor.com/docs/hooks.md) section "sessionStart".
- **Per-script `loop_limit`.** Cursor's `loop_limit` (default 5,
configurable per script) is a hard cap on how many auto-followups
Cursor will issue. MemPalace sets it to `1` in the example
`hooks.json` as defense-in-depth on top of its own `loop_count`
check.
- **Inferred wing from `workspace_roots`.** Both the wake hook and the
save hook use `basename(workspace_roots[0])` to scope memory
operations. A user with multiple Cursor workspaces gets per-project
wings without any manual configuration.
## Debugging
```bash
cat ~/.mempalace/hook_state/cursor_hook.log
```
Example 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.log
```
Both 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.json` and 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.
- **`preCompact` cannot 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`, and `mempalace/normalize.py`
has no Cursor parser yet, so the background `mempalace mine --mode
convos` is **best-effort** for Cursor — it does not yet produce clean
verbatim conversation drawers. The `followup_message` is the
load-bearing capture path (see below). Adding a Cursor parser to
`normalize.py` is tracked follow-up work; once it lands, the followup
can default to silent like the Claude hook.
## Related
- [Auto-Save Hooks (Claude Code + Codex)](/guide/hooks) — the analogous
feature for those tools.
- [`hooks/cursor/STDIN_SHAPE.md`](https://github.com/MemPalace/mempalace/blob/develop/hooks/cursor/STDIN_SHAPE.md)
— per-event JSON schema with citations.
- [Claude Code Retention](/guide/claude-code-retention) — broader
setup checklist if you mix Cursor with Claude Code.