--- title: "Connecting a coding agent" type: reference tags: [signpost, docs, reference] proposed_by: tok_bc09b340db294c709763789154d8113d model: kmem-docs-publisher extracted_at: "2026-10-08T22:28:00.944Z" --- # Connecting a coding agent kmem's agent kit is a Claude Code plugin, a portable Agent Skill, three capture hooks that ship disabled, and an stdio bridge for a client that cannot speak remote MCP. This document says how to install each one and what each one costs. The credential is a **connection token** — `Authorization: Bearer sp_…`. Create one in the web app under **Connect**: choose the client on the **MCP** tab, fill in the form, and select **Create connection**. `POST /api/tokens` does the same. [`MCP.md`](./MCP.md) is the tool reference, the measured footprint and the setup for each client. This document is only about the agent kit. [`COMPATIBILITY.md`](./COMPATIBILITY.md) says which clients somebody has driven, and the [user guide](./user-guide/110-connect-agents.md) is the same ground for a person. --- ## The two things every client needs ```sh export KMEM_URL="https://acme.kmem.app" # your organization's origin, no trailing slash export KMEM_TOKEN="sp_..." # a connection token. "Read and propose" or wider to record anything ``` Neither variable has a default. A URL with no token answers `401` on every call. A token with no URL has nowhere to go. The Connect screen and the stdio bridge use the `KMEM_` names. The Claude Code plugin's `.mcp.json` and hooks still read `SIGNPOST_URL` and `SIGNPOST_TOKEN` (`packages/agent-kit/hooks/lib/kit.mjs`). If you use that plugin, set those names too, to the same values. The stdio bridge reads the `KMEM_` names first, then the `SIGNPOST_` names. --- ## Claude Code ### The plugin, in one command ```sh /plugin marketplace add /path/to/kmem/packages/agent-kit /plugin install kmem@kmem ``` Both names come from `packages/agent-kit/.claude-plugin/`: the plugin is `kmem` and the marketplace is `kmem`. That gives you three things at once: | | What it is | Cost | | --- | --- | --- | | **MCP server** | The seven tools at `$KMEM_URL/mcp`, registered as `kmem` | 1,827 tokens of tool definitions per session. `pnpm mcp:footprint` re-measures it | | **Agent Skill** | `skills/kmem/SKILL.md` — when and how to recall and remember | Loaded on demand, not on every turn | | **Hooks** | `SessionStart`, `SessionEnd`, `PostToolUse` | **Nothing. They ship disabled.** | You can add the MCP server without the plugin: ```sh claude mcp add --transport http --scope user acme-kmem "$KMEM_URL/mcp" \ --header "Authorization: Bearer $KMEM_TOKEN" ``` `--scope user` is what the Connect screen prints: without it the server exists only in the directory the command ran in. Check it with `/mcp`, which lists the seven tools. Or run the plugin's own command, `/kmem-setup`, which probes the connection and prints what is missing. ### The skill, without the plugin `SKILL.md` is Anthropic's open Agent Skill format, and it carries nothing that is specific to Claude Code. Copy the directory into any project skill path or personal skill path: ```sh cp -r packages/agent-kit/skills/kmem ~/.claude/skills/ ``` It is a single Markdown file with YAML frontmatter. Any tool that reads that format can use it. ### The hooks **All three ship disabled, and that is the design rather than caution.** An agent that captures everything creates review fatigue, which is worse than an agent that captures nothing. A queue of forty machine-written notes nobody reads is not a knowledge base with a backlog. It is a knowledge base that somebody no longer trusts. To enable them, name the ones you want in one environment variable: ```sh export SIGNPOST_HOOKS=SessionStart # the one most people should start with export SIGNPOST_HOOKS=SessionStart,SessionEnd # and capture where the work got to export SIGNPOST_HOOKS=all # everything, including the commit prompt ``` Unset — the shipped state — means every hook exits `0` and does nothing at all. Not "nothing useful": it makes no network call, it reads no transcript, and it prints nothing. `packages/agent-kit/test/hooks.test.ts` runs each script as a subprocess against a fully configured instance and asserts that stdout is empty, because a hook that is silent and still calls out is not disabled. | Hook | What it does | What it costs | | --- | --- | --- | | `SessionStart` | One `recall` for recent decisions and conventions, injected as context | 1,500 tokens per session at most, plus one `recall` | | `SessionEnd` | Proposes a `state` note that lists the files changed and the commands run | One `remember`. Under a `suggest` scope it becomes a proposal rather than a note | | `PostToolUse` | After the **first** successful `git commit` of a session, asks the model whether a decision is worth a record | Nothing. It makes no network call. It returns a prompt and the model decides | Know three things about `SessionEnd` before you turn it on. It reads the transcript for **tool targets only** — the paths edited and the commands run — and never the prompts, the replies or any tool output. A session summary that quoted the conversation would put whatever somebody pasted into the terminal into a shared knowledge base. It keeps only the first line of a multi-line command, so a heredoc does not become a note. And it proposes nothing at all for a session with fewer than two edits or commands. `PostToolUse` fires once per session rather than once per commit. A session with six commits produces one piece of reasoning worth a record, and six prompts is how a prompt gets ignored. --- ## Other clients ### Clients that sign in ChatGPT, Claude on the web and in the desktop app, and the Gemini web app add kmem by its endpoint and sign in through hosted OAuth ([ADR 0019](./ADR/0019-hosted-oauth.md)). They need no token and no kit. The [Claude page](./user-guide/130-connect-claude.md) of the user guide has the steps. A static `Authorization` header in the Claude desktop app is not a route a person can count on: it needs the third-party `mcp-remote` bridge, which [`MCP.md`](./MCP.md) shows and nobody has run against kmem. ### Anything that speaks remote MCP with a static header Cursor, Codex, OpenCode, OpenClaw, Hermes, DeepSeek Harness, Gemini CLI, Windsurf, Zed and VS Code accept a remote HTTP MCP server with a header. The shape of the file differs. The two values do not. [`MCP.md`](./MCP.md) has the exact stanza per client, and Connect prints it with the token filled in. ```jsonc { "mcpServers": { "acme-kmem": { "type": "http", "url": "https://acme.kmem.app/mcp", "headers": { "Authorization": "Bearer sp_..." } } } } ``` ### Anything that cannot: `npx @signpost/mcp` Some clients support only a stdio server. Some support a remote server but cannot send a static `Authorization` header. For those, the bridge is the adapter: stdio in, one HTTPS POST out, stdio back. ```jsonc { "mcpServers": { "acme-kmem": { "command": "npx", "args": ["-y", "@signpost/mcp"], "env": { "KMEM_URL": "https://acme.kmem.app", "KMEM_TOKEN": "sp_..." } } } } ``` Until the package is published, point `command` at `node` and `args` at `packages/mcp/bin/signpost-mcp.mjs` in a checkout. The bridge has **no dependencies**. The whole of it is `fetch`, `JSON.parse` and a line splitter, because a client spawns it on every start and it holds a credential. It is deliberately not an MCP implementation either: it does not know what a tool is and it keeps no protocol state, so nothing in it can go out of date with the protocol. A misconfiguration prints one sentence to stderr. That sentence names the variable that is missing and where to set it. ### Anything that cannot speak MCP at all Give the agent a Quick Connect URL — `https://acme.kmem.app/s/sp_…` — from the **Quick Connect** tab in Connect. Its setup prompt tells the agent to open the URL and to stop if it cannot. That is the whole mechanism. [`API.md`](./API.md) is the endpoint reference, and [`COMPATIBILITY.md`](./COMPATIBILITY.md) records which chat clients fetch such a link and which refuse it. --- ## What each thing costs | | Cost | | --- | --- | | The MCP tool definitions | 1,827 tokens per session, re-measured by `pnpm mcp:footprint` | | The `SessionStart` hook | One `recall`, plus 1,500 injected tokens at most | | One `recall` | About 1.76 neurons of Cloudflare inference. The cross-encoder is 99.7% of it | | One `remember` | One extra embedding call for the near-duplicate check. A 500-token note is about 0.54 neurons | The duplicate check does **not** retry. The shipped embedder retries three times, with a 100 ms and then a 500 ms backoff. That is right for an index job that must not lose a chunk. It is wrong for advice about a write that is going to happen anyway: it would add 600 ms to every `remember` on an instance whose Workers AI binding was briefly unavailable, and the suggestion it produces is optional. --- ## What an agent is told about writes **On a hosted org, every agent change waits in Review for a person, unless two things hold:** an owner or an admin turned on the org's **Agent writes** setting, and the connection was made with "Read, propose and write". The setting starts off for every org ([ADR 0022](./ADR/0022-agent-writes.md)). With both, a new doc at a free address and an edit to the current version apply; a deletion, a stale or blind edit, a create at a taken address and a doc that replaces a live one still wait. A connection made with "Read and propose" queues everything, a new doc included. The instance policy is `SIGNPOST_WRITE_POLICY: "conflicts"`, the shipped default, and [ADR 0017](./ADR/0017-conflict-only-review.md) says why its queue is for conflicts. The other three values are unchanged: `open` applies every agent write, `review` queues every one, `locked` refuses every one. A self-hosted project has no org switch, so it reads Agent writes as on and the instance policy decides. Every write response carries `write_policy`, `agent_writes` and the clamp's own `because`. Five things narrow what an assistant may do, and the narrowest of them wins: the instance ceiling `SIGNPOST_WRITE_POLICY`, the org's Agent writes switch, the organization or project or note setting, the connection's scope, and the column of the person the connection acts for. [ADR 0016](./ADR/0016-direct-write-by-default.md) composes them in one function, and ADR 0022 adds the switch to it. | Outcome | `remember` returns | Where the note is | | --- | --- | --- | | The write applies | `outcome: "created"` | Live | | The write queues | `outcome: "queued"` with a `queued_as` proposal id, HTTP 202 | In the review queue. **Nowhere else** | | The write is refused | `writes-locked`, with a fix naming the setting | Nowhere. Nothing was written | Under `conflicts` and a write-mode connection, the shape of the change decides which of the first two a write gets (`changeShapeOf` in `services/commit.ts`): a create at a free address and an edit pinned to the current version apply, and a delete, a stale base, an update with no `expected_sha`, and a title that resolves onto an existing doc queue. The skill tells the agent to tell those apart, because the failure it prevents is an agent that reports *"I've written that down"* about a proposal nobody has accepted. **A `writes-locked` refusal arrives as HTTP 200 on the agent doors and as 403 on `/api`**, and that is deliberate rather than an inconsistency. `apps/worker/src/http/errors.ts` argues it at length: the request was well formed and the answer is a refusal, so an agent should read the refusal and carry on. A 4xx from its transport makes an agent conclude that the tool is broken for the rest of the session. The `code` is `writes-locked` either way, and the `fix` names the one setting that changes it. A `remember` may also come back with `possible_duplicates` and a `merge_suggestion`, when something in reach already says close to the same thing. The threshold is a cosine above 0.92, the same one the retrieval pipeline uses to collapse two claims in one pack. It is a suggestion and never a refusal: a near-duplicate is sometimes a second independent source, and a refusal there would lose information to protect against redundancy. --- ## If it does not work | Symptom | Cause | Fix | | --- | --- | --- | | `401` on every call | The connection expired, somebody revoked it, its person left the org, or it was never valid | Create a new connection in Connect, and update `KMEM_TOKEN` (and `SIGNPOST_TOKEN` if the plugin reads it) | | `403` on `remember` but `recall` works | The connection is "Read only" (`read` scope) | Create a connection with "Read and propose" | | Every write queues, from a "Read, propose and write" connection | The org's Agent writes setting is off, so the connection proposes | An owner or an admin turns it on in Org settings. The write result's `agent_writes` says so | | `writes-locked` on `remember` | The organization or the instance refuses assistant writes | An owner or an admin changes the setting the `fix` names. If the instance ceiling refuses it, ask the operator | | `recall` returns claims and the agent ignores them | The skill is not loaded | Run `/plugin` in Claude Code, or copy `SKILL.md` into a skills path | | A hook does nothing | It is disabled, which is the default | `export SIGNPOST_HOOKS=SessionStart` | | A hook still does nothing | `SIGNPOST_URL` or `SIGNPOST_TOKEN` is unset in the shell that launched the client | Each hook still reads those two names. Set them, or set `KMEM_URL` and `KMEM_TOKEN` and also the `SIGNPOST_` names to the same values | | `recall` returns nothing on a corpus you know holds the answer | Part of the index was not searchable | Read `index_health.embedding_coverage` on the pack, and `/health` |