Cursor
Cursor gets Loom in two independent pieces:
| Piece | What it gives | How |
|---|---|---|
| MCP tools | the loom_* tools in Cursor's agent |
the install link for a local single-user server, else loom install --client cursor (MCP clients) |
| Transcript capture | each conversation's turns in conversation.append |
the sessionEnd hook integrations/cursor/cursor-session-hook.sh |
There is no Cursor plugin, so no code shunt and no capture of tool calls, runs or working memory; the Claude Code and Codex plugins have those.
MCP tools
A single-user server on the same machine takes MCP calls without a token.
Add Loom to Cursor
adds this entry under mcpServers.loom, on the coding toolset the dashboard gives
Cursor. The link's config is the entry, base64-encoded:
{ "url": "http://localhost:7777/mcp?toolset=coding" }
The entry points at the default port 7777; on another port, edit url in
~/.cursor/mcp.json. For any other server, write the entry with its token:
loom install --client cursor --server-url https://loom.example.com --token "$LOOM_API_KEY"
This merges a mcpServers.loom entry (url plus an Authorization header) into
~/.cursor/mcp.json. Restart Cursor afterwards. Toolsets, the agent label and the
per-client file table are in MCP clients.
Then add .cursor/rules/loom.mdc from agent instructions, so
the agent knows when to recall, save and use the code index, unless the repository's
AGENTS.md already carries the block.
Transcript capture
A hook you register yourself. cursor-session-hook.sh hands a finished conversation's
transcript to loom ingest-transcript, which posts each turn to conversation.append.
Turns are stored only when the namespace has conversation logging on.
Install
- Install the
loomCLI from a Loom checkout (pip install -e .) or from git (pip install "git+https://github.com/ClickHouse/loom"), withpython3onPATH. - Copy the hook to
~/.cursor/hooks/cursor-session-hook.shand make it executable. -
Register it in
~/.cursor/hooks.json. Paths in the user file are relative to~/.cursor/; a project's.cursor/hooks.jsonresolves them from the project root.{ "version": 1, "hooks": { "sessionEnd": [{ "command": "./hooks/cursor-session-hook.sh", "timeout": 150 }] } } -
Give the hook its environment. An app started from the dock does not see your shell profile, so keep the token in
~/.loom/token(loom mint-token --savewrites it) and set the rest in a wrapper thathooks.jsonpoints at:#!/usr/bin/env bash export LOOM_BASE_URL=https://loom.example.com LOOM_NAMESPACE=my-namespace exec "$HOME/.cursor/hooks/cursor-session-hook.sh" "$@" -
Turn on conversation logging for the namespace (the Record conversations switch on the dashboard's Conversations page, or
conversation.set_logging). Until then the server accepts the turns and stores none.
| Variable | Default | Meaning |
|---|---|---|
LOOM_NAMESPACE |
required | namespace the thread is written to |
LOOM_API_KEY |
~/.loom/token |
the token; the CLI reads it from the environment, so it never appears on the command line |
LOOM_BASE_URL |
http://localhost:7777 |
Loom server |
LOOM_AGENT |
cursor |
agent on the conversation rows |
LOOM_BIN |
loom |
the CLI to run |
LOOM_CAPTURE_CONVERSATIONS |
true |
false turns the hook into a no-op |
LOOM_INGEST_MAX_BYTES |
16777216 |
a larger transcript is skipped, not truncated |
LOOM_INGEST_TIMEOUT |
120 |
seconds the CLI may run (needs timeout or gtimeout); keep it under the hooks.json timeout |
What the hook reads
Cursor starts a hook as a process and writes one JSON object to its stdin. The hook reads two fields and ignores the rest:
| Field | Use |
|---|---|
transcript_path |
the transcript file; null when transcripts are off, and then nothing is sent |
conversation_id |
thread id cursor-<conversation_id> (only [A-Za-z0-9._:-], at most 128 chars) |
CURSOR_TRANSCRIPT_PATH in the environment is the fallback for the path. Without a
usable conversation_id, the thread id is cursor- plus a hash of the transcript path.
Register sessionEnd; stop also works, and re-posts the whole transcript after every
agent turn.
The hook reads the JSONL Cursor writes under
~/.cursor/projects/<project>/agent-transcripts/: one object per line with role and
message.content (a string or a list of blocks). A .json array of {role, content} or
a .md file with # user / # assistant sections is passed through unchanged. Any other
file is skipped with a message on stderr.
Capture boundary
- Sent: the text of
userandassistantlines, in file order. Text blocks only:tool_useblocks, tool results, thinking and every other role are dropped on the machine. - Not sent: the hook input's
user_email,workspace_rootsor model fields. The writer is whoever the token names; the rows carryagentand nothing from the Cursor account. - Not redacted: the turn text goes as written. Keep secrets out of chats you capture, or leave the namespace's logging off.
Behaviour
- Never blocks Cursor. In hook mode every outcome exits 0 and prints
{}on stdout; the CLI's output goes to stderr. - Safe when Loom is unreachable. The hook logs one line and exits 0, with no retry.
The next run resends the transcript, and
conversation.appendskips turns that already landed (turn ids). - Bounded. The hook input is read up to 1 MiB, the transcript up to
LOOM_INGEST_MAX_BYTES, the CLI forLOOM_INGEST_TIMEOUTseconds. - Quiet. Diagnostics are one line on stderr prefixed
loom-cursor-hook:and name no transcript text and no token. - Manual mode.
cursor-session-hook.sh <transcript>ingests one file and exits with the CLI's code (2configuration,3the transcript was skipped, otherwise the CLI's).
Limits
- Stored turns are a record, not an extraction source: nothing is mined from them. To
write memories from a conversation, post its turns to
memory.set_from_messages. - The transcript format is Cursor's, undocumented by Cursor; a format change shows up as "no user or assistant text in the transcript" on stderr.
Related
- Integrations: the other transcript templates.
- MCP clients.
- Conversations and sessions.
- Source README.