Skip to content

[coding agents 3/3] Claude Code and Codex in the client: the MCP entry, the Stop hook, and capture - #1693

Draft
edwinyyyu wants to merge 19 commits into
MemMachine:mainfrom
edwinyyyu:feat/coding-agent-capture-main
Draft

edwinyyyu wants to merge 19 commits into
MemMachine:mainfrom
edwinyyyu:feat/coding-agent-capture-main

Conversation

@edwinyyyu

@edwinyyyu edwinyyyu commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

Slice 3 of design/coding_agent_integration.md (#1579), the whole client side: the installer that points Claude Code and Codex at a server's memory tools, the Stop hook it registers, and capture. Directly on main; depends functionally on #1691 (the endpoint it configures) and #1692 (the kinds it writes). It absorbs #1689, the installer alone, which is closed in its favor; the two review comments on #1689's docs are answered by commits here.

The installer

memmachine agent install {claude-code,codex} --server <url> --tenant <name> [--scope user|project] [--dry-run] and memmachine agent disable {claude-code,codex} [--scope ...], in memmachine_client.coding_agent. The MCP entry is <server>/v1/mcp over streamable HTTP with the header X-MemMachine-Tenant: <tenant>, one tenant per human user. The installer makes no request to the server: the tenant is created once with PUT /v1/tenants/{tenant} (#1690), and the docs say so and what the tools and the hook do until then.

  • Claude Code, user scope: runs claude mcp remove --scope user memmachine then claude mcp add --transport http --scope user memmachine <url> --header "X-MemMachine-Tenant: <tenant>" through the claude on PATH (the add refuses a name it already holds, so a reinstall removes first); without one, prints the exact command and exits non-zero. Project scope writes .mcp.json in the current directory, merging with what is there.
  • Codex: writes [mcp_servers.memmachine] (url) and [mcp_servers.memmachine.http_headers] into $CODEX_HOME/config.toml (default ~/.codex/config.toml) or <cwd>/.codex/config.toml, by text, replacing exactly those two tables where they stand or appending them, with the rest of the file preserved; the result is parsed back before it is written, and an entry in a form the editor does not own (inline table, dotted key) is refused rather than duplicated.
  • Every file write backs the file up to <file>.bak first; an unchanged file is not rewritten; --dry-run prints the commands (for Claude Code's user scope, the remove and the add) or a unified diff, and needs no claude.
  • Docs: docs/open_source/coding_agents.mdx, in the navigation, quoting what an expansion side that has run out answers (Nothing earlier: the session starts here., Nothing later: the session ends here.).

Verified, installer

Flag names and file shapes against the current Claude Code MCP documentation and the Codex configuration reference, and empirically against claude 2.1.274 (isolated HOME and CLAUDE_CONFIG_DIR) and codex-cli 0.149.1 (CODEX_HOME on a temp dir): the add command produces the documented entry, a second add fails without the preceding remove, codex mcp get memmachine reports streamable_http with the header, and disable leaves codex mcp list empty. The installer tests cover both agents, both scopes, reinstall, disable, dry run with and without claude, preservation of unrelated content, the backup, and the missing claude.

Two calls to review, installer

  • The client package supports Python 3.10, where tomllib does not exist. The Codex commands refuse on 3.10 with a clear message rather than adding a tomli dependency; the alternative is tomli>=2; python_version < "3.11" in the client's dependencies.
  • Claude Code's user-scope file is written by claude itself, which keeps its own backups, so the installer makes no .bak there.

Capture

  • Transcript readers, claude_code_transcript.py and codex_transcript.py over a shared coding_agent_transcript.py: one event per message, tool call, tool result, or injected passage, in transcript order. Event id is the entry's uuid (a second event of one entry is uuid5(entry uuid, index); Codex records, which carry none, get uuid5(session, record index)); session_id the agent's session (an entry's own when it carries one, so a resumed transcript keeps its events in the session they happened in); source_id claude-code or codex; the author part user or assistant; properties.agent, properties.project (the repository root of cwd, else cwd), tool_name on tool events, parent_session on a subagent's. Blocks are text, tool_call, tool_result, injected with the fields [event memory 5/7] Block kinds for tool calls, tool results, injected text and thinking #1692 registers; injected text is classified by the same markers the in-process reader used (reminder, hook, skill, command, compaction, other). Reasoning is captured under thinking; in every Codex rollout on this machine the reasoning body is encrypted with an empty summary, so those records produce nothing, which the docs say. Codex is read from its rollout JSONL, verified against ten real rollouts and the installed binary; event_msg restatements are skipped.
  • tool_result.output and injected.text are capped at 8192 bytes at a character boundary with a [truncated: N of M bytes] line, since the server stores a tool result as one segment however long and expansion is counted in segments; message text is not capped. Image results are written as [image].
  • The capture client, coding_agent_capture.py: a high-water mark per (agent, session) in ~/.claude/memmachine/capture-state.json or $CODEX_HOME/memmachine/capture-state.json (atomic writes, re-read before each write, 500 sessions kept); batches of at most 200 with requests under one deadline of a few seconds; 200 and 409 event_exists both mean held; on a 409 for a batch, each event is posted on its own in transcript order and the mark advances past each one as it is held, so a resumed or forked transcript that mixes held and new entries loses nothing and a slow link still makes progress on every Stop; any other failure leaves the mark at the last held event; stdout stays empty, since a hook's stdout is injected as context.
  • memmachine agent capture {claude-code,codex} --server --tenant reads the hook's stdin (session_id, transcript_path, cwd), with --transcript and --session-id for use by hand. install registers the Stop hook (~/.claude/settings.json or .claude/settings.json; $CODEX_HOME/hooks.json or .codex/hooks.json) as <absolute interpreter> -m memmachine_client.cli agent capture ..., since a hook's shell need not see the client's environment, idempotently and with backups; disable removes exactly that handler. Docs page extended.

Verified, capture

The hook command run under /bin/sh -c with an empty PATH and hook JSON on stdin: exit 0, event posted, mark written, stdout empty. Every event body validated against the server's EventSpec on #1692's branch, which caught an empty tool name on unpairable results (now unknown). 350 client tests, against a real loopback HTTP server: 200, 409, other 409s, 500, timeout, batching, a failed second batch keeping the first's mark, the per-event pass on 409, state per home, the trim, an unreadable state file, by-hand use, stdout empty; transcript fixtures for both agents including subagent, pairing, classification, and re-reads from the mark; installer hook entries for both agents and scopes. Ruff, ty clean on packages/client as CI runs it, and the client suite.

Not verifiable here, stated in the docs

That Codex's Stop hook transcript_path points at the rollout file (its docs call the format unstable; no Codex session has run on this machine since the hooks existed); the by-hand --transcript covers the case where it does not. Subagent entries of Claude Code were not seen in 400 recent transcripts, so that path rests on the documented fields. Codex also reads a [hooks] table in config.toml, which the installer does not write.

Stack

Two stacks, one line of branches. Every PR but #1693 targets feat/horizontal-scaling, so a diff shows everything below it on that branch until that merges; #1693 is client-only, branches from main and targets it. Rebuilt on 2026-09-28: #1684 split into time bounds and sources (#1684) and sessions (#1715), the block kind moved to #1687, and each PR restacked in dependency order. Rebased on 2026-10-01 after #1713 merged, dropping the merge commit that carried it; on 2026-10-06 after #1733 was squash-merged into feat/horizontal-scaling; on 2026-10-07 after that branch took main's #1707, which makes episode uids UUIDs; and on 2026-10-09, after #1736 was squash-merged into feat/horizontal-scaling, onto #1663's head c0a2bbd, and the same day onto fb38a72, after #1628 moved beneath #1663 and #1813 was squash-merged into feat/horizontal-scaling. #1715 and everything above it stay deferred with the coding-agent features.

Event memory, on #1663:

# PR Base Content
1/7 #1684 #1663 time bounds, sources and expansion (port of #1597 without session)
2/7 #1686 #1684 the write transaction, and the rename to the event memory store (port of #1659)
3/7 #1685 #1686 eviction at ingest (port of #1617)
4/7 #1687 #1685 the block kind column, parameter and record key; context parts; kind-keyed tables (port of #1611)
5/7 #1692 #1687 the capture block kinds: tool_call, tool_result, injected, thinking
6/7 #1715 #1692 sessions: the session column, index and walk, session_ids (deferred)
7/7 #1688 #1715 session blocks and id markers in rendering (port of #1632)

Coding agents, slices of design/coding_agent_integration.md (#1579), on the event memory stack; 3/3 shares no code with the server, so its branch is on main, but it configures the endpoint 2/3 serves and writes the kinds #1692 registers, so it merges after both:

# PR Base Content
1/3 #1690 #1688 the v1 event-memory API: tenants, query, expand, events
2/3 #1691 #1690 memory_query and memory_expand served at /v1/mcp
3/3 #1693 (this PR) main Claude Code and Codex in the client: the MCP entry, the Stop hook, and capture

🤖 Generated with Claude Code

https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE

edwinyyyu and others added 15 commits September 17, 2026 13:41
`memmachine agent install {claude-code,codex} --server <url> --tenant
<name>` points an agent at a MemMachine server's memory tools, and
`memmachine agent disable` takes it back out. Both agents speak MCP over
streamable HTTP with static headers, so one endpoint (`<server>/v1/mcp`)
and one header (`X-MemMachine-Tenant`) serve both, and the tools live on
the server.

Claude Code's user scope belongs to the `claude` executable, so the
installer runs `claude mcp add --transport http --scope user` through it,
preceded by `claude mcp remove` because `add` refuses a name it already
holds; without that executable on PATH the error names the command to run
by hand. Project scope writes `.mcp.json` in the current directory.
Codex gets `[mcp_servers.memmachine]` and its `http_headers` table in
`$CODEX_HOME/config.toml`, or in `<cwd>/.codex/config.toml` for project
scope.

A file this installer writes is parsed before it is edited, backed up to
`<file>.bak`, and left alone when it already holds the entry, so a second
install changes nothing. The Codex tables are replaced or appended as
text and every other line is carried over, and the result is parsed back
before it is written, so a config the installer cannot edit safely (an
inline table, a dotted key) is refused rather than duplicated. Reading
TOML needs Python 3.11's tomllib, so on 3.10 the Codex commands say that
instead of guessing.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The tests cover what the installer promises: the exact `claude mcp add`
and `claude mcp remove` commands (with the remove that makes a second
install leave one entry), the `.mcp.json` shape, Codex's two tables in
`$CODEX_HOME`, in `~/.codex` and in a project's `.codex`, a second
install that writes nothing, disable that leaves other servers and other
tables in place, `--dry-run` that prints a diff and writes nothing, and
the `<file>.bak` copy of what a write replaced.

A Codex config with comments, other tables and a multi-line string that
holds table headers as text comes back byte for byte after an install and
a disable, which is what the text-level edit has to hold. Installs run
through `cli.main`, so they also hold that the agent commands need no
server URL for the REST client.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The page says what an agent gets (the two MCP tools and the endpoint that
serves them), how to read the markers a result carries and which of them
`memory_expand` takes, how a tenant name is chosen, and the install and
disable steps for Claude Code and Codex in both scopes, with the command
and the file each one writes. It also states what the installer keeps:
one entry however often it runs, a `<file>.bak` beside every file it
writes, and a `--dry-run` that writes nothing.

Capture through the agents' Stop hooks is named as what comes next, and
ambient recall as designed and off, so a reader knows the tools are the
whole recall surface today. The page joins the Open Source group in the
navigation.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
One verb for the operation from EventMemory up: query.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
A coding agent records a session as a JSONL file: Claude Code one entry
per line under `~/.claude/projects`, Codex one record per line in the
rollout file under `$CODEX_HOME/sessions`. Each reader turns the entries
after a byte offset into bodies for `POST /v1/tenants/{tenant}/events`,
one event per message, tool call, tool result and injected passage, in
transcript order.

What holds:

- An event is held under the identity the transcript fixes, so reading
  the same entry twice produces the same event. A Claude Code entry
  carries its own uuid; a second event of that entry is named under it.
  A Codex record carries none, so its event is named under the session
  and the record's index in the file, which is why a mark carries both
  an offset and an index.
- Only `text` blocks reach the search surface, so a message is what a
  query finds and a tool call, its result and injected text stay on the
  timeline, reached by expanding from a message.
- Injected text says where it came from: a hook, a skill, a compaction
  summary, a system reminder or a slash command. Claude Code's own flags
  are read first and the opening of the text after them; Codex's
  developer channel is injected by construction and its tagged user
  passages by their tag.
- A reader never returns a line the agent has not finished writing, and
  it yields entries that carry nothing to remember with their offset, so
  a session that ends in them is not read again.
- A subagent runs in a session of its own, named by the agent id its
  entries carry or after the turn that launched it, with the parent in
  `properties.parent_session`.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
`memmachine agent capture {claude-code,codex} --server <url> --tenant
<name>` reads a hook's `session_id`, `transcript_path` and `cwd` from
standard input, reads the transcript from where the last run left off,
and posts the new events to `<server>/v1/tenants/<tenant>/events`.
`--transcript` and `--session-id` name them by hand instead.

What holds:

- How far a session has been posted is kept under the agent's own
  directory, `~/.claude/memmachine/capture-state.json` for Claude Code
  and `$CODEX_HOME/memmachine/capture-state.json` for Codex. The mark is
  a shortcut, not a record: a batch is stored whole or rejected whole
  under the ids the transcript fixes, so a lost mark costs one batch the
  server answers `event_exists` to, never a second copy of anything.
- A batch holds at most 200 events and no entry is split across two, so
  the mark a batch carries is past every event in it. The mark moves
  after each batch the server holds, so a failure part way through
  repeats only the rest.
- 200 and 409 `event_exists` are both "the server holds it". Anything
  else leaves the mark where it was and answers non-zero, so the next
  `Stop` posts the same entries again. The whole capture runs under a
  budget of five seconds by default, and running out of it is one of
  those failures.
- Nothing is written to standard output: an agent reads a hook's output
  as context, so what happened is reported on standard error.
- Events carry the agent, the project (the repository root of the
  session's directory, or the directory) and, on tool events, the tool's
  name.

`codex_home_directory` and the two argument validators are named for the
capture client to use, which is the only change to the installer here.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
`memmachine agent install` now writes the `Stop` hook that runs capture,
beside the MCP entry it already wrote, and `disable` takes both back out.
Claude Code reads it from `~/.claude/settings.json` for user scope and
`.claude/settings.json` for project scope; Codex reads it from
`$CODEX_HOME/hooks.json` and `<cwd>/.codex/hooks.json`.

What holds:

- Both agents read the same shape, a `hooks` object whose `Stop` key
  holds groups of handlers, so one edit serves both.
- The handler names the interpreter this client is installed in by its
  own path and reaches the client as a module of it, because both agents
  run a handler's command through a shell whose PATH need not hold that
  interpreter.
- A handler is recognized as ours by the capture command in it, whatever
  server, tenant and interpreter it names, and is replaced where it
  stands, so a second install leaves one behind and does not rewrite a
  file that already reads as the edit would leave it.
- Every other hook, every other event and every other key of the file is
  carried over, a write is backed up to `<file>.bak` like the other
  files, and disabling removes our handler and nothing else: a `Stop`
  key or a `hooks` key left holding nothing goes with it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The page said capture was coming; it is here, so it says what capture
records instead: messages, tool calls with their arguments, tool results
and injected text, of which only messages are embedded and the rest is
read by expanding from a message.

It also states the `Stop` hook the installer writes for each agent and
scope, the state file that holds how far a session has been posted and
why deleting it costs one rejected batch rather than a duplicate, and
the command that posts a transcript by name, which is how a session from
before the install reaches memory. "What is coming" now names the
session outline and ambient recall, which are what is left.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The capture's report on standard error read "captured 1 events". It now
agrees with the count it carries.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
A `tool_result` block names a tool, and the server's block takes no
empty name, so a result the reader could not pair with a call would have
been rejected -- and a batch is rejected whole, so that one result would
have held up every event after it for as long as the session ran.

Such a result is now named `unknown`, which says the transcript carried
no name. Claude Code still reads the entries before its mark for the
call's real name first; a Codex output whose call id is in no record of
the file has nowhere else to look.

Checked against the block kinds the server registers: every event body
both readers produce validates as an `EventSpec` and encodes as an
`Event`, over text, tool_call, tool_result and injected blocks.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The server holds a tool result and an injected passage as one segment
each, however long they are, and an expansion step is counted in
segments, so one long result would spend a whole step on itself and
crowd out the turns around it.

Both readers now cap those two blocks at `ONE_SEGMENT_MAX_BYTES`, 8192
bytes of UTF-8, cutting at the character boundary at or before the cap
and writing one line after it, `[truncated: N of M bytes]`, so a reader
of the memory knows the passage goes on and by how much. The marker is
the reader's rather than the agent's, so it follows the cap instead of
eating into it.

A message's text is not capped: it is what a query matches, and it is
already segmented by the server.

The cap and the cut are one constant and one function in a module both
readers hold to, since the two must agree on them.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
A batch is rejected whole, so `event_exists` for a batch says the server
holds one of its events, not that it holds them all. A session resumed
or forked from another repeats entries under the ids they were stored
under, so such a batch mixes events already held with events that are
new, and moving the mark over it lost the new ones.

The batch is now posted again an event at a time, in the order it was
read, where `event_exists` is that one event and says it is held. The
mark moves only once the server holds every event of the batch, so
nothing is stored twice and nothing is lost, and the second pass is paid
for only on the conflict path. A failure part way through that pass
leaves the mark before the whole batch, which the next `Stop` posts
again.

The count reported on standard error is now what the server stored, not
what was sent, so a batch that was mostly held says so.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
The event-by-event pass a conflicting batch falls back to started over
on every failure, so a batch that conflicts over a slow link could run
out of the budget at the same place each turn and never get past it.

Events go in the order the transcript holds them, so once the server
holds one, every event before it is held as well. Each event now carries
the mark that stands once it is held -- the mark past its entry for the
last event that entry produced, and the mark before the entry for the
events ahead of it, since an entry is passed only when every event of it
is held -- and the mark is written where the pass reached, whether it
reached the end or stopped for any reason on the way.

A failure part way through therefore keeps what was held and the next
`Stop` posts only what is left, while an entry whose events are half
held is still read again whole, so nothing is stored twice.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
Ingestion holds everything the session log holds, and no tool the model
can call writes to memory, so what a session remembers is what it did
rather than what it chose to record. Reasoning was the one thing a
transcript carried and capture dropped.

A Claude Code `thinking` block and a Codex `reasoning` record now become
`{"kind": "thinking", "text": ...}` events, on the timeline and off the
search surface like a tool call, capped at `ONE_SEGMENT_MAX_BYTES` with
the marker, in the order the turn held them.

Codex carries its reasoning encrypted: across all 2631 reasoning records
written on this machine, `summary` is empty and `content` is null, so
such a record produces nothing. Only the `summary` and `content` text is
ever posted; `encrypted_content` is opaque rather than text.

The `event_msg` records stay out, since Codex writes each message and
tool call twice, once as the conversation and once for its interface;
the docs name that as the one thing left out and why.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
@edwinyyyu edwinyyyu changed the title [coding agents 5/5] Capture Claude Code and Codex sessions through their Stop hooks [coding agents 3/3] Claude Code and Codex in the client: the MCP entry, the Stop hook, and capture Sep 17, 2026
edwinyyyu and others added 3 commits September 17, 2026 16:39
The installer makes no request to the server, so the page now says
the tenant is created once with PUT /v1/tenants/{tenant} and what
the tools and the hook do until then. An expansion that runs out
answers with a sentence, not with nothing, and the page quotes it.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
A dry run of the user-scope install printed only the add, and failed
outright where no claude executable was on PATH, although it runs
nothing. It now prints the remove and the add it would run, and the
PATH lookup happens only for a real run.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01YBbQgZiCqeoLu83EkbEFHE
@edwinyyyu
edwinyyyu changed the base branch from main to feat/horizontal-scaling October 9, 2026 00:51
@edwinyyyu
edwinyyyu changed the base branch from feat/horizontal-scaling to main October 9, 2026 00:51

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant