-
Notifications
You must be signed in to change notification settings - Fork 1
Setup And Hooks
How thatch setup wires each host, and which hook events each path relies on.
For feature parity and gaps, see mcp-parity.md. This document is the concrete
artifact reference.
No setup command — opencode loads the plugin from opencode.json:
The plugin supports both opencode lines: v1 (1.18.x) via the experimental.*
hook surface below, and v2 (2.x) via the promise-context domains
(session.hook("prompt"/"context"/"compaction"), tool.transform). Both
adapters delegate to the shared runtime (src/runtime.ts); the hooks below
describe the v1 shape the v2 mappings mirror. The v1 hooks, registered in
code (src/opencode/v1.ts):
| Hook | Input | Output | Behavior |
|---|---|---|---|
experimental.chat.system.transform |
{} |
{ system: string[] } |
Pushes the system prompt (store names, usage rules). |
experimental.session.compacting |
{ sessionID } |
{ context: string[] } |
Marks the session as compacting, pushes re-familiarization context. |
experimental.compaction.autocontinue |
{ sessionID } |
{ enabled: boolean } |
Clears the compacting flag so chat.message nudges resume post-compaction. |
tool.execute.after |
{ tool, sessionID, callID, args } |
{ title, output, metadata } |
Buffers non-thatch_, non-skill, non-task tool calls into the in-memory extraction ring buffer (max 20). In child sessions, also tracks new/updated/deleted counts via childMetrics for the extraction toast. |
chat.message |
{ sessionID, messageID } |
{ message, parts } |
Prompt-aware recall nudge (plus prediction/behavior auto-fire). No extraction nudge - extraction is plugin-driven at session idle. Suppressed while compacting or for task-dispatched sub-agent sessions (children that are not extraction children). |
event |
{ event: { type, properties } } |
— | Dispatches on event.type: session.created (top-level) → client.session.prompt with reminder + hygiene heartbeat; session.created (child with parentID) → records childToParent + snapshots parent's pending buffer. session.status idle (parent, pending interactions) → triggerExtraction (direct child-session extraction); idle (child) → complete/drain, showToast with metrics, delete child. session.error (child) → requeue parent's accepted entries. session.deleted → requeue if child, drop accepted if parent, clean maps. session.compacted → clear compacting flag. |
dispose |
— | — | Closes the DB. |
Skills install to $XDG_CONFIG_HOME/opencode/skills at plugin init — shared
and opencode-only (the coordinator needs sub-agents).
Direct extraction is the primary path for opencode: when a parent session goes
idle with pending tool interactions, the event hook creates a child session
and prompts it directly via triggerExtraction. The chat.message extraction
nudge is a fallback — it fires only if direct extraction was never triggered or
threw an error (the extracting set is cleared on failure).
client.tui.showToast is called from the event hook (on child idle, with
extraction metrics: new/updated/deleted counts) and from chat.message (on
recall, prediction, and behavior matches). Best-effort — silently ignored if
the TUI is not connected (headless mode).
thatch setup --claude (project-local) or thatch setup --claude --global.
| Artifact | Project-local | Global (--global) |
|---|---|---|
| MCP config |
.mcp.json (mcpServers.thatch, stdio, ["mcp"]) |
none — prints claude mcp add --scope user thatch -- <bin> mcp
|
| Instructions |
CLAUDE.md (idempotent appendBlock) |
$CLAUDE_CONFIG_DIR/CLAUDE.md |
| Hooks | .claude/settings.json |
$CLAUDE_CONFIG_DIR/settings.json |
| Skills |
.claude/skills/ (in the repo) |
$CLAUDE_CONFIG_DIR/skills/ |
| Commands |
.claude/commands/thatch/ (in the repo) |
$CLAUDE_CONFIG_DIR/commands/thatch/ |
CLAUDE_CONFIG_DIR overrides the default ~/.claude for all user-scoped
paths. Project-local keeps everything in the repo: .mcp.json, CLAUDE.md,
.claude/settings.json, .claude/skills/, and .claude/commands/thatch/
(so skills and commands version with the
project and every contributor gets them).
{
"hooks": {
"SessionStart": { "hooks": [{ "type": "command", "command": "<bin> reminder" }] },
"PostToolBatch": { "hooks": [{ "type": "command", "command": "<bin> buffer-batch" }] },
"UserPromptSubmit": { "hooks": [{ "type": "command", "command": "<bin> flush-tools" }] },
"Stop": { "hooks": [{ "type": "command", "command": "<bin> chat-notify" }] }
}
}| Event | Command | Output | Role |
|---|---|---|---|
SessionStart |
thatch reminder |
plain text to stdout (becomes context) | Recall instructions + hygiene heartbeat + chat identity/mail line (reads the hook's stdin session_id) |
PostToolBatch |
thatch buffer-batch |
silent (no stdout) | Appends a batch of tool calls to the file-backed JSONL queue |
UserPromptSubmit |
thatch flush-tools |
nudge text to stdout | Peeks queue (extraction nudge), else fires recall, prediction, and behavior nudges via sideband in parallel, else write nudge |
Stop |
thatch chat-notify |
{ hookSpecificOutput: { additionalContext } } when chat mail is unread, else {}
|
Post-turn chat wake: the turn continues so the model reads its mail (Claude Code's analog of opencode's poller wake); host loop protections (stop_hook_active, 8-consecutive cap) plus the delivered stamp bound repeats |
PostToolBatch is silent so the agent loop is not delayed; the buffered
content is invisible until UserPromptSubmit peeks it.
thatch setup --cursor (project-local) or thatch setup --cursor --global.
Cursor uses the same stdio MCP server as Claude Code; only the hooks differ.
| Artifact | Project-local | Global (--global) |
|---|---|---|
| MCP config | .cursor/mcp.json |
$CURSOR_CONFIG_DIR/mcp.json (or ~/.cursor/mcp.json) |
| Instructions |
AGENTS.md (idempotent appendBlock, Cursor-marked) |
$CURSOR_CONFIG_DIR/AGENTS.md |
| Hooks |
.cursor/hooks.json (flat) |
$CURSOR_CONFIG_DIR/hooks.json |
| Skills |
.cursor/skills/ (in the repo) |
$CURSOR_CONFIG_DIR/skills/ |
Cursor has no documented env override like CLAUDE_CONFIG_DIR; CURSOR_CONFIG_DIR
is honored for symmetry and forward-compatibility. Cursor has no equivalent of
claude mcp add --scope user — writing ~/.cursor/mcp.json directly is enough.
{
"version": 1,
"hooks": {
"sessionStart": [{ "command": "<bin> reminder --json" }],
"postToolUse": [{ "command": "<bin> buffer-tool" }],
"beforeSubmitPrompt": [{ "command": "<bin> flush-tools --json" }],
"stop": [{ "command": "<bin> chat-notify", "loop_limit": 3 }]
}
}| Event | Command | Output | Role |
|---|---|---|---|
sessionStart |
thatch reminder --json |
{ additional_context: "..." } |
Recall + heartbeat + chat identity/mail line, JSON-wrapped for Cursor |
postToolUse |
thatch buffer-tool |
silent | Appends a single tool call to the file-backed queue |
beforeSubmitPrompt |
thatch flush-tools --json |
JSON additional_context
|
Peeks queue, else recall, prediction, and behavior via sideband, else write nudge |
stop |
thatch chat-notify |
{ followup_message } or {}
|
Post-turn chat wake: when chat mail is unread, Cursor auto-submits the follow-up as the next user message (Cursor's analog of opencode's poller wake); loop_limit: 3 bounds consecutive follow-ups |
Differences from Claude Code:
-
Flat format (
{version, hooks:{event:[{command}]}}) vs nested. -
postToolUsefires per tool (Cursor has noPostToolBatch);buffer-toolreadsconversation_id(vssession_id) and normalizes it to a safe filename. -
--jsononreminderandflush-toolsso Cursor parses the output asadditional_context.
All setup operations are idempotent:
-
Instructions:
appendBlockwraps the block in sentinel comments (<!-- thatch:begin -->/<!-- thatch:end -->) and replaces the block between them, so re-running setup updates drifted content without clobbering surrounding text. No marker found → append. Pre-sentinel installs used prose sentences from the instructions themselves as delimiters;appendBlockmigrates intact legacy blocks and heals the broken kind (an agent edit normalizing punctuation - hyphens to em dashes - breaks the prose end marker) when the file tail still looks like the instructions; anything else is left for manual repair. -
Hooks:
replaceThatchHooks/replaceCursorThatchHooksfilter out any hook group whose command containsthatch, then add the current ones. A legacythatch echohook is replaced withflush-tools. Non-thatch hooks are preserved. -
MCP config: existing
mcpServersare preserved; only thethatchentry is set. -
Skills:
installSkillsonly writes when on-disk content differs from the definition (drift detection). Skills are plugin-owned — local edits are overwritten on the next plugin init orthatch setup.
thatch setup resolves <bin> from PATH, falling back to the absolute path of
the running script. This is baked into every installed hook command, so the
hooks keep working after the session that ran setup ends.
When the MCP server starts (thatch mcp, spawned by Claude Code or Cursor),
it calls checkSetup (src/setup.ts) to verify that thatch setup was run
for the current host. This catches two failure modes that would otherwise leave
the agent without usage instructions:
The host is determined from env vars set by the host process:
-
CURSOR_PROJECT_DIRset → Cursor -
CLAUDE_PROJECT_DIRset (and Cursor not) → Claude Code - Neither set → returns
null(no check — manualthatch mcpinvocation)
Cursor takes priority because Cursor also sets CLAUDE_PROJECT_DIR as an alias.
checkSetup looks for the instruction markers (start and end) in the host's
instructions file — CLAUDE.md for Claude Code, AGENTS.md for Cursor. It
checks local (in the project directory) first, then global (in the config
directory). Local takes priority.
| Outcome | Condition | Action |
|---|---|---|
| installed | Markers found in local or global instructions file | No warning — setup is complete |
| not-installed | No instructions file with markers found anywhere | Warning to stderr + prepended to first tools/call response: "Tell the user to run thatch setup --<host>" |
| markers-broken | Start marker found but end marker missing (file edited externally) | Warning with the specific file path and fix instructions: run thatch setup, or manually remove the corrupted block and re-run |
The warning is cleared after the first tools/call response so it surfaces
once, not on every tool call. The message instructs the LLM to notify the user
with the specific thatch setup command to run.
User
- Guide: Behavior Engine
- Guide: Cli
- Guide: Code Review
- Guide: Commands
- Guide: Cross Session Chat
- Guide: Deduplication
- Guide: Default Behaviors
- Guide: Extraction
- Guide: Hygiene
- Guide: Memory
- Guide: Notifications
- Guide: Prediction Engine
- Guide: Overview
- Guide: Setup
- Guide: Skills
- Guide: Watchers
Developer
Dev Feature Guides
- Feature: Behavior Engine
- Feature: Cicd
- Feature: Cli
- Feature: Commands
- Feature: Compaction Recovery
- Feature: Cross Session Chat
- Feature: Database
- Feature: Deduplication
- Feature: Extraction
- Feature: Hygiene
- Feature: Memory Store
- Feature: Multi Host
- Feature: Notifications
- Feature: Nudge Pipeline
- Feature: Opencode Plugin
- Feature: Prediction Engine
- Feature: Qa System
- Feature: Overview
- Feature: Repo Identity
- Feature: Session Lifecycle
- Feature: Setup
- Feature: Sideband
- Feature: Watchers
{ "plugin": ["@jeffober/thatch"] }