Adds first-class Cursor IDE integration alongside the existing Claude
Code and Codex hook flows, so Cursor users get the same automatic
diary saves, pre-compaction transcript capture, and session-start
memory recall — without changing any default behaviour for existing
users.
What's included
---------------
Cursor hook scripts (hooks/cursor/):
- mempal_save_hook_cursor.sh — Stop event, counter +
loop_count guard, pending-save marker consumption, background
mempalace mine, followup_message emission.
- mempal_precompact_hook_cursor.sh — synchronous mine before
compaction, drops a pending_save marker, returns user_message.
- mempal_wake_hook_cursor.sh — sessionStart event,
wing-scoped recall guidance via additional_context.
- lib/common.sh — shared parsing + state helpers
(bash 3.2 safe, no heredoc-in-subshell traps).
- install.sh — idempotent installer with
--scope, --variant, --dry-run, --uninstall. Recognises existing
entries by basename so re-installs across paths work.
- STDIN_SHAPE.md, README.md — payload schemas + quick
reference.
Cursor plugin (.cursor-plugin/ + repo-root components):
- plugin.json, marketplace.json, README.md.
- skills/mempalace/SKILL.md — model-invocable skill mirroring the
Claude plugin's skill surface.
- commands/mempalace-{help,init,mine,search,status}.md — slash
commands for marketplace-published installs (filename = slug).
- mcp.json — auto-registers the mempalace MCP
server, wrapped under the documented mcpServers key.
Examples + docs:
- examples/cursor/hooks.json, hooks.minimal.json + README.
- website/guide/cursor-hooks.md + sidebar entry.
- README.md and CHANGELOG.md updates.
Tests (129 new, all green):
- tests/test_cursor_hooks_shell.py — 75 behavioural tests for
the three hook scripts: kill switches, input parsing, counter
logic, loop prevention, pending markers, wing inference, logging.
- tests/test_cursor_hooks_install.py — 19 contract tests for the
installer: dry-run, idempotent merge, basename-matched uninstall,
refusal to overwrite malformed JSON.
- tests/test_cursor_plugin_manifest.py — 35 contract tests for the
plugin: manifest validity, version sync with mempalace.version,
mcp.json shape, skill/command frontmatter, default-discovery
layout invariants.
Design notes
------------
- Local-first and zero-API by default; hooks never call external
services. Same privacy model as the existing Claude Code hooks.
- Fail-open: hook scripts deliberately do not use set -e so a broken
hook can never block the user's conversation.
- Cursor preCompact cannot block + return a followup, so we
synchronously mine the transcript and drop a pending_save marker
that the next stop hook consumes — guarantees verbatim capture
before context window compression.
- Cursor's default plugin discovery requires real commands/, skills/,
and mcp.json at the plugin root (verified against the cached
cloudflare plugin); .cursor-plugin/{commands,skills} are convenience
symlinks back to those canonical locations.
- bash 3.2 compatibility throughout: avoids heredoc-in-command-
substitution parser bugs; uses python -c for JSON parsing;
basename-matched entry recognition in install.sh.
- All changes are additive. No existing files are removed, no
existing hooks change behaviour, and no new runtime dependencies
are introduced.
Co-authored-by: Cursor <cursoragent@cursor.com>
6.6 KiB
Cursor Hook Stdin Shape — Reference
This file documents the JSON payloads the Cursor IDE sends to the
MemPalace hook scripts in hooks/cursor/. It exists so a future
contributor does not have to re-discover the schema by writing a
probe hook.
Source: cursor.com/docs/hooks.md,
fetched 2026-05-27. Cursor's hook system is documented as a stable
v1 schema ({"version": 1, ...} at the top of hooks.json).
If you suspect Cursor has changed the payload shape since that fetch
date, re-verify against the upstream docs and update both this file
and hooks/cursor/lib/common.sh::mempal_parse_stdin. The hook
scripts deliberately ignore fields they do not consume, so adding
new fields is non-breaking.
Common fields (all events)
Every hook receives these on stdin in addition to its event-specific fields. Source: docs section "Common schema → Input (all hooks)".
{
"conversation_id": "string",
"generation_id": "string",
"model": "string",
"hook_event_name": "string",
"cursor_version": "string",
"workspace_roots": ["<absolute path>"],
"user_email": "string | null",
"transcript_path": "string | null"
}
Field notes (verified):
conversation_idis the stable per-conversation ID. The Cursorstopevent does not carry asession_id— onlyconversation_id. MemPalace keys its counter files on this. CursorsessionStartdoes carry asession_id, and the docs note it is "same asconversation_id".generation_idchanges every user message. We do not use it.transcript_pathmay benullif the user has disabled transcripts in Cursor settings. The hooks degrade gracefully when the value is empty.workspace_rootsis normally a single-entry array but multi-root workspaces are supported; MemPalace uses index[0].
Event-specific fields
stop (consumed by mempal_save_hook_cursor.sh)
{
"status": "completed" | "aborted" | "error",
"loop_count": 0
}
loop_countindicates how many times this stop hook has already triggered an automatic followup for this conversation (starts at 0). Whenloop_count > 0we know our own previousfollowup_messageis currently being processed — the save hook returns{}so the agent can finish. Equivalent to Claude Code'sstop_hook_active.- The per-script
loop_limit(default 5 for Cursor hooks, configurable via theloop_limitfield on the hook entry inhooks.json) is defense-in-depth on top of our own check. The examplehooks.jsoninexamples/cursor/setsloop_limit: 1.
Allowed output fields (only):
{ "followup_message": "<text to auto-submit as next user turn>" }
preCompact (consumed by mempal_precompact_hook_cursor.sh)
{
"trigger": "auto" | "manual",
"context_usage_percent": 85,
"context_tokens": 120000,
"context_window_size": 128000,
"message_count": 45,
"messages_to_compact": 30,
"is_first_compaction": true
}
Critical constraint: preCompact is documented as observational only. It cannot block compaction and its allowed output fields are limited to:
{ "user_message": "<short message shown to the user when compaction occurs>" }
There is no followup_message and no decision: block on
this event — unlike Claude Code's PreCompact. MemPalace works
around this by:
- Running
mempalace minesynchronously inside the hook so the verbatim transcript lands in the palace before compaction summarises it. - Dropping a
cursor_<conversation_id>.pendingmarker that the nextstopinvocation reads and uses to force a save followup regardless of its counter.
sessionStart (consumed by mempal_wake_hook_cursor.sh)
{
"session_id": "<unique session identifier>",
"is_background_agent": true,
"composer_mode": "agent" | "ask" | "edit"
}
session_id equals conversation_id on this event (docs are
explicit about this).
Allowed output fields:
{
"env": { "<key>": "<value>" },
"additional_context": "<text added to conversation's initial system context>"
}
additional_context is the field MemPalace uses. The schema also
accepts continue and user_message but the docs explicitly note
"current callers do not enforce them; session creation is not
blocked even when continue is false". We do not emit either.
Environment variables (all hooks)
Cursor sets these env vars on every hook execution; the hook scripts fall back to them when JSON parsing fails for any reason.
| Variable | Description |
|---|---|
CURSOR_PROJECT_DIR |
Workspace root (= workspace_roots[0]) |
CURSOR_VERSION |
Cursor version string |
CURSOR_USER_EMAIL |
Authenticated user email (if logged in) |
CURSOR_TRANSCRIPT_PATH |
Conversation transcript path (if transcripts on) |
CURSOR_CODE_REMOTE |
"true" if running in a remote workspace |
CLAUDE_PROJECT_DIR |
Alias for CURSOR_PROJECT_DIR (Claude compat) |
Exit code semantics
Cursor interprets command-hook exit codes as follows (docs "Hook Types → Command-Based Hooks → Exit code behavior"):
0— success, use the JSON output.2— block the action (equivalent topermission: "deny").- Other — hook failed; action proceeds (fail-open by default).
MemPalace hooks always exit 0 and emit either {} (no-op) or a
valid JSON response. We never use exit code 2; nothing MemPalace
does should ever block an agent action.
Working directory contract
- User hooks (
~/.cursor/hooks.json) run from~/.cursor/. - Project hooks (
.cursor/hooks.json) run from the project root.
The MemPalace hooks always resolve their sibling lib/common.sh via
BASH_SOURCE[0] so the working directory does not matter for the
script's own loading — only the command path in hooks.json needs
to point at the absolute location of the script.
Transcript file format (out of scope)
The format of the file at transcript_path is not documented by
Cursor as of the fetch date above. MemPalace deliberately does not
parse it: the save hook counts stop invocations (each one
corresponds to one assistant turn) and hands the transcript to
mempalace mine, which has its own normaliser layer.
If you need to consume the transcript directly, probe its shape with
a throw-away hook that does cat > /tmp/cursor-transcript-sample.txt
and inspect the output — there is no shortcut.