@memhtml/integrations
@memhtml/integrations wires a coding agent to a memhtml store. For each host in HOSTS (Claude Code, Codex CLI, Cursor and OpenCode) and each scope in SCOPES (user, rooted at $HOME, or project, rooted at a git top level), it can write an MCP server entry, hooks, an instruction block and a skill, and it records what it wrote in a receipt. It renders text and edits files, and it opens no store itself; doctor starts the MCP server as a child process to check it. It depends on @memhtml/contracts for the recall discipline text, effect for its one error class, and jsonc-parser for JSON edits. Its one user is the CLI in apps/cli: the memhtml integrations commands, the memhtml hook engine that every installed hook runs, and the manifest and argument checks, which read HOSTS and HOOK_EVENTS. The CLI resolves $HOME, the project root and the store root and passes them in.
hostSpec gives the HostSpec for one host at one scope: the absolute path of each host file install touches, how each one is edited (McpTarget, HooksTarget, InstructionTarget), and the host’s manual nextSteps. The hosts differ. Cursor gets no user-level instruction file, so instruction is null there, and at project scope it gets a whole-file rule, .cursor/rules/memhtml.mdc. OpenCode’s hooks are a generated plugin file. Codex keeps its MCP entry in a fenced region of config.toml, a file people also edit by hand. Codex, Cursor and OpenCode share one skill file under .agents/skills/memhtml/. detectHosts returns, in HOSTS order, the hosts whose directory in DETECT_DIRS exists under $HOME, which is how memhtml integrations install with no host picks. DEFAULT_TRACE_ROOT gives only Claude Code a default transcript root, ~/.claude, and the indexing hooks are written only when a root is known.
cliCommand, mcpCommand and hookCommand build every CommandLine from one BinaryLocation. By default the recorded node runs the recorded entry script, so the command does not depend on PATH; with bare the command is memhtml or memhtml-mcp. A hook runs the CLI with hook <event> --host <host> --repo <store>, plus --trace-root when HookCommandOptions names one. shellJoin and shellQuote turn a line into the shell string that Claude Code, Codex and Cursor store. claudeMcpEntry, cursorMcpEntry, opencodeMcpEntry and codexMcpToml render the MCP entry with the env they are given, which install fills with MEMHTML_ROOT only. In mode none, claudeHooks, codexHooks and cursorHooks return no hooks. Otherwise claudeHooks returns SessionStart, and in mode all it adds UserPromptSubmit, plus PreCompact and SessionEnd when there is a transcript root; codexHooks returns the first two only, and cursorHooks returns sessionStart alone. claudeOwned, also exported as codexOwned and cursorOwned, decides whether a hooks entry is ours: every command in it must have the hook <event> --host <host> shape and name the recorded entry script, or start with memhtml hook when installed bare.
renderOpenCodePlugin is OpenCode’s hook surface: a generated module that imports only node:child_process and runs the recorded command itself. It wires chat.message for per-prompt recall in mode all, experimental.session.compacting to add the session-start pack to a compaction, and a session.idle indexer only when there is a transcript root. In the plugin, any error, nonzero exit or 1500 ms timeout counts as no output. renderInstructionBlock is the body of the block install fences into a host’s instruction file: the store path, how to reach it, and the recall discipline from @memhtml/contracts/guidance. renderCursorRules wraps the same body as a Cursor rule with alwaysApply: true, and renderClaudeImportBlock is the @AGENTS.md import line. renderSkill renders SKILL.md with the same discipline and a table of CLI commands and MCP tools. renderShellSnippet and rcFileFor serve memhtml integrations shell, whose snippet exports MEMHTML_ROOT and adds a directory to PATH for a person at a terminal.
renderHookOutput is what memhtml hook prints for each host and event. Claude Code and OpenCode get plain text, Codex gets hookSpecificOutput.additionalContext JSON with the event names in CODEX_EVENT_NAMES, and Cursor gets an additional_context JSON object at session start. It returns the empty string for blank text and for an event the host takes no context on: compaction and session end on every host, and per-prompt on Cursor. promptOf and cwdOf read the prompt and the working directory from a host’s hook payload, and cwdOf falls back to the first of Cursor’s workspace_roots.
The file layer edits a shared file’s text rather than parsing it and printing it again. upsertJsonPath, removeJsonPath, upsertArrayEntries and removeArrayEntries edit JSON or JSONC as text splices at jsonc-parser offsets, so comments, key order and the layout of other members stay as they were, and a document that does not parse is left without the edit. A removal that empties a nested object, such as mcpServers, takes that object out too, with any comment inside it. fenced builds the region operations behind the Markdown block (upsertFencedBlock), the Codex TOML region (upsertFencedToml) and the shell rc region (upsertShellFence): replace the lines between two marker lines, or append a new region. Duplicated, unpaired or out-of-order markers throw instead of being repaired. hasUnfencedTable finds a hand-written [mcp_servers.memhtml] table outside the fence. writeTextAtomic writes a temporary file beside the target and renames it into place, keeping an existing file’s permission bits, so a reader of that file sees the old text or the new text. snapshotText and restoreText save and put back a file’s text and mode. assertNoSymlinkComponents, which install runs on every target and on the receipt path, refuses a path when any part of it that exists is a symlink. sha256 is the hash behind every receipt claim.
A receipt records what install owns, one per host per scope, at the path receiptPath names: ~/.config/memhtml/integrations/<host>.json at user scope and .memhtml-integrations/<host>.json at a project’s root. Each ReceiptEntry holds a path, a role from MANAGED_ROLES, a kind and the SHA-256 of the owned bytes. A file entry owns the whole file. A fragment entry owns a JSON key, a fenced region or the memhtml entries in a hooks object, inside a file other tools also write. inspectReceiptFile tells an absent receipt from a valid or an invalid one. Invalid covers text that is not a receipt, an entry path outside the receipt’s root, and a receipt stored where its own host, scope and root would not put it. readReceipt folds absent and invalid into null, for readers that only display. compareEntries re-hashes each entry through a reader the caller supplies and answers installed or modified, with a ReceiptDrift row for each entry that changed or is gone. receiptDriftReader is that reader, built from the receipt alone through editorFor, and install, uninstall, listIntegrations and doctor all use it.
planInstall turns InstallOptions into an InstallPlan: one PlannedWrite per artifact, each carrying pure apply, strip and ownedNow functions of a file’s current text. It reads disk only for the existing receipt and, for Claude Code at project scope, for AGENTS.md and CLAUDE.md: when AGENTS.md exists, the block goes there and CLAUDE.md gets an @AGENTS.md import, unless it already holds only that line. install runs one plan, for one host at one scope. It throws IntegrationModified and writes nothing when this host’s receipt cannot be read or an entry in it no longer matches the disk, when something already sits where an artifact goes and that receipt does not claim it, or when Codex’s config.toml has a hand-written memhtml table. With force it goes ahead and keeps the old text beside the file, in a copy named with .memhtml-backup- and a timestamp. An artifact whose bytes already equal the plan is not refused, which is how three hosts share one skill file. Then it snapshots every target, writes the files, and writes the receipt last. On a throw in that phase it restores the snapshots as best it can, removes its backups and rethrows. With dryRun it returns the InstallReport with each file’s new text in contents and writes nothing. A run that would change nothing writes nothing, not even the receipt, and reports changed: false. A re-run with a narrower hooks mode removes, in the same call, the hooks that mode no longer wants.
uninstall removes what one host’s receipt at one scope claims. It re-hashes every entry first and throws IntegrationModified, removing nothing, if any entry changed or is gone; it has no force option. It also throws, removing nothing, when the host’s receipt, or another host’s at the same scope, cannot be read. A path that another host’s receipt at the same scope also claims, such as the shared skill, is left as it is. A file that is wholly ours, or that strip leaves empty, is deleted even if it existed before install, and the receipt goes last. With no receipt file the call succeeds as not-installed with changed: false. listIntegrations returns an IntegrationRow for each host at user scope, and at project scope when given a root. The state comes from the receipts and a re-hash of what they claim, never from a config entry that only looks like ours: a hand-written MCP entry with no receipt reads not-installed, and a receipt that cannot be read reads modified.
locateBinary resolves the BinaryLocation an install records: process.execPath, the entry script the caller passes, and the memhtml-mcp script found from it through MCP_CANDIDATES or named by mcpOverride, both scripts with symlinks resolved. It throws rather than return a location with no server. doctor checks one host at one scope against its receipt and returns a DoctorResult of DoctorCheck rows. The recorded binaries must exist, the CLI’s manifest must report the recorded version, the store root must hold .memhtml/, every receipt entry must still match, the hooks, instruction block and skill must be present, and a transcript root that a hook names must be listable. Then the server must complete an MCP initialize handshake and give MCP_SERVER_NAME as its name. An absent or unreadable receipt returns one failing receipt row. The handshake is not attempted, and its row fails, when the store root has no .memhtml/, because starting the server would create one. The two checks that start a process go through DoctorProbes, which default to spawnVersion and spawnHandshake, bounded by VERSION_TIMEOUT_MS and HANDSHAKE_TIMEOUT_MS, and a caller can pass its own. Each probe gets a command line built from the receipt, so after a bare install the defaults run memhtml and memhtml-mcp from doctor’s own PATH, not the recorded scripts. On Codex it adds a hook-trust row that is always ok and only reminds the operator to trust the hooks in Codex.
Modules
Section titled “Modules”The package publishes one import path, @memhtml/integrations. Its index.ts re-exports every module with export *, partly through the barrels primitives.ts (the file layer), render.ts (the content layer) and transaction.ts (install, uninstall, list, doctor and locate), so every name in the table is on this page. The inherited signature of IntegrationModified also names YieldableError and MakeOptions, which come from effect.
| Module | What it holds |
|---|---|
types.ts |
HOSTS, HostId, isHostId, SCOPES, Scope, HOOK_EVENTS, HookEvent, isHookEvent, HOOK_MODES, HookMode, BinaryLocation, CommandLine, InstallOptions, ManagedWrite, MANAGED_ROLES, ManagedRole, Receipt, ReceiptEntry, INSTALL_STATES, InstallState and DoctorCheck. |
errors.ts |
IntegrationModified, a Schema.TaggedError class from effect carrying host, path and detail. |
dialect.ts |
renderHookOutput, CODEX_EVENT_NAMES, promptOf and cwdOf. |
hosts.ts |
McpTarget, HooksTarget, InstructionTarget, HostSpec, DISPLAY_NAMES, DETECT_DIRS, DEFAULT_TRACE_ROOT, CODEX_MCP_TABLE, hostSpec and detectHosts. |
command-line.ts |
cliCommand, mcpCommand, HookCommandOptions, hookCommand, shellQuote and shellJoin. |
render/mcp.ts |
MCP_SERVER_KEY, JsonMcpEntry, OpenCodeMcpEntry, claudeMcpEntry, cursorMcpEntry, opencodeMcpEntry and codexMcpToml. |
render/hooks.ts |
CommandHook, CommandHookGroup, CursorHookEntry, CURSOR_HOOKS_VERSION, claudeHooks, codexHooks, cursorHooks, claudeOwned, codexOwned and cursorOwned. |
render/plugin.ts |
OPENCODE_PLUGIN_FILE and renderOpenCodePlugin. |
render/block.ts |
InstructionBlockOptions, renderInstructionBlock, renderCursorRules and renderClaudeImportBlock. |
render/skill.ts |
SKILL_DIR_NAME, SkillOptions and renderSkill. |
shell.ts |
ShellSnippetOptions, renderShellSnippet and rcFileFor. |
fs.ts |
sha256, readTextOrNull, writeTextAtomic, TextSnapshot, snapshotText, restoreText, isDirectory, exists, realpathOrSelf, assertNoSymlinkComponents and expandHome. |
adapters/json.ts |
readJsonPath, upsertJsonPath, removeJsonPath, upsertArrayEntries, removeArrayEntries and parseJsonc. |
adapters/fenced.ts |
FencedText, fenced and linesOutsideFence. |
adapters/markdown.ts |
BLOCK_START, BLOCK_END, upsertFencedBlock, removeFencedBlock, readFencedBlock and isClaudeImportOnly. |
adapters/toml.ts |
FENCE_START, FENCE_END, upsertFencedToml, removeFencedToml, readFencedToml and hasUnfencedTable. |
adapters/shellrc.ts |
SHELL_FENCE_START, SHELL_FENCE_END, upsertShellFence, removeShellFence and readShellFence. |
receipt.ts |
receiptPath, ReceiptFileInspection, inspectReceiptFile, readReceipt, writeReceipt, removeReceipt, ReceiptDrift, EntryComparison and compareEntries. |
install.ts |
ENTRY_ACTIONS, EntryAction, PlannedWrite, InstallPlan, EntryReport, InstallReport, Editor, editorFor, claimOf, receiptDriftReader, planInstall and install. |
uninstall.ts |
UninstallReport and uninstall. |
list.ts |
IntegrationRow and listIntegrations. |
locate.ts |
MCP_CANDIDATES, LocateInput and locateBinary. |
doctor.ts |
ProbeResult, DoctorProbes, DoctorOptions, DoctorResult, HANDSHAKE_TIMEOUT_MS, VERSION_TIMEOUT_MS, MCP_PROTOCOL_VERSION, MCP_SERVER_NAME, spawnVersion, spawnHandshake, DEFAULT_PROBES, traceRootIn and doctor. |
index.ts |
The re-exports above: dialect.ts, errors.ts, hosts.ts and types.ts directly, the rest through primitives.ts, render.ts and transaction.ts. |
Classes
Section titled “Classes”IntegrationModified
Section titled “IntegrationModified”A receipt-owned file or entry no longer matches its receipt. Install refuses without --force, and
uninstall refuses always, so a human’s edit to a config file survives both. path is the managed
file, detail names what differs (a hash, a missing entry, an unmanaged sibling entry).
Extends
Section titled “Extends”object&YieldableError<this>
Constructors
Section titled “Constructors”Constructor
Section titled “Constructor”new IntegrationModified(...args): IntegrationModified;Parameters
Section titled “Parameters”…[props: object, options?: MakeOptions]
Returns
Section titled “Returns”Inherited from
Section titled “Inherited from”Schema.TaggedError<IntegrationModified>()( "IntegrationModified", { host: Schema.String, path: Schema.String, detail: Schema.String } ).constructor
Properties
Section titled “Properties”detail
Section titled “detail”readonly detail: string = Schema.String;Inherited from
Section titled “Inherited from”Schema.TaggedError<IntegrationModified>()( "IntegrationModified", { host: Schema.String, path: Schema.String, detail: Schema.String } ).detail
readonly host: string = Schema.String;Inherited from
Section titled “Inherited from”Schema.TaggedError<IntegrationModified>()( "IntegrationModified", { host: Schema.String, path: Schema.String, detail: Schema.String } ).host
readonly path: string = Schema.String;Inherited from
Section titled “Inherited from”Schema.TaggedError<IntegrationModified>()( "IntegrationModified", { host: Schema.String, path: Schema.String, detail: Schema.String } ).path
Interfaces
Section titled “Interfaces”BinaryLocation
Section titled “BinaryLocation”How the installed configs invoke this binary. absolute records process.execPath plus the
resolved entry script, so a GUI host with no shell PATH still finds it; bare writes memhtml and
memhtml-mcp and leaves PATH to the operator.
Properties
Section titled “Properties”readonly cli: string;The memhtml entry script, absolute and realpath-resolved.
readonly mcp: string;The memhtml-mcp entry script, absolute and realpath-resolved.
readonly node: string;The node executable, absolute.
version
Section titled “version”readonly version: string;The version the binary reports, recorded so doctor can tell an upgrade from a move.
CommandHook
Section titled “CommandHook”One command handler inside a Claude Code or Codex hook group.
Properties
Section titled “Properties”command
Section titled “command”readonly command: string;timeout
Section titled “timeout”readonly timeout: number;Seconds. A recall hook shares the turn’s latency budget; an indexer does not.
readonly type: "command";CommandHookGroup
Section titled “CommandHookGroup”One entry of a Claude Code / Codex event array. matcher is deliberately ABSENT: both hosts treat a
missing matcher as “every source”, and writing matcher: "*" would make the entry look like a filter
that had been narrowed.
Properties
Section titled “Properties”readonly hooks: readonly CommandHook[];CommandLine
Section titled “CommandLine”One command line the host will run: the MCP server, or a hook.
Properties
Section titled “Properties”readonly args: readonly string[];command
Section titled “command”readonly command: string;CursorHookEntry
Section titled “CursorHookEntry”One entry of a Cursor event array: a bare command string, no group and no timeout field.
Properties
Section titled “Properties”command
Section titled “command”readonly command: string;DoctorCheck
Section titled “DoctorCheck”One row of integrations doctor.
Properties
Section titled “Properties”detail
Section titled “detail”readonly detail: string;readonly name: string;readonly ok: boolean;suggestions
Section titled “suggestions”readonly suggestions: readonly string[];DoctorOptions
Section titled “DoctorOptions”Properties
Section titled “Properties”probes?
Section titled “probes?”readonly probes?: Partial<DoctorProbes>;Override either probe; the rest fall back to the spawning defaults.
DoctorProbes
Section titled “DoctorProbes”The two checks that need a child process.
Split into two named probes rather than one generic spawn, because the two consume their child
completely differently — one reads a JSON envelope off a process that exits, the other holds a
long-lived stdio server open through a two-message handshake — and a caller handed one function could
not tell which behavior a row wanted.
Properties
Section titled “Properties”handshake
Section titled “handshake”readonly handshake: (mcp, env) => Promise<ProbeResult>;Complete an MCP initialize handshake with the server this command line names.
Parameters
Section titled “Parameters”Readonly<Record<string, string>>
Returns
Section titled “Returns”Promise<ProbeResult>
version
Section titled “version”readonly version: (cli, env) => Promise<ProbeResult>;Run <cli> manifest and report the version it prints.
Parameters
Section titled “Parameters”Readonly<Record<string, string>>
Returns
Section titled “Returns”Promise<ProbeResult>
DoctorResult
Section titled “DoctorResult”Properties
Section titled “Properties”checks
Section titled “checks”readonly checks: readonly DoctorCheck[];healthy
Section titled “healthy”readonly healthy: boolean;Editor
Section titled “Editor”How one artifact’s bytes are read back out of a file and removed from it.
Properties
Section titled “Properties”ownedNow
Section titled “ownedNow”readonly ownedNow: (current) => string | null;Parameters
Section titled “Parameters”current
Section titled “current”string | null
Returns
Section titled “Returns”string | null
readonly strip: (current) => string | null;Parameters
Section titled “Parameters”current
Section titled “current”string | null
Returns
Section titled “Returns”string | null
EntryComparison
Section titled “EntryComparison”What compareEntries reports: the install state, and each entry whose hash on disk differs from the receipt.
Properties
Section titled “Properties”readonly drift: readonly ReceiptDrift[];readonly state: "not-installed" | "installed" | "modified";EntryReport
Section titled “EntryReport”Properties
Section titled “Properties”action
Section titled “action”readonly action: "created" | "updated" | "unchanged" | "planned" | "removed";readonly kind: "file" | "fragment";readonly path: string;readonly role: | "mcp-entry" | "hooks" | "instruction-block" | "skill" | "plugin" | "rules";FencedText
Section titled “FencedText”The three operations a fenced file supports, bound to one pair of markers.
Properties
Section titled “Properties”endMarker
Section titled “endMarker”readonly endMarker: string;readonly has: (text) => boolean;True when the file carries a well-formed fence. Throws on malformed markers, like the rest.
Parameters
Section titled “Parameters”string | null
Returns
Section titled “Returns”boolean
readonly read: (text) => string | null;The body between the markers, or null when the file carries no fence.
Parameters
Section titled “Parameters”string | null
Returns
Section titled “Returns”string | null
remove
Section titled “remove”readonly remove: (text) => string;Drop the fenced region and the blank line separating it. "" when the fence was the whole file.
Parameters
Section titled “Parameters”string | null
Returns
Section titled “Returns”string
startMarker
Section titled “startMarker”readonly startMarker: string;upsert
Section titled “upsert”readonly upsert: (text, body) => string;Replace the fenced region with body, or append a fence when the file has none.
Parameters
Section titled “Parameters”string | null
string
Returns
Section titled “Returns”string
HookCommandOptions
Section titled “HookCommandOptions”What a hook command line needs beyond the event: which store, and where transcripts live.
Properties
Section titled “Properties”memhtmlRoot
Section titled “memhtmlRoot”readonly memhtmlRoot: string;The store the hook reads, passed as the global --repo so the hook never depends on cwd or env.
traceRoot?
Section titled “traceRoot?”readonly traceRoot?: string;Where this host writes transcripts, when a format exists for it; absent omits the flag.
HostSpec
Section titled “HostSpec”Everything install needs to know about one host at one scope. Absolute paths throughout.
Properties
Section titled “Properties”displayName
Section titled “displayName”readonly displayName: string;readonly hooks: HooksTarget;readonly host: "claude" | "codex" | "cursor" | "opencode";instruction
Section titled “instruction”readonly instruction: InstructionTarget | null;null for Cursor at user scope: it has no user-level instruction file this build writes.
readonly mcp: McpTarget;nextSteps
Section titled “nextSteps”readonly nextSteps: readonly string[];The host’s manual half: a restart, a trust step, an approval prompt.
readonly root: string;$HOME at user scope, the git top level at project scope.
readonly scope: "user" | "project";readonly skill: string;The SKILL.md install writes, absolute.
InstallOptions
Section titled “InstallOptions”Properties
Section titled “Properties”bareCommand
Section titled “bareCommand”readonly bareCommand: boolean;binary
Section titled “binary”readonly binary: BinaryLocation;dryRun
Section titled “dryRun”readonly dryRun: boolean;readonly force: boolean;readonly hooks: "all" | "session" | "none";readonly host: "claude" | "codex" | "cursor" | "opencode";memhtmlRoot
Section titled “memhtmlRoot”readonly memhtmlRoot: string;The store the integration points at. Absolute.
readonly root: string;$HOME for user scope, the git top level for project scope. Absolute.
readonly scope: "user" | "project";traceRoot?
Section titled “traceRoot?”readonly traceRoot?: string;Where this host writes transcripts, when known; absent means no indexing hooks.
InstallPlan
Section titled “InstallPlan”Properties
Section titled “Properties”readonly host: "claude" | "codex" | "cursor" | "opencode";memhtmlRoot
Section titled “memhtmlRoot”readonly memhtmlRoot: string;nextSteps
Section titled “nextSteps”readonly nextSteps: readonly string[];previous
Section titled “previous”readonly previous: Receipt | null;The receipt as it stands, or null when this host is not installed at this scope.
previousProblem?
Section titled “previousProblem?”readonly previousProblem?: string;Set when a receipt is present but unreadable, which only --force gets past; it is backed up.
receiptPath
Section titled “receiptPath”readonly receiptPath: string;readonly root: string;readonly scope: "user" | "project";writes
Section titled “writes”readonly writes: readonly PlannedWrite[];InstallReport
Section titled “InstallReport”Properties
Section titled “Properties”backups
Section titled “backups”readonly backups: readonly string[];Prior bytes kept beside a file --force overwrote, absolute.
changed
Section titled “changed”readonly changed: boolean;False when every managed fragment already matched the plan and the receipt already described it.
contents?
Section titled “contents?”readonly contents?: Readonly<Record<string, string>>;The whole new text of every file, dry run only.
dryRun
Section titled “dryRun”readonly dryRun: boolean;entries
Section titled “entries”readonly entries: readonly EntryReport[];readonly host: "claude" | "codex" | "cursor" | "opencode";memhtmlRoot
Section titled “memhtmlRoot”readonly memhtmlRoot: string;nextSteps
Section titled “nextSteps”readonly nextSteps: readonly string[];receiptPath
Section titled “receiptPath”readonly receiptPath: string;readonly root: string;readonly scope: "user" | "project";InstructionBlockOptions
Section titled “InstructionBlockOptions”Properties
Section titled “Properties”readonly cli: CommandLine;How this host invokes the CLI, rendered as the shell string an operator could paste.
hasMcp
Section titled “hasMcp”readonly hasMcp: boolean;Whether an MCP entry was installed; when false the block names the CLI only.
readonly host: "claude" | "codex" | "cursor" | "opencode";The host this block is rendered for. The body is byte-identical across the four hosts today, and the parameter is here so a host-specific line has exactly one place to land rather than being spliced in by whoever needs it first.
memhtmlRoot
Section titled “memhtmlRoot”readonly memhtmlRoot: string;The store, absolute, named in the prose so the agent can see which repo it is remembering into.
IntegrationRow
Section titled “IntegrationRow”Properties
Section titled “Properties”detail?
Section titled “detail?”readonly detail?: string;What is wrong, on a modified row: the drifted path, or why the receipt cannot be read.
readonly host: "claude" | "codex" | "cursor" | "opencode";memhtmlRoot?
Section titled “memhtmlRoot?”readonly memhtmlRoot?: string;The store it points at, when a receipt says.
receiptPath
Section titled “receiptPath”readonly receiptPath: string;readonly root: string;$HOME at user scope, the git top level at project scope.
readonly scope: "user" | "project";readonly state: "not-installed" | "installed" | "modified";version?
Section titled “version?”readonly version?: string;The memhtml version that installed it, when a receipt says.
JsonMcpEntry
Section titled “JsonMcpEntry”What the installer merges into mcpServers for Claude Code and Cursor.
Properties
Section titled “Properties”readonly args?: readonly string[];Omitted entirely when the command needs no arguments, rather than written as [].
command
Section titled “command”readonly command: string;readonly env: Readonly<Record<string, string>>;LocateInput
Section titled “LocateInput”Properties
Section titled “Properties”readonly entry: string;The memhtml entry script this process was started as, normally process.argv[1].
mcpOverride?
Section titled “mcpOverride?”readonly mcpOverride?: string;An explicit memhtml-mcp path, for a deployment that does not keep the two bins together.
version
Section titled “version”readonly version: string;The version to record, normally the CLI manifest’s.
ManagedWrite
Section titled “ManagedWrite”One thing install writes. kind says how ownership is checked: a file is wholly ours and its
SHA-256 is the receipt’s claim; a fragment lives inside a file other tools also own (a JSON key, a
TOML fence, a Markdown block, a hooks array entry) and only the fragment’s rendered bytes are claimed.
Extended by
Section titled “Extended by”Properties
Section titled “Properties”content
Section titled “content”readonly content: string;The bytes install owns: the whole file, or the fragment as rendered.
readonly kind: "file" | "fragment";readonly path: string;Absolute path of the file touched.
readonly role: | "mcp-entry" | "hooks" | "instruction-block" | "skill" | "plugin" | "rules";What the fragment is, for reports: mcp-entry, hooks, instruction-block, skill, plugin, rules.
OpenCodeMcpEntry
Section titled “OpenCodeMcpEntry”What the installer merges into mcp for OpenCode.
Properties
Section titled “Properties”command
Section titled “command”readonly command: readonly string[];OpenCode’s one-array form: the command and its arguments together.
enabled
Section titled “enabled”readonly enabled: boolean;environment
Section titled “environment”readonly environment: Readonly<Record<string, string>>;readonly type: "local";PlannedWrite
Section titled “PlannedWrite”One artifact, plus the three total functions that make it a transaction step.
Extends
Section titled “Extends”Properties
Section titled “Properties”readonly apply: (current) => string | null;The whole new file, given its current text (null when absent); null means the file goes.
Parameters
Section titled “Parameters”current
Section titled “current”string | null
Returns
Section titled “Returns”string | null
content
Section titled “content”readonly content: string;The bytes install owns: the whole file, or the fragment as rendered.
Inherited from
Section titled “Inherited from”readonly kind: "file" | "fragment";Inherited from
Section titled “Inherited from”ownedNow
Section titled “ownedNow”readonly ownedNow: (current) => string | null;The bytes we claim as they are on disk NOW, or null when our part is absent.
Parameters
Section titled “Parameters”current
Section titled “current”string | null
Returns
Section titled “Returns”string | null
readonly path: string;Absolute path of the file touched.
Inherited from
Section titled “Inherited from”retire?
Section titled “retire?”readonly retire?: true;A write that REMOVES an artifact the previous receipt claims and claims nothing itself: the hooks or
plugin of an install being re-run with --hooks none. Reported as removed, absent from the new
receipt. Without it the new receipt would drop the entry while the hooks stayed armed on disk, with
no record left to remove them by.
readonly role: | "mcp-entry" | "hooks" | "instruction-block" | "skill" | "plugin" | "rules";What the fragment is, for reports: mcp-entry, hooks, instruction-block, skill, plugin, rules.
Inherited from
Section titled “Inherited from”readonly strip: (current) => string | null;The file with our part removed; null when nothing of the caller’s would be left.
Parameters
Section titled “Parameters”current
Section titled “current”string | null
Returns
Section titled “Returns”string | null
ProbeResult
Section titled “ProbeResult”What one probe answers. value is the version a manifest probe read, when it read one.
Properties
Section titled “Properties”detail
Section titled “detail”readonly detail: string;readonly ok: boolean;value?
Section titled “value?”readonly value?: string;Receipt
Section titled “Receipt”The receipt: ownership proof and the record of what to remove. One per host per scope, at
receiptPath(...). files maps the absolute path plus role to the SHA-256 of the owned bytes.
Properties
Section titled “Properties”bareCommand
Section titled “bareCommand”readonly bareCommand: boolean;binary
Section titled “binary”readonly binary: BinaryLocation;entries
Section titled “entries”readonly entries: readonly ReceiptEntry[];readonly hooks: "all" | "session" | "none";readonly host: "claude" | "codex" | "cursor" | "opencode";installedAt
Section titled “installedAt”readonly installedAt: string;memhtmlRoot
Section titled “memhtmlRoot”readonly memhtmlRoot: string;package
Section titled “package”readonly package: "memhtml";readonly root: string;readonly scope: "user" | "project";version
Section titled “version”readonly version: string;ReceiptDrift
Section titled “ReceiptDrift”One entry whose bytes on disk no longer hash to what the receipt claims. null means gone.
Properties
Section titled “Properties”actual
Section titled “actual”readonly actual: string | null;readonly entry: ReceiptEntry;ReceiptEntry
Section titled “ReceiptEntry”Properties
Section titled “Properties”readonly kind: "file" | "fragment";readonly path: string;readonly role: | "mcp-entry" | "hooks" | "instruction-block" | "skill" | "plugin" | "rules";sha256
Section titled “sha256”readonly sha256: string;ReceiptFileInspection
Section titled “ReceiptFileInspection”What a receipt file turned out to be. problem is present exactly when state is invalid.
Properties
Section titled “Properties”problem?
Section titled “problem?”readonly problem?: string;receipt
Section titled “receipt”readonly receipt: Receipt | null;readonly state: "absent" | "valid" | "invalid";ShellSnippetOptions
Section titled “ShellSnippetOptions”Properties
Section titled “Properties”binDir
Section titled “binDir”readonly binDir: string;The directory holding the memhtml and memhtml-mcp binaries, absolute.
memhtmlRoot
Section titled “memhtmlRoot”readonly memhtmlRoot: string;The store, absolute.
SkillOptions
Section titled “SkillOptions”Properties
Section titled “Properties”readonly cli: CommandLine;How this host invokes the CLI; every row of the command table is rendered from it.
memhtmlRoot
Section titled “memhtmlRoot”readonly memhtmlRoot: string;The store, absolute.
TextSnapshot
Section titled “TextSnapshot”What a file looked like before install touched it. text: null records “there was no file”.
Properties
Section titled “Properties”readonly mode: number | null;readonly path: string;readonly text: string | null;UninstallReport
Section titled “UninstallReport”Properties
Section titled “Properties”changed
Section titled “changed”readonly changed: boolean;False when there was nothing to remove, so an uninstall is re-runnable and says so.
readonly host: "claude" | "codex" | "cursor" | "opencode";receiptPath
Section titled “receiptPath”readonly receiptPath: string;removed
Section titled “removed”readonly removed: readonly EntryReport[];readonly root: string;readonly scope: "user" | "project";readonly state: "not-installed" | "installed" | "modified";not-installed when there was no receipt, installed when the removal ran.
Type Aliases
Section titled “Type Aliases”EntryAction
Section titled “EntryAction”type EntryAction = typeof ENTRY_ACTIONS[number];HookEvent
Section titled “HookEvent”type HookEvent = typeof HOOK_EVENTS[number];HookMode
Section titled “HookMode”type HookMode = typeof HOOK_MODES[number];HooksTarget
Section titled “HooksTarget”type HooksTarget = | { file: string; format: "json-arrays"; keyPath: ReadonlyArray<string>; versionKey?: { keyPath: ReadonlyArray<string>; value: number; };} | { file: string; format: "plugin-file";};Where a host’s hooks go.
json-arrays is Claude Code, Codex, and Cursor: one object under keyPath whose keys are the
host’s own event names and whose values are arrays install appends to entry by entry. versionKey
is Cursor’s {"version": 1} preamble, which the file needs and which is not an array.
plugin-file is OpenCode, where the hook surface is a JavaScript module install generates whole.
Union Members
Section titled “Union Members”Type Literal
Section titled “Type Literal”{ file: string; format: "json-arrays"; keyPath: ReadonlyArray<string>; versionKey?: { keyPath: ReadonlyArray<string>; value: number; };}readonly file: string;format
Section titled “format”readonly format: "json-arrays";keyPath
Section titled “keyPath”readonly keyPath: ReadonlyArray<string>;versionKey?
Section titled “versionKey?”readonly versionKey?: object;A scalar the file must carry beside the hooks, when the host versions its hooks file.
versionKey.keyPath
Section titled “versionKey.keyPath”readonly keyPath: ReadonlyArray<string>;versionKey.value
Section titled “versionKey.value”readonly value: number;Type Literal
Section titled “Type Literal”{ file: string; format: "plugin-file";}HostId
Section titled “HostId”type HostId = typeof HOSTS[number];InstallState
Section titled “InstallState”type InstallState = typeof INSTALL_STATES[number];InstructionTarget
Section titled “InstructionTarget”type InstructionTarget = | { agentsFile?: string; file: string; format: "markdown-block";} | { file: string; format: "cursor-rules";};Where the instruction prose goes.
markdown-block is a fenced region inside a file the operator also writes. cursor-rules is a
whole file of ours, because Cursor reads .cursor/rules/*.mdc and each rule file is one rule with
its own frontmatter — there is nothing to merge into.
agentsFile is Claude Code’s project-scope fork, and it is present only there. Claude Code reads
CLAUDE.md and not AGENTS.md, while every other host in a repository reads AGENTS.md, so a repo
that already has one gets the block THERE and a @AGENTS.md import in CLAUDE.md — one copy of the
prose, visible to both. The transaction layer decides between the two by looking at disk, since
which file exists is not a property of the host.
ManagedRole
Section titled “ManagedRole”type ManagedRole = typeof MANAGED_ROLES[number];McpTarget
Section titled “McpTarget”type McpTarget = | { file: string; format: "json"; keyPath: ReadonlyArray<string>;} | { file: string; format: "toml-fence"; table: string;};Where a host’s MCP server entry goes, and what kind of document it is.
json carries the key path the entry lands at, so the transaction layer never spells a vendor’s
key. toml-fence carries the table NAME instead, which is what hasUnfencedTable needs to refuse a
hand-written twin of our entry: the fence cannot see structure, so the collision check is by name.
type Scope = typeof SCOPES[number];Variables
Section titled “Variables”BLOCK_END
Section titled “BLOCK_END”const BLOCK_END: "<!-- MEMHTML:END -->" = "<!-- MEMHTML:END -->";BLOCK_START
Section titled “BLOCK_START”const BLOCK_START: "<!-- MEMHTML:START -->" = "<!-- MEMHTML:START -->";CODEX_EVENT_NAMES
Section titled “CODEX_EVENT_NAMES”const CODEX_EVENT_NAMES: Readonly<Record<HookEvent, string>>;Codex spells its event names the way Claude Code does.
CODEX_MCP_TABLE
Section titled “CODEX_MCP_TABLE”const CODEX_MCP_TABLE: "mcp_servers.memhtml";Codex’s table name, the one hasUnfencedTable refuses a hand-written copy of.
codexOwned
Section titled “codexOwned”const codexOwned: (entry, binary?, bare?) => boolean = claudeOwned;Codex writes Claude Code’s entry shape, so ownership is decided the same way.
Is this entry from an existing hooks file one of OURS? Used by install to replace and by uninstall to
remove, so a human’s own SessionStart hook survives both.
The signal is the invocation itself: the hook <event> --host <host> shape only hookCommand
renders, then the program. Installed bare, the program is the literal memhtml the line starts with;
installed absolute, it is the entry script the receipt names. EVERY command in the entry must match —
a group someone else has added a second handler to is not ours to rewrite — and an entry carrying no
command at all is never ours.
Parameters
Section titled “Parameters”unknown
binary?
Section titled “binary?”boolean
Returns
Section titled “Returns”boolean
CURSOR_HOOKS_VERSION
Section titled “CURSOR_HOOKS_VERSION”const CURSOR_HOOKS_VERSION: 1 = 1;hooks.json for Cursor is versioned; the installer writes this when it creates the file.
cursorOwned
Section titled “cursorOwned”const cursorOwned: (entry, binary?, bare?) => boolean = claudeOwned;Cursor’s entry is a bare { command }, which commandsOf already reads.
Is this entry from an existing hooks file one of OURS? Used by install to replace and by uninstall to
remove, so a human’s own SessionStart hook survives both.
The signal is the invocation itself: the hook <event> --host <host> shape only hookCommand
renders, then the program. Installed bare, the program is the literal memhtml the line starts with;
installed absolute, it is the entry script the receipt names. EVERY command in the entry must match —
a group someone else has added a second handler to is not ours to rewrite — and an entry carrying no
command at all is never ours.
Parameters
Section titled “Parameters”unknown
binary?
Section titled “binary?”boolean
Returns
Section titled “Returns”boolean
DEFAULT_PROBES
Section titled “DEFAULT_PROBES”const DEFAULT_PROBES: DoctorProbes;DEFAULT_TRACE_ROOT
Section titled “DEFAULT_TRACE_ROOT”const DEFAULT_TRACE_ROOT: Readonly<Record<HostId, string | null>>;Where each host writes transcripts, ~-relative, or null when memhtml cannot read them.
Claude Code alone, because @memhtml/traces reads Claude Code’s JSONL transcript format and no
other. A null host installs no indexing hook rather than one with nothing to read; an operator who
knows better says so with MEMHTML_TRACE_ROOT, which the CLI passes through for any host.
DETECT_DIRS
Section titled “DETECT_DIRS”const DETECT_DIRS: Readonly<Record<HostId, string>>;The directory whose existence means a host is installed on this machine, relative to $HOME.
integrations install with no host argument wires every host this finds, so the rule has to be one
a host creates itself on first run and never as a side effect of something else.
DISPLAY_NAMES
Section titled “DISPLAY_NAMES”const DISPLAY_NAMES: Readonly<Record<HostId, string>>;What each host calls itself, for a report a human reads.
ENTRY_ACTIONS
Section titled “ENTRY_ACTIONS”const ENTRY_ACTIONS: readonly ["created", "updated", "unchanged", "planned", "removed"];What happened to one managed path. planned is the dry-run answer and removed is uninstall’s, so
one vocabulary describes every row either verb reports.
FENCE_END
Section titled “FENCE_END”const FENCE_END: "# MEMHTML:END" = "# MEMHTML:END";FENCE_START
Section titled “FENCE_START”const FENCE_START: "# MEMHTML:START" = "# MEMHTML:START";HANDSHAKE_TIMEOUT_MS
Section titled “HANDSHAKE_TIMEOUT_MS”const HANDSHAKE_TIMEOUT_MS: 20000 = 20_000;How long the handshake may take. A stdio server that has not answered by then is not wired.
Twice the version probe, because it does strictly more: initialize is answered only after the
server has loaded the CLI’s module graph and opened and migrated the store’s database. On a loaded
box (the full mise run check on a 2-vCPU runner) that cold start exceeded 5 s on 2026-09-14 and
2026-09-22, against a server that answered in under a second alone.
HOOK_EVENTS
Section titled “HOOK_EVENTS”const HOOK_EVENTS: readonly ["session-start", "user-prompt-submit", "pre-compact", "session-end"];The lifecycle events memhtml hook understands, host-neutral. Each host’s installer maps these onto
the host’s own event names (SessionStart, sessionStart, session.created, …) and the hook
engine maps the host’s stdin payload back.
HOOK_MODES
Section titled “HOOK_MODES”const HOOK_MODES: readonly ["all", "session", "none"];Which hooks integrations install writes.
const HOSTS: readonly ["claude", "codex", "cursor", "opencode"];The four coding agents this package can wire, in the order the docs present them.
INSTALL_STATES
Section titled “INSTALL_STATES”const INSTALL_STATES: readonly ["not-installed", "installed", "modified"];MANAGED_ROLES
Section titled “MANAGED_ROLES”const MANAGED_ROLES: readonly ["mcp-entry", "hooks", "instruction-block", "skill", "plugin", "rules"];MCP_CANDIDATES
Section titled “MCP_CANDIDATES”const MCP_CANDIDATES: readonly ["./memhtml-mcp.mjs", "../../mcp/dist/bin.js"];Where memhtml-mcp sits relative to the memhtml entry script, in each layout that ships.
The same two candidates apps/cli/src/serve.ts walks, and for the same reason: the two apps are one
build, and where that build puts them differs. ./memhtml-mcp.mjs is the published package, where
both bins are entry points of one bundle and land beside each other; ../../mcp/dist/bin.js is the
workspace, where apps/cli/dist/bin.js reaches apps/mcp/dist/bin.js two directories up and across.
MCP_PROTOCOL_VERSION
Section titled “MCP_PROTOCOL_VERSION”const MCP_PROTOCOL_VERSION: "2025-06-18" = "2025-06-18";The protocol revision memhtml-mcp speaks, and the only adapter it ships.
MCP_SERVER_KEY
Section titled “MCP_SERVER_KEY”const MCP_SERVER_KEY: "memhtml" = "memhtml";The key this entry is stored under in every host’s server map.
MCP_SERVER_NAME
Section titled “MCP_SERVER_NAME”const MCP_SERVER_NAME: "memhtml" = "memhtml";The name serverInfo must carry for the handshake to be with OUR server.
OPENCODE_PLUGIN_FILE
Section titled “OPENCODE_PLUGIN_FILE”const OPENCODE_PLUGIN_FILE: "memhtml.js" = "memhtml.js";The file name inside the host’s plugins directory.
SCOPES
Section titled “SCOPES”const SCOPES: readonly ["user", "project"];user anchors at $HOME; project anchors at a git repository’s top level.
SHELL_FENCE_END
Section titled “SHELL_FENCE_END”const SHELL_FENCE_END: "# MEMHTML:END" = "# MEMHTML:END";SHELL_FENCE_START
Section titled “SHELL_FENCE_START”const SHELL_FENCE_START: "# MEMHTML:START" = "# MEMHTML:START";SKILL_DIR_NAME
Section titled “SKILL_DIR_NAME”const SKILL_DIR_NAME: "memhtml" = "memhtml";The skill’s directory name, which must equal the frontmatter name.
spawnHandshake
Section titled “spawnHandshake”const spawnHandshake: DoctorProbes["handshake"];Spawn the recorded server and complete initialize.
The check that catches a config which READS correctly and starts nothing: a moved entry script, a node that no longer exists, a bundle missing an asset. Every failure mode resolves rather than rejects, so one bad row cannot take the report down with it, and the child is killed on every path — a doctor that left a server holding the store’s database open would be worse than no doctor.
spawnVersion
Section titled “spawnVersion”const spawnVersion: DoctorProbes["version"];<cli> manifest, read for its version.
manifest is the one command that answers with no store, no database, and no credential, which is
why it is the probe: a version check that opened the store would fail for reasons about the store.
VERSION_TIMEOUT_MS
Section titled “VERSION_TIMEOUT_MS”const VERSION_TIMEOUT_MS: 10000 = 10_000;How long the version probe may take. manifest opens nothing, so this is generous already.
Functions
Section titled “Functions”assertNoSymlinkComponents()
Section titled “assertNoSymlinkComponents()”function assertNoSymlinkComponents(path): Promise<void>;Refuse path when any component that already exists is a symlink.
Walking stops at the first component that does not exist, because nothing below a missing directory
exists either. The rule is openwiki’s, learned the hard way: an installer must never follow a symlink
into a place it did not intend to write — a ~/.claude linked into a dotfiles repo turns a config
edit into a commit in someone else’s tree.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Promise<void>
claimOf()
Section titled “claimOf()”function claimOf( receipt, path, role): ReceiptEntry | undefined;Which entry of a receipt claims this path and role, if any.
Parameters
Section titled “Parameters”receipt
Section titled “receipt”Receipt | null
string
| "mcp-entry"
| "hooks"
| "instruction-block"
| "skill"
| "plugin"
| "rules"
Returns
Section titled “Returns”ReceiptEntry | undefined
claudeHooks()
Section titled “claudeHooks()”function claudeHooks( binary, bare, mode, opts): Record<string, CommandHookGroup[]>;Claude Code’s hooks object in ~/.claude/settings.json (or .claude/settings.json at project
scope). PreCompact and SessionEnd appear only when a transcript root is known, since the indexer
has nothing to read without one.
Parameters
Section titled “Parameters”binary
Section titled “binary”boolean
"all" | "session" | "none"
Returns
Section titled “Returns”Record<string, CommandHookGroup[]>
claudeMcpEntry()
Section titled “claudeMcpEntry()”function claudeMcpEntry(mcp, env): JsonMcpEntry;Claude Code’s mcpServers.memhtml. args is omitted when empty because claude mcp get prints the
entry back and an empty array reads as a truncated command.
Parameters
Section titled “Parameters”Readonly<Record<string, string>>
Returns
Section titled “Returns”claudeOwned()
Section titled “claudeOwned()”function claudeOwned( entry, binary?, bare?): boolean;Is this entry from an existing hooks file one of OURS? Used by install to replace and by uninstall to
remove, so a human’s own SessionStart hook survives both.
The signal is the invocation itself: the hook <event> --host <host> shape only hookCommand
renders, then the program. Installed bare, the program is the literal memhtml the line starts with;
installed absolute, it is the entry script the receipt names. EVERY command in the entry must match —
a group someone else has added a second handler to is not ours to rewrite — and an entry carrying no
command at all is never ours.
Parameters
Section titled “Parameters”unknown
binary?
Section titled “binary?”boolean
Returns
Section titled “Returns”boolean
cliCommand()
Section titled “cliCommand()”function cliCommand(binary, bare): CommandLine;The memhtml CLI. bare writes memhtml and leaves resolution to PATH; the default runs the
recorded node against the recorded entry script, which is what survives a GUI host with no shell
PATH.
Parameters
Section titled “Parameters”binary
Section titled “binary”boolean
Returns
Section titled “Returns”codexHooks()
Section titled “codexHooks()”function codexHooks( binary, bare, mode, opts): Record<string, CommandHookGroup[]>;Codex’s hooks object in ~/.codex/hooks.json. Same entry shape and event names as Claude Code’s,
and no compaction hook: there is no Codex transcript reader to index with.
The entries stay inert until the operator trusts them in /hooks — trust is keyed to the hook’s
hash, so nothing an installer writes can pre-approve itself. integrations doctor says so rather
than pretending to verify it.
Parameters
Section titled “Parameters”binary
Section titled “binary”boolean
"all" | "session" | "none"
Returns
Section titled “Returns”Record<string, CommandHookGroup[]>
codexMcpToml()
Section titled “codexMcpToml()”function codexMcpToml(mcp, env): string;Codex’s [mcp_servers.memhtml] table plus its env sub-table, as the text the installer places
inside its # MEMHTML:START / # MEMHTML:END fence in ~/.codex/config.toml.
The env table is a separate [mcp_servers.memhtml.env] header rather than an inline table, so a
long absolute path never runs past a line the operator has to read. It comes LAST, because every key
after a table header belongs to that table.
Parameters
Section titled “Parameters”Readonly<Record<string, string>>
Returns
Section titled “Returns”string
compareEntries()
Section titled “compareEntries()”function compareEntries(entries, current): Promise<EntryComparison>;Hash every entry against disk and report installed or modified.
current is supplied by the caller because only it knows how to extract a fragment — the SHA-256 of a
whole file for kind: "file", of the rendered fence or JSON entry for kind: "fragment" — and it
answers null when the file or the fragment is gone. A missing fragment is drift rather than a separate
state: from the operator’s side, “someone deleted our hook” and “someone edited our hook” both mean the
config no longer matches the receipt, and both must block an uninstall that would delete their work.
not-installed is never returned here. That is the absence of a receipt, which is the caller’s
question, not this function’s.
Parameters
Section titled “Parameters”entries
Section titled “entries”readonly ReceiptEntry[]
current
Section titled “current”(entry) => Promise<string | null>
Returns
Section titled “Returns”Promise<EntryComparison>
cursorHooks()
Section titled “cursorHooks()”function cursorHooks( binary, bare, mode, opts): Record<string, CursorHookEntry[]>;Cursor’s hooks object in ~/.cursor/hooks.json, whose file wraps this as
{ version: CURSOR_HOOKS_VERSION, hooks: … }. One event only: sessionStart returns
additional_context, and it is the sole Cursor event that can put anything in front of the model.
Parameters
Section titled “Parameters”binary
Section titled “binary”boolean
"all" | "session" | "none"
Returns
Section titled “Returns”Record<string, CursorHookEntry[]>
cursorMcpEntry()
Section titled “cursorMcpEntry()”function cursorMcpEntry(mcp, env): JsonMcpEntry;Cursor’s mcp.json takes the same object as Claude Code’s, key for key.
Parameters
Section titled “Parameters”Readonly<Record<string, string>>
Returns
Section titled “Returns”cwdOf()
Section titled “cwdOf()”function cwdOf(payload): string | undefined;The working directory a session runs in, when the payload carries one. Claude Code and Codex send
cwd; Cursor sends workspace_roots (the first is used); the OpenCode plugin sends cwd.
Parameters
Section titled “Parameters”payload
Section titled “payload”unknown
Returns
Section titled “Returns”string | undefined
detectHosts()
Section titled “detectHosts()”function detectHosts(home): Promise<readonly ("claude" | "codex" | "cursor" | "opencode")[]>;Which hosts this machine has, in HOSTS order, by DETECT_DIRS.
A directory rather than a binary on PATH: three of the four hosts ship as a GUI app or through a
version manager, so the config directory is the artifact that exists on every install path. A host
whose directory is absent is not wired, which is what makes integrations install with no argument
a statement about this machine rather than a guess.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Promise<readonly ("claude" | "codex" | "cursor" | "opencode")[]>
doctor()
Section titled “doctor()”function doctor( host, scope, root, opts?): Promise<DoctorResult>;Every check for one host at one scope.
An absent or unreadable receipt answers with that ONE row and stops: nothing else is knowable without it, and a report of twelve failures whose single cause is “not installed” is a report nobody reads to the end.
Parameters
Section titled “Parameters”"claude" | "codex" | "cursor" | "opencode"
"user" | "project"
string
DoctorOptions = {}
Returns
Section titled “Returns”Promise<DoctorResult>
editorFor()
Section titled “editorFor()”function editorFor( spec, entry, binary, bare): Editor;The editor for one receipt entry, derived from the host’s spec and the receipt’s own binary.
The binary comes from the RECEIPT rather than from what resolves today, because ownership of a hooks entry is decided by the command line that was written: an upgrade moves the entry script, and an entry matched against the new path would read as somebody else’s.
Parameters
Section titled “Parameters”Pick<ReceiptEntry, "role" | "kind">
binary
Section titled “binary”boolean
Returns
Section titled “Returns”exists()
Section titled “exists()”function exists(path): Promise<boolean>;True when something exists at path. lstat, so a dangling symlink counts as existing — install
must treat a broken link as an obstacle rather than as free space.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Promise<boolean>
expandHome()
Section titled “expandHome()”function expandHome(path, home): string;~ and ~/… resolved against home. Nothing else is expanded: $VAR and ~other are left alone,
since a config value that looks like a shell expression is not one by the time this package sees it.
Parameters
Section titled “Parameters”string
string
Returns
Section titled “Returns”string
fenced()
Section titled “fenced()”function fenced(startMarker, endMarker): FencedText;Build the fence operations for one marker pair. startMarker and endMarker are matched against a
line’s trimmed content, so an indented marker still counts — the markers our own renderers write are
flush left, but a hand-reindented file must not silently grow a second region.
Parameters
Section titled “Parameters”startMarker
Section titled “startMarker”string
endMarker
Section titled “endMarker”string
Returns
Section titled “Returns”hasUnfencedTable()
Section titled “hasUnfencedTable()”function hasUnfencedTable(text, table): boolean;True when a [table] or [table.child] header appears outside our fence — the operator’s own entry.
Called with mcp_servers.memhtml, so it also catches [mcp_servers.memhtml.env] written by hand
without its parent, which is legal TOML and would still collide with the block we render.
Parameters
Section titled “Parameters”string | null
string
Returns
Section titled “Returns”boolean
hookCommand()
Section titled “hookCommand()”function hookCommand( binary, bare, event, host, opts): CommandLine;One installed hook’s command line: the CLI, then hook <event> --host <host> --repo <root>.
--repo is always written even though the store could come from MEMHTML_ROOT, because a hook is
spawned by the host, not by a shell, and three of the four hosts do not pass a shell environment
through. --trace-root appears only when the caller knows where this host’s transcripts land.
Parameters
Section titled “Parameters”binary
Section titled “binary”boolean
"session-start" | "user-prompt-submit" | "pre-compact" | "session-end"
"claude" | "codex" | "cursor" | "opencode"
Returns
Section titled “Returns”hostSpec()
Section titled “hostSpec()”function hostSpec( host, scope, root): HostSpec;The plan for one host at one scope.
root is $HOME for user and the git top level for project, and nothing here reads the other
one: a project install is entirely inside the repository, which is what lets it be committed.
Parameters
Section titled “Parameters”"claude" | "codex" | "cursor" | "opencode"
"user" | "project"
string
Returns
Section titled “Returns”inspectReceiptFile()
Section titled “inspectReceiptFile()”function inspectReceiptFile(path): Promise<ReceiptFileInspection>;Absent, valid, or invalid-with-a-reason. The only reader that can tell those three apart.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Promise<ReceiptFileInspection>
install()
Section titled “install()”function install(options): Promise<InstallReport>;Wire one host at one scope.
The order is the contract, and each step exists because skipping it produced a real failure mode: validate the root, refuse a symlinked path, re-hash the receipt, refuse an artifact we do not own, then — only then — snapshot, write, and record.
Parameters
Section titled “Parameters”options
Section titled “options”Returns
Section titled “Returns”Promise<InstallReport>
isClaudeImportOnly()
Section titled “isClaudeImportOnly()”function isClaudeImportOnly(text): boolean;True when the file is nothing but Claude Code’s @AGENTS.md import.
Such a CLAUDE.md is a pointer, not a document: the operator has chosen to keep one instruction file
and have Claude Code import it. Writing our block into the pointer would duplicate what AGENTS.md
already carries, so install writes to AGENTS.md and leaves this file untouched — openwiki’s issue
#841, where the installer helpfully appended to the import file and every session then saw the block
twice.
Parameters
Section titled “Parameters”string | null
Returns
Section titled “Returns”boolean
isDirectory()
Section titled “isDirectory()”function isDirectory(path): Promise<boolean>;True when path resolves to a directory. A missing path is false, not an error.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Promise<boolean>
isHookEvent()
Section titled “isHookEvent()”function isHookEvent(value): value is "session-start" | "user-prompt-submit" | "pre-compact" | "session-end";Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”value is “session-start” | “user-prompt-submit” | “pre-compact” | “session-end”
isHostId()
Section titled “isHostId()”function isHostId(value): value is "claude" | "codex" | "cursor" | "opencode";Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”value is “claude” | “codex” | “cursor” | “opencode”
linesOutsideFence()
Section titled “linesOutsideFence()”function linesOutsideFence( text, startMarker, endMarker): readonly string[];The lines of text that sit OUTSIDE the fence. Used by the TOML adapter to tell an entry we own from
one the operator wrote by hand. Malformed markers are tolerated here — this is a read used to build a
refusal message, and it must not throw before the caller can report anything.
Parameters
Section titled “Parameters”string | null
startMarker
Section titled “startMarker”string
endMarker
Section titled “endMarker”string
Returns
Section titled “Returns”readonly string[]
listIntegrations()
Section titled “listIntegrations()”function listIntegrations(home, projectRoot?): Promise<readonly IntegrationRow[]>;Every host at user scope, and every host at project scope when a repository root is given.
HOSTS order throughout, so two runs on one machine read the same way and a diff of the output is a
diff of the states.
Parameters
Section titled “Parameters”string
projectRoot?
Section titled “projectRoot?”string
Returns
Section titled “Returns”Promise<readonly IntegrationRow[]>
locateBinary()
Section titled “locateBinary()”function locateBinary(input): Promise<BinaryLocation>;The binary an install records.
Both entry scripts are realpath-resolved, so a config written through a version manager’s shim names
the file rather than the shim — a shim that moves on the next nvm use leaves a host config pointing
at nothing, and the failure surfaces as an MCP server that will not start.
A missing server script throws a plain Error naming pnpm build rather than returning a partial
location. An install that wired hooks and no MCP entry would be a half-integration whose receipt
claims a server nothing can spawn, which is exactly the state doctor exists to find and which
install should never create.
Parameters
Section titled “Parameters”Returns
Section titled “Returns”Promise<BinaryLocation>
mcpCommand()
Section titled “mcpCommand()”function mcpCommand(binary, bare): CommandLine;The memhtml-mcp stdio server, the command a host’s mcpServers entry spawns.
Parameters
Section titled “Parameters”binary
Section titled “binary”boolean
Returns
Section titled “Returns”opencodeMcpEntry()
Section titled “opencodeMcpEntry()”function opencodeMcpEntry(mcp, env): OpenCodeMcpEntry;OpenCode’s mcp.memhtml: one command array, environment rather than env, explicitly enabled.
Parameters
Section titled “Parameters”Readonly<Record<string, string>>
Returns
Section titled “Returns”parseJsonc()
Section titled “parseJsonc()”function parseJsonc(text): unknown;The whole document as a value, or undefined when it does not parse. Comments are allowed.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”unknown
planInstall()
Section titled “planInstall()”function planInstall(options): Promise<InstallPlan>;Every artifact one install would write, as pure functions, plus the receipt it would replace.
Reads disk in exactly two places, and both are questions a renderer cannot answer: whether this
host is already installed (the receipt, which decides which command lines count as ours), and whether
a project already keeps its instructions in AGENTS.md (which decides where Claude Code’s block
lands).
Parameters
Section titled “Parameters”options
Section titled “options”Returns
Section titled “Returns”Promise<InstallPlan>
promptOf()
Section titled “promptOf()”function promptOf(payload): string | undefined;The prompt text inside a host’s hook payload, for user-prompt-submit. Claude Code and Codex both
send prompt; the OpenCode plugin sends the same key by construction. Absent or blank means the
hook has nothing to search for and prints nothing.
Parameters
Section titled “Parameters”payload
Section titled “payload”unknown
Returns
Section titled “Returns”string | undefined
rcFileFor()
Section titled “rcFileFor()”function rcFileFor(shell, home): string;The rc file to append to, from $SHELL. zsh reads ~/.zshrc for interactive shells and bash reads
~/.bashrc; an unknown or absent $SHELL gets ~/.bashrc, which is the safer guess because bash is
the shell a script inherits.
Parameters
Section titled “Parameters”string | undefined
string
Returns
Section titled “Returns”string
readFencedBlock()
Section titled “readFencedBlock()”function readFencedBlock(text): string | null;The body between the markers, or null when the file carries no block. Throws on malformed markers.
Parameters
Section titled “Parameters”string | null
Returns
Section titled “Returns”string | null
readFencedToml()
Section titled “readFencedToml()”function readFencedToml(text): string | null;The TOML we own, or null when the file carries no fence. Throws on malformed markers.
Parameters
Section titled “Parameters”string | null
Returns
Section titled “Returns”string | null
readJsonPath()
Section titled “readJsonPath()”function readJsonPath(text, segments): unknown;The value at segments, or undefined when the path is absent or the document does not parse. A JSON
null comes back as null, so absent and present-but-null stay distinguishable.
Parameters
Section titled “Parameters”string | null
segments
Section titled “segments”readonly (string | number)[]
Returns
Section titled “Returns”unknown
readReceipt()
Section titled “readReceipt()”function readReceipt(path): Promise<Receipt | null>;The receipt, or null when there is none or it cannot be trusted.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Promise<Receipt | null>
readShellFence()
Section titled “readShellFence()”function readShellFence(text): string | null;The snippet we own, or null when the rc file carries no fence. Throws on malformed markers.
Parameters
Section titled “Parameters”string | null
Returns
Section titled “Returns”string | null
readTextOrNull()
Section titled “readTextOrNull()”function readTextOrNull(path): Promise<string | null>;The file’s text, or null when it does not exist. Any other failure — a directory in the way, a
permission error — throws, because “unreadable” and “absent” lead to different installer decisions.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Promise<string | null>
realpathOrSelf()
Section titled “realpathOrSelf()”function realpathOrSelf(path): Promise<string>;path with every symlink resolved, or path itself when it cannot be resolved (it may not exist).
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Promise<string>
receiptDriftReader()
Section titled “receiptDriftReader()”function receiptDriftReader(receipt): (entry) => Promise<string | null>;Re-hash every receipt claim against disk.
Handed to compareEntries, which is what turns the result into installed or modified. The reader
is built from the receipt alone — its host, scope, root, binary — so an entry the CURRENT plan no
longer renders is still checked, which is what stops an uninstall from deleting a leftover a human has
since edited.
Parameters
Section titled “Parameters”receipt
Section titled “receipt”Returns
Section titled “Returns”(entry) => Promise<string | null>
receiptPath()
Section titled “receiptPath()”function receiptPath( scope, root, host): string;User scope hides the receipt under .config; project scope keeps it visible at the repo root.
Parameters
Section titled “Parameters”"user" | "project"
string
"claude" | "codex" | "cursor" | "opencode"
Returns
Section titled “Returns”string
removeArrayEntries()
Section titled “removeArrayEntries()”function removeArrayEntries( text, segments, owned): string;Drop every entry owned matches from the array at segments, and drop the key itself once nothing is
left — an uninstall that leaves "SessionStart": [] behind has not restored the file.
Parameters
Section titled “Parameters”string | null
segments
Section titled “segments”readonly (string | number)[]
(entry) => boolean
Returns
Section titled “Returns”string
removeFencedBlock()
Section titled “removeFencedBlock()”function removeFencedBlock(text): string;Drop the managed block. Returns "" when the block was the entire file, which is the caller’s signal
that it may delete the file instead of leaving an empty one behind.
Parameters
Section titled “Parameters”string | null
Returns
Section titled “Returns”string
removeFencedToml()
Section titled “removeFencedToml()”function removeFencedToml(text): string;Drop the fenced region and the blank line before it. A file that was only the fence becomes "".
Parameters
Section titled “Parameters”string | null
Returns
Section titled “Returns”string
removeJsonPath()
Section titled “removeJsonPath()”function removeJsonPath(text, segments): string;Remove segments. A path that is already absent is a no-op, not an error.
Parameters
Section titled “Parameters”string | null
segments
Section titled “segments”readonly (string | number)[]
Returns
Section titled “Returns”string
removeReceipt()
Section titled “removeReceipt()”function removeReceipt(path): Promise<void>;Delete the receipt. Absent is success: uninstall must be re-runnable.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Promise<void>
removeShellFence()
Section titled “removeShellFence()”function removeShellFence(text): string;Drop the fenced snippet and its separating blank line. "" when the fence was the whole file.
Parameters
Section titled “Parameters”string | null
Returns
Section titled “Returns”string
renderClaudeImportBlock()
Section titled “renderClaudeImportBlock()”function renderClaudeImportBlock(): string;The body for a project CLAUDE.md when the block itself lives in AGENTS.md.
Claude Code does not read AGENTS.md; it reads CLAUDE.md, where a line beginning @ is an IMPORT
(resolved up to four hops deep). A Markdown link would render as text the model may or may not follow,
which is why this is @AGENTS.md and not [AGENTS.md](./AGENTS.md).
Returns
Section titled “Returns”string
renderCursorRules()
Section titled “renderCursorRules()”function renderCursorRules(block): string;The Cursor rules file. Cursor reads .cursor/rules/*.mdc and IGNORES a plain .md, and only
alwaysApply: true puts a rule in front of every request rather than waiting for its description to
match. The description is quoted because it contains a colon-space, which a plain YAML scalar cannot.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
renderHookOutput()
Section titled “renderHookOutput()”function renderHookOutput( host, event, text): string;How each host reads a hook’s stdout, per event, as of the vendor docs read 2026-09-13.
Claude Code: plain stdout enters the model’s context on SessionStart and UserPromptSubmit; the
other events ignore stdout (a PreCompact/SessionEnd hook can only observe). Codex: the same two
events accept hookSpecificOutput.additionalContext JSON, which is also valid Claude Code output, so
Codex gets the JSON form to be safe on both. Cursor: sessionStart returns {additional_context};
beforeSubmitPrompt cannot inject at all, so a Cursor per-prompt hook is never installed and the
dialect returns nothing for it. OpenCode: the generated plugin calls this binary itself and pushes the
text as a message part, so the plugin wants plain text.
An empty text renders as an empty string on every host: a hook that has nothing to add says nothing,
which every host treats as “no change”.
Parameters
Section titled “Parameters”"claude" | "codex" | "cursor" | "opencode"
"session-start" | "user-prompt-submit" | "pre-compact" | "session-end"
string
Returns
Section titled “Returns”string
renderInstructionBlock()
Section titled “renderInstructionBlock()”function renderInstructionBlock(opts): string;The block body: what the store is, how to reach it, the five discipline rules, and where the text came from. Short by construction — under forty lines — because it is prepended to every session on hosts that read their instruction file eagerly.
Parameters
Section titled “Parameters”Returns
Section titled “Returns”string
renderOpenCodePlugin()
Section titled “renderOpenCodePlugin()”function renderOpenCodePlugin( binary, bare, mode, opts): string;Render the plugin for one install.
mode prunes handlers rather than flags them off: all wires the per-message recall, the compaction
context, and the idle indexer; session drops the per-message recall (the compaction and idle
handlers remain, since neither runs per turn); none renders NOTHING — the empty string, so the
installer writes no plugin file at all. The idle handler exists only when a transcript root is known,
because indexing needs something to read.
Parameters
Section titled “Parameters”binary
Section titled “binary”boolean
"all" | "session" | "none"
Returns
Section titled “Returns”string
renderShellSnippet()
Section titled “renderShellSnippet()”function renderShellSnippet(opts): string;Two exports and two comments. The PATH line is guarded with a case on :$PATH: rather than appended
unconditionally, because an rc file is sourced once per shell and a nested shell would otherwise
accumulate the same directory over and over.
Parameters
Section titled “Parameters”Returns
Section titled “Returns”string
renderSkill()
Section titled “renderSkill()”function renderSkill(opts): string;Parameters
Section titled “Parameters”Returns
Section titled “Returns”string
restoreText()
Section titled “restoreText()”function restoreText(snapshot): Promise<void>;Put path back exactly as snapshotText found it. A snapshot of an absent file removes whatever is
there now, so a rolled-back install leaves no file it created — the case that makes a failed install
indistinguishable from one that never ran.
Parameters
Section titled “Parameters”snapshot
Section titled “snapshot”Returns
Section titled “Returns”Promise<void>
sha256()
Section titled “sha256()”function sha256(text): string;Hex SHA-256 of text as UTF-8. Every receipt claim and every drift check is this one function.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
shellJoin()
Section titled “shellJoin()”function shellJoin(line): string;A command line as one shell string. Claude Code, Codex, and Cursor all store a hook as a command
STRING and run it through a shell, so an unquoted space in the binary path is a two-word command
that silently does nothing. The OpenCode plugin gets the argv array instead and never needs this.
Parameters
Section titled “Parameters”Returns
Section titled “Returns”string
shellQuote()
Section titled “shellQuote()”function shellQuote(value): string;One argument, quoted for sh. A single quote cannot be escaped inside single quotes, so it is
spliced as '"'"': close, one double-quoted quote, reopen.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string
snapshotText()
Section titled “snapshotText()”function snapshotText(path): Promise<TextSnapshot>;Capture path’s bytes and permission bits, or the fact that it does not exist.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”Promise<TextSnapshot>
traceRootIn()
Section titled “traceRootIn()”function traceRootIn(text): string | undefined;The transcript root an installed hook names, or undefined when none does.
Read out of the installed command line rather than out of the receipt, because the receipt has no field for it: the hook line IS the record of what install decided, in both spellings it ships — a shell string for the three hosts that store a hook as text, and a JSON argv array inside the OpenCode plugin.
Parameters
Section titled “Parameters”string
Returns
Section titled “Returns”string | undefined
uninstall()
Section titled “uninstall()”function uninstall( host, scope, root): Promise<UninstallReport>;Unwire one host at one scope.
Absent receipt is SUCCESS with state: "not-installed", so the call is re-runnable and a
machine-teardown script does not have to ask first.
Parameters
Section titled “Parameters”"claude" | "codex" | "cursor" | "opencode"
"user" | "project"
string
Returns
Section titled “Returns”Promise<UninstallReport>
upsertArrayEntries()
Section titled “upsertArrayEntries()”function upsertArrayEntries( text, segments, entries, owned): string;Rewrite the array at segments as “everything the operator put there, then everything we put there”.
This is how a hooks array is edited: owned recognizes our own entries by their command string, they
are dropped, and entries is appended. Re-running install is therefore idempotent — the second run
removes exactly what the first added and appends the same thing — and a hook the operator wrote by hand
keeps its position among the survivors AND its own bytes, down to whether it was written on one line.
An existing value that is not an array is treated as absent and replaced, since a hooks.SessionStart
that is an object is not something we can merge into.
Parameters
Section titled “Parameters”string | null
segments
Section titled “segments”readonly (string | number)[]
entries
Section titled “entries”readonly unknown[]
(entry) => boolean
Returns
Section titled “Returns”string
upsertFencedBlock()
Section titled “upsertFencedBlock()”function upsertFencedBlock(text, body): string;Replace the managed block with body, or append it after a blank line. The markers each occupy their
own line so the body is addressable by line number in a diff. Malformed or duplicated markers throw.
Parameters
Section titled “Parameters”string | null
string
Returns
Section titled “Returns”string
upsertFencedToml()
Section titled “upsertFencedToml()”function upsertFencedToml(text, block): string;Replace the fenced region with block, or append one after a blank line. Malformed markers — a start
with no end, a duplicate of either — throw an Error naming which marker is wrong and how many were
found, since a repair guess can delete the operator’s own tables.
Parameters
Section titled “Parameters”string | null
string
Returns
Section titled “Returns”string
upsertJsonPath()
Section titled “upsertJsonPath()”function upsertJsonPath( text, segments, value): string;Set segments to value, creating every missing intermediate object on the way. Comments, key order,
and the existing indentation of untouched lines all survive, and a document that does not parse is left
exactly as it is rather than rewritten into one that does.
Parameters
Section titled “Parameters”string | null
segments
Section titled “segments”readonly (string | number)[]
unknown
Returns
Section titled “Returns”string
upsertShellFence()
Section titled “upsertShellFence()”function upsertShellFence(text, body): string;Replace the fenced snippet with body, or append it after a blank line. Malformed markers throw.
Parameters
Section titled “Parameters”string | null
string
Returns
Section titled “Returns”string
writeReceipt()
Section titled “writeReceipt()”function writeReceipt(path, receipt): Promise<void>;Write the receipt atomically, pretty-printed, so a diff of one reads as a diff of one thing.
Parameters
Section titled “Parameters”string
receipt
Section titled “receipt”Returns
Section titled “Returns”Promise<void>
writeTextAtomic()
Section titled “writeTextAtomic()”function writeTextAtomic( path, text, mode?): Promise<void>;Write text to path so a concurrent reader sees either the old file or the new one: parent
directories are created, the bytes go to a temp sibling, and rename publishes them.
The temp file is a sibling rather than a $TMPDIR entry on purpose — rename across filesystems
fails with EXDEV, and ~/.claude.json and /tmp are routinely on different ones.
With mode omitted an existing file’s permission bits carry over, which matters for the shell rc
files and any config an operator has deliberately tightened. chmod happens on the temp file before
the rename, so the mode is never briefly wrong at the real path, and it is set explicitly rather than
through writeFile’s mode option because that one is masked by the process umask.
Parameters
Section titled “Parameters”string
string
number
Returns
Section titled “Returns”Promise<void>