185 lines
6.6 KiB
Markdown
185 lines
6.6 KiB
Markdown
# 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`](https://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)".
|
|
|
|
```json
|
|
{
|
|
"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_id` is the stable per-conversation ID. The Cursor
|
|
`stop` event does **not** carry a `session_id` — only
|
|
`conversation_id`. MemPalace keys its counter files on this. Cursor
|
|
`sessionStart` does carry a `session_id`, and the docs note it is
|
|
"same as `conversation_id`".
|
|
- `generation_id` changes every user message. We do not use it.
|
|
- `transcript_path` may be `null` if the user has disabled
|
|
transcripts in Cursor settings. The hooks degrade gracefully when
|
|
the value is empty.
|
|
- `workspace_roots` is 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`)
|
|
|
|
```json
|
|
{
|
|
"status": "completed" | "aborted" | "error",
|
|
"loop_count": 0
|
|
}
|
|
```
|
|
|
|
- `loop_count` indicates how many times this stop hook has already
|
|
triggered an automatic followup for this conversation (starts at
|
|
0). When `loop_count > 0` we know our own previous `followup_message`
|
|
is currently being processed — the save hook returns `{}` so the
|
|
agent can finish. Equivalent to Claude Code's `stop_hook_active`.
|
|
- The per-script `loop_limit` (default 5 for Cursor hooks, configurable
|
|
via the `loop_limit` field on the hook entry in `hooks.json`) is
|
|
defense-in-depth on top of our own check. The example `hooks.json`
|
|
in `examples/cursor/` sets `loop_limit: 1`.
|
|
|
|
**Allowed output fields** (only):
|
|
|
|
```json
|
|
{ "followup_message": "<text to auto-submit as next user turn>" }
|
|
```
|
|
|
|
### `preCompact` (consumed by `mempal_precompact_hook_cursor.sh`)
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{ "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:
|
|
|
|
1. Running `mempalace mine` synchronously inside the hook so the
|
|
verbatim transcript lands in the palace before compaction
|
|
summarises it.
|
|
2. Dropping a `cursor_<conversation_id>.pending` marker that the next
|
|
`stop` invocation reads and uses to force a save followup
|
|
regardless of its counter.
|
|
|
|
### `sessionStart` (consumed by `mempal_wake_hook_cursor.sh`)
|
|
|
|
```json
|
|
{
|
|
"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:**
|
|
|
|
```json
|
|
{
|
|
"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 to `permission: "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.
|