---
title: @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

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

### 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

- `object` & `YieldableError`\<`this`\>

#### Constructors

##### Constructor

```ts
new IntegrationModified(...args): IntegrationModified;
```

###### Parameters

###### args

...\[`props`: `object`, `options?`: `MakeOptions`\]

###### Returns

[`IntegrationModified`](/api/integrations/#integrationmodified)

###### Inherited from

`Schema.TaggedError<IntegrationModified>()( "IntegrationModified", { host: Schema.String, path: Schema.String, detail: Schema.String } ).constructor`

#### Properties

##### detail

```ts
readonly detail: string = Schema.String;
```

###### Inherited from

`Schema.TaggedError<IntegrationModified>()( "IntegrationModified", { host: Schema.String, path: Schema.String, detail: Schema.String } ).detail`

##### host

```ts
readonly host: string = Schema.String;
```

###### Inherited from

`Schema.TaggedError<IntegrationModified>()( "IntegrationModified", { host: Schema.String, path: Schema.String, detail: Schema.String } ).host`

##### path

```ts
readonly path: string = Schema.String;
```

###### Inherited from

`Schema.TaggedError<IntegrationModified>()( "IntegrationModified", { host: Schema.String, path: Schema.String, detail: Schema.String } ).path`

## Interfaces

### 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

##### cli

```ts
readonly cli: string;
```

The `memhtml` entry script, absolute and realpath-resolved.

##### mcp

```ts
readonly mcp: string;
```

The `memhtml-mcp` entry script, absolute and realpath-resolved.

##### node

```ts
readonly node: string;
```

The node executable, absolute.

##### version

```ts
readonly version: string;
```

The version the binary reports, recorded so doctor can tell an upgrade from a move.

***

### CommandHook

One command handler inside a Claude Code or Codex hook group.

#### Properties

##### command

```ts
readonly command: string;
```

##### timeout

```ts
readonly timeout: number;
```

Seconds. A recall hook shares the turn's latency budget; an indexer does not.

##### type

```ts
readonly type: "command";
```

***

### 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

##### hooks

```ts
readonly hooks: readonly CommandHook[];
```

***

### CommandLine

One command line the host will run: the MCP server, or a hook.

#### Properties

##### args

```ts
readonly args: readonly string[];
```

##### command

```ts
readonly command: string;
```

***

### CursorHookEntry

One entry of a Cursor event array: a bare command string, no group and no timeout field.

#### Properties

##### command

```ts
readonly command: string;
```

***

### DoctorCheck

One row of `integrations doctor`.

#### Properties

##### detail

```ts
readonly detail: string;
```

##### name

```ts
readonly name: string;
```

##### ok

```ts
readonly ok: boolean;
```

##### suggestions

```ts
readonly suggestions: readonly string[];
```

***

### DoctorOptions

#### Properties

##### probes?

```ts
readonly probes?: Partial<DoctorProbes>;
```

Override either probe; the rest fall back to the spawning defaults.

***

### 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

##### handshake

```ts
readonly handshake: (mcp, env) => Promise<ProbeResult>;
```

Complete an MCP `initialize` handshake with the server this command line names.

###### Parameters

###### mcp

[`CommandLine`](/api/integrations/#commandline)

###### env

`Readonly`\<`Record`\<`string`, `string`\>\>

###### Returns

`Promise`\<[`ProbeResult`](/api/integrations/#proberesult)\>

##### version

```ts
readonly version: (cli, env) => Promise<ProbeResult>;
```

Run `<cli> manifest` and report the version it prints.

###### Parameters

###### cli

[`CommandLine`](/api/integrations/#commandline)

###### env

`Readonly`\<`Record`\<`string`, `string`\>\>

###### Returns

`Promise`\<[`ProbeResult`](/api/integrations/#proberesult)\>

***

### DoctorResult

#### Properties

##### checks

```ts
readonly checks: readonly DoctorCheck[];
```

##### healthy

```ts
readonly healthy: boolean;
```

***

### Editor

How one artifact's bytes are read back out of a file and removed from it.

#### Properties

##### ownedNow

```ts
readonly ownedNow: (current) => string | null;
```

###### Parameters

###### current

`string` \| `null`

###### Returns

`string` \| `null`

##### strip

```ts
readonly strip: (current) => string | null;
```

###### Parameters

###### current

`string` \| `null`

###### Returns

`string` \| `null`

***

### EntryComparison

What [compareEntries](/api/integrations/#compareentries) reports: the install state, and each entry whose hash on disk differs from the receipt.

#### Properties

##### drift

```ts
readonly drift: readonly ReceiptDrift[];
```

##### state

```ts
readonly state: "not-installed" | "installed" | "modified";
```

***

### EntryReport

#### Properties

##### action

```ts
readonly action: "created" | "updated" | "unchanged" | "planned" | "removed";
```

##### kind

```ts
readonly kind: "file" | "fragment";
```

##### path

```ts
readonly path: string;
```

##### role

```ts
readonly role: 
  | "mcp-entry"
  | "hooks"
  | "instruction-block"
  | "skill"
  | "plugin"
  | "rules";
```

***

### FencedText

The three operations a fenced file supports, bound to one pair of markers.

#### Properties

##### endMarker

```ts
readonly endMarker: string;
```

##### has

```ts
readonly has: (text) => boolean;
```

True when the file carries a well-formed fence. Throws on malformed markers, like the rest.

###### Parameters

###### text

`string` \| `null`

###### Returns

`boolean`

##### read

```ts
readonly read: (text) => string | null;
```

The body between the markers, or `null` when the file carries no fence.

###### Parameters

###### text

`string` \| `null`

###### Returns

`string` \| `null`

##### remove

```ts
readonly remove: (text) => string;
```

Drop the fenced region and the blank line separating it. `""` when the fence was the whole file.

###### Parameters

###### text

`string` \| `null`

###### Returns

`string`

##### startMarker

```ts
readonly startMarker: string;
```

##### upsert

```ts
readonly upsert: (text, body) => string;
```

Replace the fenced region with `body`, or append a fence when the file has none.

###### Parameters

###### text

`string` \| `null`

###### body

`string`

###### Returns

`string`

***

### HookCommandOptions

What a hook command line needs beyond the event: which store, and where transcripts live.

#### Properties

##### memhtmlRoot

```ts
readonly memhtmlRoot: string;
```

The store the hook reads, passed as the global `--repo` so the hook never depends on cwd or env.

##### traceRoot?

```ts
readonly traceRoot?: string;
```

Where this host writes transcripts, when a format exists for it; absent omits the flag.

***

### HostSpec

Everything install needs to know about one host at one scope. Absolute paths throughout.

#### Properties

##### displayName

```ts
readonly displayName: string;
```

##### hooks

```ts
readonly hooks: HooksTarget;
```

##### host

```ts
readonly host: "claude" | "codex" | "cursor" | "opencode";
```

##### instruction

```ts
readonly instruction: InstructionTarget | null;
```

`null` for Cursor at user scope: it has no user-level instruction file this build writes.

##### mcp

```ts
readonly mcp: McpTarget;
```

##### nextSteps

```ts
readonly nextSteps: readonly string[];
```

The host's manual half: a restart, a trust step, an approval prompt.

##### root

```ts
readonly root: string;
```

`$HOME` at user scope, the git top level at project scope.

##### scope

```ts
readonly scope: "user" | "project";
```

##### skill

```ts
readonly skill: string;
```

The `SKILL.md` install writes, absolute.

***

### InstallOptions

#### Properties

##### bareCommand

```ts
readonly bareCommand: boolean;
```

##### binary

```ts
readonly binary: BinaryLocation;
```

##### dryRun

```ts
readonly dryRun: boolean;
```

##### force

```ts
readonly force: boolean;
```

##### hooks

```ts
readonly hooks: "all" | "session" | "none";
```

##### host

```ts
readonly host: "claude" | "codex" | "cursor" | "opencode";
```

##### memhtmlRoot

```ts
readonly memhtmlRoot: string;
```

The store the integration points at. Absolute.

##### root

```ts
readonly root: string;
```

`$HOME` for user scope, the git top level for project scope. Absolute.

##### scope

```ts
readonly scope: "user" | "project";
```

##### traceRoot?

```ts
readonly traceRoot?: string;
```

Where this host writes transcripts, when known; absent means no indexing hooks.

***

### InstallPlan

#### Properties

##### host

```ts
readonly host: "claude" | "codex" | "cursor" | "opencode";
```

##### memhtmlRoot

```ts
readonly memhtmlRoot: string;
```

##### nextSteps

```ts
readonly nextSteps: readonly string[];
```

##### previous

```ts
readonly previous: Receipt | null;
```

The receipt as it stands, or `null` when this host is not installed at this scope.

##### previousProblem?

```ts
readonly previousProblem?: string;
```

Set when a receipt is present but unreadable, which only `--force` gets past; it is backed up.

##### receiptPath

```ts
readonly receiptPath: string;
```

##### root

```ts
readonly root: string;
```

##### scope

```ts
readonly scope: "user" | "project";
```

##### writes

```ts
readonly writes: readonly PlannedWrite[];
```

***

### InstallReport

#### Properties

##### backups

```ts
readonly backups: readonly string[];
```

Prior bytes kept beside a file `--force` overwrote, absolute.

##### changed

```ts
readonly changed: boolean;
```

False when every managed fragment already matched the plan and the receipt already described it.

##### contents?

```ts
readonly contents?: Readonly<Record<string, string>>;
```

The whole new text of every file, dry run only.

##### dryRun

```ts
readonly dryRun: boolean;
```

##### entries

```ts
readonly entries: readonly EntryReport[];
```

##### host

```ts
readonly host: "claude" | "codex" | "cursor" | "opencode";
```

##### memhtmlRoot

```ts
readonly memhtmlRoot: string;
```

##### nextSteps

```ts
readonly nextSteps: readonly string[];
```

##### receiptPath

```ts
readonly receiptPath: string;
```

##### root

```ts
readonly root: string;
```

##### scope

```ts
readonly scope: "user" | "project";
```

***

### InstructionBlockOptions

#### Properties

##### cli

```ts
readonly cli: CommandLine;
```

How this host invokes the CLI, rendered as the shell string an operator could paste.

##### hasMcp

```ts
readonly hasMcp: boolean;
```

Whether an MCP entry was installed; when false the block names the CLI only.

##### host

```ts
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

```ts
readonly memhtmlRoot: string;
```

The store, absolute, named in the prose so the agent can see which repo it is remembering into.

***

### IntegrationRow

#### Properties

##### detail?

```ts
readonly detail?: string;
```

What is wrong, on a `modified` row: the drifted path, or why the receipt cannot be read.

##### host

```ts
readonly host: "claude" | "codex" | "cursor" | "opencode";
```

##### memhtmlRoot?

```ts
readonly memhtmlRoot?: string;
```

The store it points at, when a receipt says.

##### receiptPath

```ts
readonly receiptPath: string;
```

##### root

```ts
readonly root: string;
```

`$HOME` at user scope, the git top level at project scope.

##### scope

```ts
readonly scope: "user" | "project";
```

##### state

```ts
readonly state: "not-installed" | "installed" | "modified";
```

##### version?

```ts
readonly version?: string;
```

The memhtml version that installed it, when a receipt says.

***

### JsonMcpEntry

What the installer merges into `mcpServers` for Claude Code and Cursor.

#### Properties

##### args?

```ts
readonly args?: readonly string[];
```

Omitted entirely when the command needs no arguments, rather than written as `[]`.

##### command

```ts
readonly command: string;
```

##### env

```ts
readonly env: Readonly<Record<string, string>>;
```

***

### LocateInput

#### Properties

##### entry

```ts
readonly entry: string;
```

The `memhtml` entry script this process was started as, normally `process.argv[1]`.

##### mcpOverride?

```ts
readonly mcpOverride?: string;
```

An explicit `memhtml-mcp` path, for a deployment that does not keep the two bins together.

##### version

```ts
readonly version: string;
```

The version to record, normally the CLI manifest's.

***

### 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

- [`PlannedWrite`](/api/integrations/#plannedwrite)

#### Properties

##### content

```ts
readonly content: string;
```

The bytes install owns: the whole file, or the fragment as rendered.

##### kind

```ts
readonly kind: "file" | "fragment";
```

##### path

```ts
readonly path: string;
```

Absolute path of the file touched.

##### role

```ts
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

What the installer merges into `mcp` for OpenCode.

#### Properties

##### command

```ts
readonly command: readonly string[];
```

OpenCode's one-array form: the command and its arguments together.

##### enabled

```ts
readonly enabled: boolean;
```

##### environment

```ts
readonly environment: Readonly<Record<string, string>>;
```

##### type

```ts
readonly type: "local";
```

***

### PlannedWrite

One artifact, plus the three total functions that make it a transaction step.

#### Extends

- [`ManagedWrite`](/api/integrations/#managedwrite)

#### Properties

##### apply

```ts
readonly apply: (current) => string | null;
```

The whole new file, given its current text (`null` when absent); `null` means the file goes.

###### Parameters

###### current

`string` \| `null`

###### Returns

`string` \| `null`

##### content

```ts
readonly content: string;
```

The bytes install owns: the whole file, or the fragment as rendered.

###### Inherited from

[`ManagedWrite`](/api/integrations/#managedwrite).[`content`](/api/integrations/#managedwrite)

##### kind

```ts
readonly kind: "file" | "fragment";
```

###### Inherited from

[`ManagedWrite`](/api/integrations/#managedwrite).[`kind`](/api/integrations/#managedwrite)

##### ownedNow

```ts
readonly ownedNow: (current) => string | null;
```

The bytes we claim as they are on disk NOW, or `null` when our part is absent.

###### Parameters

###### current

`string` \| `null`

###### Returns

`string` \| `null`

##### path

```ts
readonly path: string;
```

Absolute path of the file touched.

###### Inherited from

[`ManagedWrite`](/api/integrations/#managedwrite).[`path`](/api/integrations/#managedwrite)

##### retire?

```ts
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.

##### role

```ts
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

[`ManagedWrite`](/api/integrations/#managedwrite).[`role`](/api/integrations/#managedwrite)

##### strip

```ts
readonly strip: (current) => string | null;
```

The file with our part removed; `null` when nothing of the caller's would be left.

###### Parameters

###### current

`string` \| `null`

###### Returns

`string` \| `null`

***

### ProbeResult

What one probe answers. `value` is the version a `manifest` probe read, when it read one.

#### Properties

##### detail

```ts
readonly detail: string;
```

##### ok

```ts
readonly ok: boolean;
```

##### value?

```ts
readonly value?: string;
```

***

### 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

##### bareCommand

```ts
readonly bareCommand: boolean;
```

##### binary

```ts
readonly binary: BinaryLocation;
```

##### entries

```ts
readonly entries: readonly ReceiptEntry[];
```

##### hooks

```ts
readonly hooks: "all" | "session" | "none";
```

##### host

```ts
readonly host: "claude" | "codex" | "cursor" | "opencode";
```

##### installedAt

```ts
readonly installedAt: string;
```

##### memhtmlRoot

```ts
readonly memhtmlRoot: string;
```

##### package

```ts
readonly package: "memhtml";
```

##### root

```ts
readonly root: string;
```

##### scope

```ts
readonly scope: "user" | "project";
```

##### version

```ts
readonly version: string;
```

***

### ReceiptDrift

One entry whose bytes on disk no longer hash to what the receipt claims. `null` means gone.

#### Properties

##### actual

```ts
readonly actual: string | null;
```

##### entry

```ts
readonly entry: ReceiptEntry;
```

***

### ReceiptEntry

#### Properties

##### kind

```ts
readonly kind: "file" | "fragment";
```

##### path

```ts
readonly path: string;
```

##### role

```ts
readonly role: 
  | "mcp-entry"
  | "hooks"
  | "instruction-block"
  | "skill"
  | "plugin"
  | "rules";
```

##### sha256

```ts
readonly sha256: string;
```

***

### ReceiptFileInspection

What a receipt file turned out to be. `problem` is present exactly when `state` is `invalid`.

#### Properties

##### problem?

```ts
readonly problem?: string;
```

##### receipt

```ts
readonly receipt: Receipt | null;
```

##### state

```ts
readonly state: "absent" | "valid" | "invalid";
```

***

### ShellSnippetOptions

#### Properties

##### binDir

```ts
readonly binDir: string;
```

The directory holding the `memhtml` and `memhtml-mcp` binaries, absolute.

##### memhtmlRoot

```ts
readonly memhtmlRoot: string;
```

The store, absolute.

***

### SkillOptions

#### Properties

##### cli

```ts
readonly cli: CommandLine;
```

How this host invokes the CLI; every row of the command table is rendered from it.

##### memhtmlRoot

```ts
readonly memhtmlRoot: string;
```

The store, absolute.

***

### TextSnapshot

What a file looked like before install touched it. `text: null` records "there was no file".

#### Properties

##### mode

```ts
readonly mode: number | null;
```

##### path

```ts
readonly path: string;
```

##### text

```ts
readonly text: string | null;
```

***

### UninstallReport

#### Properties

##### changed

```ts
readonly changed: boolean;
```

False when there was nothing to remove, so an uninstall is re-runnable and says so.

##### host

```ts
readonly host: "claude" | "codex" | "cursor" | "opencode";
```

##### receiptPath

```ts
readonly receiptPath: string;
```

##### removed

```ts
readonly removed: readonly EntryReport[];
```

##### root

```ts
readonly root: string;
```

##### scope

```ts
readonly scope: "user" | "project";
```

##### state

```ts
readonly state: "not-installed" | "installed" | "modified";
```

`not-installed` when there was no receipt, `installed` when the removal ran.

## Type Aliases

### EntryAction

```ts
type EntryAction = typeof ENTRY_ACTIONS[number];
```

***

### HookEvent

```ts
type HookEvent = typeof HOOK_EVENTS[number];
```

***

### HookMode

```ts
type HookMode = typeof HOOK_MODES[number];
```

***

### HooksTarget

```ts
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

##### Type Literal

```ts
{
  file: string;
  format: "json-arrays";
  keyPath: ReadonlyArray<string>;
  versionKey?: {
     keyPath: ReadonlyArray<string>;
     value: number;
  };
}
```

###### file

```ts
readonly file: string;
```

###### format

```ts
readonly format: "json-arrays";
```

###### keyPath

```ts
readonly keyPath: ReadonlyArray<string>;
```

###### versionKey?

```ts
readonly versionKey?: object;
```

A scalar the file must carry beside the hooks, when the host versions its hooks file.

###### versionKey.keyPath

```ts
readonly keyPath: ReadonlyArray<string>;
```

###### versionKey.value

```ts
readonly value: number;
```

***

##### Type Literal

```ts
{
  file: string;
  format: "plugin-file";
}
```

***

### HostId

```ts
type HostId = typeof HOSTS[number];
```

***

### InstallState

```ts
type InstallState = typeof INSTALL_STATES[number];
```

***

### InstructionTarget

```ts
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

```ts
type ManagedRole = typeof MANAGED_ROLES[number];
```

***

### McpTarget

```ts
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.

***

### Scope

```ts
type Scope = typeof SCOPES[number];
```

## Variables

### BLOCK\_END

```ts
const BLOCK_END: "<!-- MEMHTML:END -->" = "<!-- MEMHTML:END -->";
```

***

### BLOCK\_START

```ts
const BLOCK_START: "<!-- MEMHTML:START -->" = "<!-- MEMHTML:START -->";
```

***

### CODEX\_EVENT\_NAMES

```ts
const CODEX_EVENT_NAMES: Readonly<Record<HookEvent, string>>;
```

Codex spells its event names the way Claude Code does.

***

### CODEX\_MCP\_TABLE

```ts
const CODEX_MCP_TABLE: "mcp_servers.memhtml";
```

Codex's table name, the one `hasUnfencedTable` refuses a hand-written copy of.

***

### codexOwned

```ts
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

##### entry

`unknown`

##### binary?

[`BinaryLocation`](/api/integrations/#binarylocation)

##### bare?

`boolean`

#### Returns

`boolean`

***

### CURSOR\_HOOKS\_VERSION

```ts
const CURSOR_HOOKS_VERSION: 1 = 1;
```

`hooks.json` for Cursor is versioned; the installer writes this when it creates the file.

***

### cursorOwned

```ts
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

##### entry

`unknown`

##### binary?

[`BinaryLocation`](/api/integrations/#binarylocation)

##### bare?

`boolean`

#### Returns

`boolean`

***

### DEFAULT\_PROBES

```ts
const DEFAULT_PROBES: DoctorProbes;
```

***

### DEFAULT\_TRACE\_ROOT

```ts
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

```ts
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

```ts
const DISPLAY_NAMES: Readonly<Record<HostId, string>>;
```

What each host calls itself, for a report a human reads.

***

### ENTRY\_ACTIONS

```ts
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

```ts
const FENCE_END: "# MEMHTML:END" = "# MEMHTML:END";
```

***

### FENCE\_START

```ts
const FENCE_START: "# MEMHTML:START" = "# MEMHTML:START";
```

***

### HANDSHAKE\_TIMEOUT\_MS

```ts
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

```ts
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

```ts
const HOOK_MODES: readonly ["all", "session", "none"];
```

Which hooks `integrations install` writes.

***

### HOSTS

```ts
const HOSTS: readonly ["claude", "codex", "cursor", "opencode"];
```

The four coding agents this package can wire, in the order the docs present them.

***

### INSTALL\_STATES

```ts
const INSTALL_STATES: readonly ["not-installed", "installed", "modified"];
```

***

### MANAGED\_ROLES

```ts
const MANAGED_ROLES: readonly ["mcp-entry", "hooks", "instruction-block", "skill", "plugin", "rules"];
```

***

### MCP\_CANDIDATES

```ts
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

```ts
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

```ts
const MCP_SERVER_KEY: "memhtml" = "memhtml";
```

The key this entry is stored under in every host's server map.

***

### MCP\_SERVER\_NAME

```ts
const MCP_SERVER_NAME: "memhtml" = "memhtml";
```

The name `serverInfo` must carry for the handshake to be with OUR server.

***

### OPENCODE\_PLUGIN\_FILE

```ts
const OPENCODE_PLUGIN_FILE: "memhtml.js" = "memhtml.js";
```

The file name inside the host's plugins directory.

***

### SCOPES

```ts
const SCOPES: readonly ["user", "project"];
```

`user` anchors at `$HOME`; `project` anchors at a git repository's top level.

***

### SHELL\_FENCE\_END

```ts
const SHELL_FENCE_END: "# MEMHTML:END" = "# MEMHTML:END";
```

***

### SHELL\_FENCE\_START

```ts
const SHELL_FENCE_START: "# MEMHTML:START" = "# MEMHTML:START";
```

***

### SKILL\_DIR\_NAME

```ts
const SKILL_DIR_NAME: "memhtml" = "memhtml";
```

The skill's directory name, which must equal the frontmatter `name`.

***

### spawnHandshake

```ts
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

```ts
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

```ts
const VERSION_TIMEOUT_MS: 10000 = 10_000;
```

How long the version probe may take. `manifest` opens nothing, so this is generous already.

## Functions

### assertNoSymlinkComponents()

```ts
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

##### path

`string`

#### Returns

`Promise`\<`void`\>

***

### claimOf()

```ts
function claimOf(
   receipt, 
   path, 
   role
): ReceiptEntry | undefined;
```

Which entry of a receipt claims this path and role, if any.

#### Parameters

##### receipt

[`Receipt`](/api/integrations/#receipt) \| `null`

##### path

`string`

##### role

  \| `"mcp-entry"`
  \| `"hooks"`
  \| `"instruction-block"`
  \| `"skill"`
  \| `"plugin"`
  \| `"rules"`

#### Returns

[`ReceiptEntry`](/api/integrations/#receiptentry) \| `undefined`

***

### claudeHooks()

```ts
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

##### binary

[`BinaryLocation`](/api/integrations/#binarylocation)

##### bare

`boolean`

##### mode

`"all"` \| `"session"` \| `"none"`

##### opts

[`HookCommandOptions`](/api/integrations/#hookcommandoptions)

#### Returns

`Record`\<`string`, [`CommandHookGroup`](/api/integrations/#commandhookgroup)[]\>

***

### claudeMcpEntry()

```ts
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

##### mcp

[`CommandLine`](/api/integrations/#commandline)

##### env

`Readonly`\<`Record`\<`string`, `string`\>\>

#### Returns

[`JsonMcpEntry`](/api/integrations/#jsonmcpentry)

***

### claudeOwned()

```ts
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

##### entry

`unknown`

##### binary?

[`BinaryLocation`](/api/integrations/#binarylocation)

##### bare?

`boolean`

#### Returns

`boolean`

***

### cliCommand()

```ts
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

##### binary

[`BinaryLocation`](/api/integrations/#binarylocation)

##### bare

`boolean`

#### Returns

[`CommandLine`](/api/integrations/#commandline)

***

### codexHooks()

```ts
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

##### binary

[`BinaryLocation`](/api/integrations/#binarylocation)

##### bare

`boolean`

##### mode

`"all"` \| `"session"` \| `"none"`

##### opts

[`HookCommandOptions`](/api/integrations/#hookcommandoptions)

#### Returns

`Record`\<`string`, [`CommandHookGroup`](/api/integrations/#commandhookgroup)[]\>

***

### codexMcpToml()

```ts
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

##### mcp

[`CommandLine`](/api/integrations/#commandline)

##### env

`Readonly`\<`Record`\<`string`, `string`\>\>

#### Returns

`string`

***

### compareEntries()

```ts
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

##### entries

readonly [`ReceiptEntry`](/api/integrations/#receiptentry)[]

##### current

(`entry`) => `Promise`\<`string` \| `null`\>

#### Returns

`Promise`\<[`EntryComparison`](/api/integrations/#entrycomparison)\>

***

### cursorHooks()

```ts
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

##### binary

[`BinaryLocation`](/api/integrations/#binarylocation)

##### bare

`boolean`

##### mode

`"all"` \| `"session"` \| `"none"`

##### opts

[`HookCommandOptions`](/api/integrations/#hookcommandoptions)

#### Returns

`Record`\<`string`, [`CursorHookEntry`](/api/integrations/#cursorhookentry)[]\>

***

### cursorMcpEntry()

```ts
function cursorMcpEntry(mcp, env): JsonMcpEntry;
```

Cursor's `mcp.json` takes the same object as Claude Code's, key for key.

#### Parameters

##### mcp

[`CommandLine`](/api/integrations/#commandline)

##### env

`Readonly`\<`Record`\<`string`, `string`\>\>

#### Returns

[`JsonMcpEntry`](/api/integrations/#jsonmcpentry)

***

### cwdOf()

```ts
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

##### payload

`unknown`

#### Returns

`string` \| `undefined`

***

### detectHosts()

```ts
function detectHosts(home): Promise<readonly ("claude" | "codex" | "cursor" | "opencode")[]>;
```

Which hosts this machine has, in `HOSTS` order, by [DETECT\_DIRS](/api/integrations/#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

##### home

`string`

#### Returns

`Promise`\<readonly (`"claude"` \| `"codex"` \| `"cursor"` \| `"opencode"`)[]\>

***

### doctor()

```ts
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

##### host

`"claude"` \| `"codex"` \| `"cursor"` \| `"opencode"`

##### scope

`"user"` \| `"project"`

##### root

`string`

##### opts?

[`DoctorOptions`](/api/integrations/#doctoroptions) = `{}`

#### Returns

`Promise`\<[`DoctorResult`](/api/integrations/#doctorresult)\>

***

### editorFor()

```ts
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

##### spec

[`HostSpec`](/api/integrations/#hostspec)

##### entry

`Pick`\<[`ReceiptEntry`](/api/integrations/#receiptentry), `"role"` \| `"kind"`\>

##### binary

[`BinaryLocation`](/api/integrations/#binarylocation)

##### bare

`boolean`

#### Returns

[`Editor`](/api/integrations/#editor)

***

### exists()

```ts
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

##### path

`string`

#### Returns

`Promise`\<`boolean`\>

***

### expandHome()

```ts
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

##### path

`string`

##### home

`string`

#### Returns

`string`

***

### fenced()

```ts
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

##### startMarker

`string`

##### endMarker

`string`

#### Returns

[`FencedText`](/api/integrations/#fencedtext)

***

### hasUnfencedTable()

```ts
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

##### text

`string` \| `null`

##### table

`string`

#### Returns

`boolean`

***

### hookCommand()

```ts
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

##### binary

[`BinaryLocation`](/api/integrations/#binarylocation)

##### bare

`boolean`

##### event

`"session-start"` \| `"user-prompt-submit"` \| `"pre-compact"` \| `"session-end"`

##### host

`"claude"` \| `"codex"` \| `"cursor"` \| `"opencode"`

##### opts

[`HookCommandOptions`](/api/integrations/#hookcommandoptions)

#### Returns

[`CommandLine`](/api/integrations/#commandline)

***

### hostSpec()

```ts
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

##### host

`"claude"` \| `"codex"` \| `"cursor"` \| `"opencode"`

##### scope

`"user"` \| `"project"`

##### root

`string`

#### Returns

[`HostSpec`](/api/integrations/#hostspec)

***

### inspectReceiptFile()

```ts
function inspectReceiptFile(path): Promise<ReceiptFileInspection>;
```

Absent, valid, or invalid-with-a-reason. The only reader that can tell those three apart.

#### Parameters

##### path

`string`

#### Returns

`Promise`\<[`ReceiptFileInspection`](/api/integrations/#receiptfileinspection)\>

***

### install()

```ts
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

##### options

[`InstallOptions`](/api/integrations/#installoptions)

#### Returns

`Promise`\<[`InstallReport`](/api/integrations/#installreport)\>

***

### isClaudeImportOnly()

```ts
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

##### text

`string` \| `null`

#### Returns

`boolean`

***

### isDirectory()

```ts
function isDirectory(path): Promise<boolean>;
```

True when `path` resolves to a directory. A missing path is `false`, not an error.

#### Parameters

##### path

`string`

#### Returns

`Promise`\<`boolean`\>

***

### isHookEvent()

```ts
function isHookEvent(value): value is "session-start" | "user-prompt-submit" | "pre-compact" | "session-end";
```

#### Parameters

##### value

`string`

#### Returns

value is "session-start" \| "user-prompt-submit" \| "pre-compact" \| "session-end"

***

### isHostId()

```ts
function isHostId(value): value is "claude" | "codex" | "cursor" | "opencode";
```

#### Parameters

##### value

`string`

#### Returns

value is "claude" \| "codex" \| "cursor" \| "opencode"

***

### linesOutsideFence()

```ts
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

##### text

`string` \| `null`

##### startMarker

`string`

##### endMarker

`string`

#### Returns

readonly `string`[]

***

### listIntegrations()

```ts
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

##### home

`string`

##### projectRoot?

`string`

#### Returns

`Promise`\<readonly [`IntegrationRow`](/api/integrations/#integrationrow)[]\>

***

### locateBinary()

```ts
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

##### input

[`LocateInput`](/api/integrations/#locateinput)

#### Returns

`Promise`\<[`BinaryLocation`](/api/integrations/#binarylocation)\>

***

### mcpCommand()

```ts
function mcpCommand(binary, bare): CommandLine;
```

The `memhtml-mcp` stdio server, the command a host's `mcpServers` entry spawns.

#### Parameters

##### binary

[`BinaryLocation`](/api/integrations/#binarylocation)

##### bare

`boolean`

#### Returns

[`CommandLine`](/api/integrations/#commandline)

***

### opencodeMcpEntry()

```ts
function opencodeMcpEntry(mcp, env): OpenCodeMcpEntry;
```

OpenCode's `mcp.memhtml`: one command array, `environment` rather than `env`, explicitly enabled.

#### Parameters

##### mcp

[`CommandLine`](/api/integrations/#commandline)

##### env

`Readonly`\<`Record`\<`string`, `string`\>\>

#### Returns

[`OpenCodeMcpEntry`](/api/integrations/#opencodemcpentry)

***

### parseJsonc()

```ts
function parseJsonc(text): unknown;
```

The whole document as a value, or `undefined` when it does not parse. Comments are allowed.

#### Parameters

##### text

`string`

#### Returns

`unknown`

***

### planInstall()

```ts
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

##### options

[`InstallOptions`](/api/integrations/#installoptions)

#### Returns

`Promise`\<[`InstallPlan`](/api/integrations/#installplan)\>

***

### promptOf()

```ts
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

##### payload

`unknown`

#### Returns

`string` \| `undefined`

***

### rcFileFor()

```ts
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

##### shell

`string` \| `undefined`

##### home

`string`

#### Returns

`string`

***

### readFencedBlock()

```ts
function readFencedBlock(text): string | null;
```

The body between the markers, or `null` when the file carries no block. Throws on malformed markers.

#### Parameters

##### text

`string` \| `null`

#### Returns

`string` \| `null`

***

### readFencedToml()

```ts
function readFencedToml(text): string | null;
```

The TOML we own, or `null` when the file carries no fence. Throws on malformed markers.

#### Parameters

##### text

`string` \| `null`

#### Returns

`string` \| `null`

***

### readJsonPath()

```ts
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

##### text

`string` \| `null`

##### segments

readonly (`string` \| `number`)[]

#### Returns

`unknown`

***

### readReceipt()

```ts
function readReceipt(path): Promise<Receipt | null>;
```

The receipt, or `null` when there is none or it cannot be trusted.

#### Parameters

##### path

`string`

#### Returns

`Promise`\<[`Receipt`](/api/integrations/#receipt) \| `null`\>

***

### readShellFence()

```ts
function readShellFence(text): string | null;
```

The snippet we own, or `null` when the rc file carries no fence. Throws on malformed markers.

#### Parameters

##### text

`string` \| `null`

#### Returns

`string` \| `null`

***

### readTextOrNull()

```ts
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

##### path

`string`

#### Returns

`Promise`\<`string` \| `null`\>

***

### realpathOrSelf()

```ts
function realpathOrSelf(path): Promise<string>;
```

`path` with every symlink resolved, or `path` itself when it cannot be resolved (it may not exist).

#### Parameters

##### path

`string`

#### Returns

`Promise`\<`string`\>

***

### receiptDriftReader()

```ts
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

##### receipt

[`Receipt`](/api/integrations/#receipt)

#### Returns

(`entry`) => `Promise`\<`string` \| `null`\>

***

### receiptPath()

```ts
function receiptPath(
   scope, 
   root, 
   host
): string;
```

User scope hides the receipt under `.config`; project scope keeps it visible at the repo root.

#### Parameters

##### scope

`"user"` \| `"project"`

##### root

`string`

##### host

`"claude"` \| `"codex"` \| `"cursor"` \| `"opencode"`

#### Returns

`string`

***

### removeArrayEntries()

```ts
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

##### text

`string` \| `null`

##### segments

readonly (`string` \| `number`)[]

##### owned

(`entry`) => `boolean`

#### Returns

`string`

***

### removeFencedBlock()

```ts
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

##### text

`string` \| `null`

#### Returns

`string`

***

### removeFencedToml()

```ts
function removeFencedToml(text): string;
```

Drop the fenced region and the blank line before it. A file that was only the fence becomes `""`.

#### Parameters

##### text

`string` \| `null`

#### Returns

`string`

***

### removeJsonPath()

```ts
function removeJsonPath(text, segments): string;
```

Remove `segments`. A path that is already absent is a no-op, not an error.

#### Parameters

##### text

`string` \| `null`

##### segments

readonly (`string` \| `number`)[]

#### Returns

`string`

***

### removeReceipt()

```ts
function removeReceipt(path): Promise<void>;
```

Delete the receipt. Absent is success: uninstall must be re-runnable.

#### Parameters

##### path

`string`

#### Returns

`Promise`\<`void`\>

***

### removeShellFence()

```ts
function removeShellFence(text): string;
```

Drop the fenced snippet and its separating blank line. `""` when the fence was the whole file.

#### Parameters

##### text

`string` \| `null`

#### Returns

`string`

***

### renderClaudeImportBlock()

```ts
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

`string`

***

### renderCursorRules()

```ts
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

##### block

`string`

#### Returns

`string`

***

### renderHookOutput()

```ts
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

##### host

`"claude"` \| `"codex"` \| `"cursor"` \| `"opencode"`

##### event

`"session-start"` \| `"user-prompt-submit"` \| `"pre-compact"` \| `"session-end"`

##### text

`string`

#### Returns

`string`

***

### renderInstructionBlock()

```ts
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

##### opts

[`InstructionBlockOptions`](/api/integrations/#instructionblockoptions)

#### Returns

`string`

***

### renderOpenCodePlugin()

```ts
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

##### binary

[`BinaryLocation`](/api/integrations/#binarylocation)

##### bare

`boolean`

##### mode

`"all"` \| `"session"` \| `"none"`

##### opts

[`HookCommandOptions`](/api/integrations/#hookcommandoptions)

#### Returns

`string`

***

### renderShellSnippet()

```ts
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

##### opts

[`ShellSnippetOptions`](/api/integrations/#shellsnippetoptions)

#### Returns

`string`

***

### renderSkill()

```ts
function renderSkill(opts): string;
```

#### Parameters

##### opts

[`SkillOptions`](/api/integrations/#skilloptions)

#### Returns

`string`

***

### restoreText()

```ts
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

##### snapshot

[`TextSnapshot`](/api/integrations/#textsnapshot)

#### Returns

`Promise`\<`void`\>

***

### sha256()

```ts
function sha256(text): string;
```

Hex SHA-256 of `text` as UTF-8. Every receipt claim and every drift check is this one function.

#### Parameters

##### text

`string`

#### Returns

`string`

***

### shellJoin()

```ts
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

##### line

[`CommandLine`](/api/integrations/#commandline)

#### Returns

`string`

***

### shellQuote()

```ts
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

##### value

`string`

#### Returns

`string`

***

### snapshotText()

```ts
function snapshotText(path): Promise<TextSnapshot>;
```

Capture `path`'s bytes and permission bits, or the fact that it does not exist.

#### Parameters

##### path

`string`

#### Returns

`Promise`\<[`TextSnapshot`](/api/integrations/#textsnapshot)\>

***

### traceRootIn()

```ts
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

##### text

`string`

#### Returns

`string` \| `undefined`

***

### uninstall()

```ts
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

##### host

`"claude"` \| `"codex"` \| `"cursor"` \| `"opencode"`

##### scope

`"user"` \| `"project"`

##### root

`string`

#### Returns

`Promise`\<[`UninstallReport`](/api/integrations/#uninstallreport)\>

***

### upsertArrayEntries()

```ts
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

##### text

`string` \| `null`

##### segments

readonly (`string` \| `number`)[]

##### entries

readonly `unknown`[]

##### owned

(`entry`) => `boolean`

#### Returns

`string`

***

### upsertFencedBlock()

```ts
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

##### text

`string` \| `null`

##### body

`string`

#### Returns

`string`

***

### upsertFencedToml()

```ts
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

##### text

`string` \| `null`

##### block

`string`

#### Returns

`string`

***

### upsertJsonPath()

```ts
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

##### text

`string` \| `null`

##### segments

readonly (`string` \| `number`)[]

##### value

`unknown`

#### Returns

`string`

***

### upsertShellFence()

```ts
function upsertShellFence(text, body): string;
```

Replace the fenced snippet with `body`, or append it after a blank line. Malformed markers throw.

#### Parameters

##### text

`string` \| `null`

##### body

`string`

#### Returns

`string`

***

### writeReceipt()

```ts
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

##### path

`string`

##### receipt

[`Receipt`](/api/integrations/#receipt)

#### Returns

`Promise`\<`void`\>

***

### writeTextAtomic()

```ts
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

##### path

`string`

##### text

`string`

##### mode?

`number`

#### Returns

`Promise`\<`void`\>