# MemHub for Claude Code, Codex, and Cursor
MemHub gives coding agents shared team memory: interactive MCP tools for
searching and saving knowledge, automatic session capture, situated directive
recall, and workflows for artifacts, specs, handoffs, rules, and PR review.
The same `memhub` plugin supports Claude Code, OpenAI Codex, and Cursor through
each host's native plugin system.
## What's in here
This repository publishes the `memhub` plugin to all three hosts.
```
.agents/plugins/marketplace.json # Codex and Cursor marketplace
.claude-plugin/marketplace.json # Claude Code marketplace
plugins/memhub/ # production plugin installed by every host
โโโ .claude-plugin/plugin.json # Claude Code manifest
โโโ .codex-plugin/plugin.json # Codex manifest
โโโ .cursor-plugin/plugin.json # Cursor manifest
โโโ plugin.json # Agent Plugins manifest
โโโ .mcp.json # Claude Code MCP configuration
โโโ mcp.json # Codex/Cursor MCP configuration
โโโ hooks/ # host-specific capture and recall hooks
โโโ scripts/ # shared readers, capture, auth, and setup code
โโโ skills/ # model-invoked MemHub workflows
codex/ # legacy forwarding shims and Codex reference guide
```
## Read native sessions locally
The plugin also exposes the existing Codex and Cursor readers as a local JSONL
command. This is useful for discovery or importing history into another local
consumer:
```bash
python3 plugins/memhub/scripts/readers_cli.py --host codex --metadata-only
python3 plugins/memhub/scripts/readers_cli.py --host cursor --since 2026-01-01T00:00:00Z
```
Each session has an identity header followed by canonical records. Metadata-only
mode omits records and prompt-derived titles. Missing or unreadable sources
return an incomplete-coverage error; the command does not upload data or advance
capture cursors. See the [reader stream contract](docs/wire-contract.md) for
fields, filtering and exit codes.
## Install
There are two separate authentications on every host:
1. the **MCP connector login** enables interactive tools such as
`search_memory` and `save_artifact`;
2. **Log in to MemHub** provisions the credential used by background capture
hooks.
Completing one does not complete the other.
### One command, every agent
```bash
curl -fsSL https://raw.githubusercontent.com/XTraceAI/agent-plugins/main/install.sh | sh
```
This installs MemHub into each of Claude Code, Codex and Cursor it finds on
`PATH` (`sh -s -- --agents claude,codex` for a subset), disables any other
enabled MemHub copy in Claude Code so sessions are not captured twice, turns on
Claude Code's marketplace auto-update and its fork agent (`--no-auto-update` /
`--no-fork-agent` leave either as it is), installs the Codex hooks bridge, and
signs the machine in once โ every agent's capture hooks read the same
credential. On a first install it asks two things when a terminal is attached:
where MemHub is active (only repositories you add later with
`/memhub:repos add .`, every repository, or a list now) and whether to show the
companion animal (off by default; a one-line status strip shows instead).
`--selected-repos`, `--all-repos`, `--repos org/a,org/b`, `--companion` and
`--no-companion` answer from the command line (after `sh -s --`); unattended, it
picks selected repositories (none yet) and no animal. It is safe to re-run. It
cannot do the steps
only the host UI can: restarting, trusting the Codex hooks in `/hooks`, and
selecting **Add** in Cursor; it prints those at the end. The per-host steps
below are the same install done by hand.
### Claude Code
```text
/plugin marketplace add XTraceAI/agent-plugins
/plugin install memhub@memhub
/reload-plugins
```
Open `/mcp`, select `memhub`, choose **Authenticate**, and approve in the
browser. Then run `/memhub:login` for capture and `/memhub:onboard` from the
repository whose team brain you want to create or select.
### OpenAI Codex
```bash
codex plugin marketplace add XTraceAI/agent-plugins
codex plugin add memhub@xtrace-plugins
```
Restart Codex after installation. Codex normally starts MCP authentication
during installation; if it still reports that `memhub` is not logged in, run:
```bash
codex mcp login memhub --oauth-client-registration cimd
```
Do **not** manually add a second `memhub` server or OAuth client in
`~/.codex/config.toml`. A global server with the same name shadows the plugin
server and can force an incompatible static OAuth client. See
[`codex/README.md`](codex/README.md) for the complete setup, hook approval, and
recovery steps.
In a new Codex task, ask it to **Log in to MemHub**, then **Set up MemHub**.
Restart, open `/hooks`, and trust only MemHub's `SessionStart`, `PreToolUse`,
`PostToolUse`, and `Stop` handlers from `~/.codex/hooks.json`. Finally ask it to **Onboard MemHub
for this repo**.
> **Both of those steps are required, not optional.** Codex reports
> `plugin_hooks` as `removed` (`codex features list`, verified on 0.146 and
> 0.154), so the `hooks` key in the plugin's own manifest is never dispatched โ
> **Set up MemHub** installs the user-level bridge that is the only path by
> which MemHub hooks run. And an untrusted handler is never dispatched either,
> so a session started before the `/hooks` approval captures nothing. Skip
> either step and capture is silently off, with the plugin otherwise appearing
> installed and healthy.
### Cursor
```bash
cursor-agent plugin marketplace add https://github.com/XTraceAI/agent-plugins
```
Open **Customize**, find **MemHub** in the XTrace marketplace, and select
**Add**. In the MemHub plugin details, authenticate the MCP entry when it says
it needs attention. Then ask Cursor Agent to **Log in to MemHub** for capture
and **Onboard MemHub for this repo**.
Cursor's hooks are observational โ `afterShellExecution` defines no reply the
agent can see โ so the rulebook and the session โ PR link hook do not run
there. Cursor links a session to a pull request with `/memhub:link-pr`.
### Capture credential
The foreground login skill opens a browser once and mints a 90-day personal
access key (`mhk_โฆ`) under `~/.config/memhub-plugin/`. Background hooks use
that key because they cannot use the host application's MCP credential store
or open a browser to refresh an OAuth token. Skipping this step leaves capture
unauthenticated even when interactive MCP tools work.
The Claude Code marketplace pins `memhub` to a released tag. Codex and Cursor
install from the Agent Plugins marketplace snapshot and refresh that snapshot
through their marketplace update flow.
> **Working on MemHub itself?** There's a staging build that points at the
> staging backend. It is not part of this marketplace โ see
> [CONTRIBUTING.md](CONTRIBUTING.md).
## How capture works
Claude Code and Cursor load their bundled hooks natively. Codex currently uses
the user-level compatibility bridge installed by the `setup` skill. All hosts
normalize their native transcript format before sending it to the same
watermark-based ingestion endpoint; native usage counters are included where
the host exposes them.
The detailed lifecycle below describes the Claude Code path. Codex and Cursor
use equivalent host-specific readers and flushers under
`plugins/memhub/scripts/`.
One Codex-specific rule falls out of that:
- **Codex's own threads are not captured.** Codex runs threads alongside
yours โ spawned subagents, guardian action-reviews, memory consolidation โ
and each copies the conversation it is working on, so it looks like a real
transcript and takes its title from the reviewer's prompt. Capture reads
`thread_source` from the rollout header and skips those three kinds by name.
Everything else is yours and is captured: a rollout old enough to predate the
field, and โ deliberately โ a kind we have never seen. Codex's own
`ThreadSource` type is open-ended (`Feature(String)`), so new product
surfaces appear as new values; refusing them by default would silently drop
real sessions, and because the skip advances the capture cursor that loss
would be unrecoverable. An unfamiliar value is captured and noted in the
Codex capture log instead.
Capture runs on independent paths that all feed one server-side watermark
(keyed on `conversation_id` = `session_id`), so re-sending never double-saves:
1. **Per-turn, via the `Stop` hook (primary).** After every assistant turn,
`flush_turn.py` ships only the transcript bytes written since the last
successful flush โ a byte cursor, not a re-send โ to `import_conversation`
with `flush: "auto"`: durable on arrival, but batched into episodes rather
than extracted turn-by-turn (a 2โ5 event fragment would shred episode
boundaries). A cheap prefilter (`turn_flush_prefilter.py`, plain `python3`,
no `uv`) skips the expensive spawn when there's nothing new to send, a
flush is already in flight, or capture is switched off
(`MEMHUB_TURN_FLUSH=0`). The delta is **bounded** before it is sent โ it
goes through `transcript_chunks.slices()`, the same splitter the
whole-transcript paths use, and only the first payload ships, with the
cursor landing on the last record in it. Without that bound, a session whose
pending delta outgrew the server's request limit got a `413` on every turn,
could not advance a cursor past bytes that were never sent, and re-sent the
same ever-growing delta forever โ per-turn capture dead for the rest of that
session's life, on exactly the long sessions worth keeping. A backlog now
drains over the next few turns, because steady-state deltas are kilobytes;
if the server's limit turns out to be lower still the cap halves for that
session, and a single record no payload can carry is stepped over rather
than pinning every turn behind it.
2. **`SessionEnd` (backstop).** Deliberately independent of the per-turn
cursor โ it re-sends the whole transcript-so-far and lets the server's
watermark dedup, so it still captures a session whose per-turn path was
dormant, unauthenticated, or failing all along.
3. **`PostToolUse` on commit/PR.** A precise prefilter (`flush_prefilter.py`)
confirms the just-run Bash command actually performed `git commit`, `gh pr
create`, or `gh pr merge` (not merely mentioned it) before flushing the
transcript-so-far in the background. Commits are semantic work boundaries:
flushing there makes memory available mid-session (parallel sessions see
fresh decisions minutes later) and shapes episodes into work-unit
narratives.
4. **Routing.** Whichever path fires, the server auto-detects the Claude Code
shape and runs the **agentic** extraction path (tool-bearing events, the
agent treated as a valid belief source). The session routes into the
repo's own agent brain via a per-user cache at
`~/.config/memhub-plugin/rooms.json` โ resolved once by `/memhub:onboard`
and read by every writer, capture included; until then, everything lands
in personal memory instead of the repo's room.
5. **Naming.** A captured session is called what its host calls it, so the
sessions list in MemHub reads the same as the one in the editor: Claude
Code's generated title (a rename by the user outranks it), and on Codex the
`thread_name` Codex itself generated โ read from the rollout, or from
`~/.codex/session_index.jsonl` for the hosts that only record it there โ
passed through verbatim. Only a session its host never named falls back to
a title derived from the first prompt, trimmed to one readable line. Codex
re-derives this on every flush, so a thread renamed mid-session updates on
its next turn; Cursor exposes no host-generated name, so its title stays the
one derived from the opening ask.
All of the above authenticate with the plugin's own credential โ separate
from `/mcp`, provisioned by `/memhub:login` (see Install) โ because they run
as cold background processes that can never open a browser.
`SessionStart` also runs `capture_health.py`, a *synchronous* check (the
async flush hooks can't surface anything to the user) that reports via
`systemMessage` when the capture credential has expired or a recent flush
failed; silent on the healthy path so it doesn't become wallpaper.
### Cursor observation history
Cursor's native session files omit some usage and timing information. Hooks
save available usage by generation and timestamp pins locally before waiting
for an upload, so a busy cloud upload does not discard a completed-turn usage
observation. Existing cloud delivery keeps its own upload lock and progress.
Saved usage remains available beyond 512 measured records for later local
reads and manual imports. The state retains one exact sample per measured
record rather than a second transcript archive. Unobserved historical usage
stays unknown; a later read never invents missing measurements.
### Directive recall
Independent of capture: `PreToolUse` (Edit/Write/NotebookEdit/Bash) and
`PostToolUse` (on failure) hooks call `recall_directives` โ the concrete file
path or command about to run (or, on the reactive path, the failing output)
is checked against situated team lessons/procedures, and any hit is injected
as context *before* the agent acts (or right after a failure, when the error
often names the real cause better than the command line did). A client-side
precision gate drops directives whose triggers don't concretely match the
call โ blocking generic filler tokens and the repo's own name, so an
over-broad trigger can't fire on ~every call โ and each directive injects at
most once per session. On Bash, a prefilter (`directive_prefilter.py`) only
recalls for commands that mutate durable state (`git commit`, `rm`, package
installs, migrations, โฆ); read-only commands (`grep`, `cat`, `git show`) skip
the round-trip entirely.
### Session orientation
Also independent of capture: a `SessionStart` hook (`brain_brief.py brief`)
names the repo's default agent brain before the first prompt and renders
three checkpoints under one budget, keyed on identifiers, never on similarity:
- **Map** โ what the brain holds: the top of the compiled overview's Index
(counts and the drill commands), plus a short clip of its prose.
- **Apply** โ lessons and procedures whose triggers intersect the files this
branch touches (`git diff --name-only origin/<default>` plus the last 20
commits' paths), from `recall_directives(entities=โฆ)`. At most five.
- **Recall & Consult** โ the episodes and artifacts that *name* the same
identifiers (paths, `PR #N`, `ENG-N`); a search hit survives only when it
contains one of them as an exact token. At most five.
`brief` is stdlib-only and makes no network call: the map comes from the
overview cache, apply/recall from a pointer cache, and it spawns a detached
`brain_brief.py pointers` child to refresh that cache, which the first
prompt's hook then delivers. (A live call at `SessionStart` would put the
map itself at the mercy of the host's 5 s hook timeout.) A `UserPromptSubmit`
hook (`brain_brief.py prompt`) extracts identifiers from each prompt โ file
paths, symbols that exist in the repo, `PR #N` / `ENG-N`, quoted error strings
โ and fires one bounded recall on them, at most three pointers in 600
characters; a prompt with no identifier costs nothing and prints nothing.
There is deliberately no semantic search on prompt text.
Everything rendered joins the session's served list (shared with
`directive_recall`'s `already_fired`), so nothing is shown twice. The budget
is `MEMHUB_BRIEF_TOKEN_BUDGET` (default 2,500 tokens, chars/4), split 2:1
between the brief and the rulebook's session block; when the brief is cut it
drops Recall pointers first, then Apply, never the map, and ends with
`โฆ trimmed to budget`. `MEMHUB_BRIEF_POINTERS=0` disables the background
worker. The brief only tells the *user*, via `systemMessage`, when the
resolved brain changes; a repo with no cached room stays silent. A companion
`Stop` hook (`brain_brief.py refresh`, async) fetches `get_brain_overview`
into the overview cache, throttled to once per 6 hours.
### Rulebooks
A team rule lives in a **rulebook** โ a container with its own membership, not
a brain and not a document. Whoever is a member of a rulebook has their coding
agent bound by its active rules; a rulebook can bind an explicit list of people
or every member of the org, and one person can be in several (their org's book
plus their team's). Membership is managed in MemHub, not from here: the plugin
reads it, and the only container it ever creates is one you say yes to.
The `SessionStart` / `PreToolUse` / `PostToolUse` hooks
(`scripts/rulebook_hook.py`) cache every book that binds you, per repo, and
inject a matching rule as an advisory at the moment of the call. An edit rule
(`event: edit`) sees a file however it was written: through the Edit/Write
tools before the write, and through a Bash command โ `cat > f <<EOF`, a
`python - <<PY โฆ write_text()`, `sed -i` โ right after it, because the
post-call hook reads what the command left changed on disk (git decides what
is a candidate, the file's mtime decides what that call touched) and runs the
same matcher on it. In auto mode the agent is told to write through Bash, so
without this an edit rule would be silent exactly where it is needed. Two
books can both fire on one call. When they do, the plugin does **not** pick a winner and The repo is
the one the **call** works in โ the edited file's checkout first, the session's
directory second โ so working from a folder that merely *contains* your
checkouts still applies each one's own rules to the calls that touch it, and a
rule scoped to a repo matches from any of its worktrees. A path is followed
only while it stays inside the session's directory, so what the agent edits
can never point the hook at a checkout you did not open. Session-start rules
are the exception, since that event names no file: they resolve from the
session's directory alone, and a session rooted *above* your checkouts still
gets none.
Two books can both fire on one call. When they do, the plugin does **not** pick a winner and
hide the loser โ both fire and both reach the local ledger โ but the per-call
advisory cap and the session-start note budget are spent **widest book first**,
so an org-wide policy is never crowded out by a two-person book's note. Session
start names the books in play when there is more than one; in-flight advisories
stay as short as they were. The server takes no position on any of this: it
puts each rule's book, scope and member count on the wire and the client
decides.
A fire is reported under the same session identity capture used, so the fire
history can show *which session* a rule fired in. That identity is namespaced
per host โ Claude sends the session id bare, Codex and Cursor send
`codex-`/`cursor-`-prefixed, matching what their capture uploads. Before this,
a Codex fire named the bare id while its session was stored prefixed, so every
Codex fire showed as `Not linked yet`. The namespace is applied only on the
wire: local ordering state, obligations and dedup keys still key off the raw
id, so an in-flight session keeps its own state. Fires recorded by an older
plugin keep the id they were written with and stay unlinked โ linking those
retroactively is a server-side change, not a client one.
Authoring (`/memhub:create-rule`, `/memhub:start-rulebook`) resolves which
book a rule lands in before drafting anything โ one visible book is the answer,
several is a question for you, none is an offer to create one that binds only
you. The conflict check spans every book you can see, and flags a collision in
a book you are *not* filing into: nothing you file can supersede that rule, and
both will fire, so it goes to you as a decision.
### Harness-tied memory (flagged off)
With `MEMHUB_HARNESS_EXTRACT=1` in the environment, the plugin helps a
correction you make in a session become a proposed team rule. At each turn's
Stop, a detached child sends a redacted slice of that turn to MemHub
(`POST /v1/team/rulebook/harness/classify`), whose classifier says whether the
moment is worth the agent's attention. At a later turn's Stop, once the agent
has finished your request, the hook **blocks the stop** and hands the flagged
moment to the agent that lived the turn. The agent cannot end the turn without
a verdict: if there is a lesson that would change what an agent does next
time, it runs the create-rule skill on it (which asks you which rulebook,
checks for a twin, and proves the rule), passing the turn's harness stamp;
if there is none, it says so in one line, `No rule from turn N: <why>`. At most
8 stops a session are blocked, and a moment more than 3 turns old is dropped. A
moment from the session's last turn is never handed. The rule lands
`proposed` for a person to activate; nothing fires from it, and the plugin
never activates one. The slice is redacted before it leaves the machine
(MemHub keys, home directories, e-mail addresses, command-line credentials,
quoted or not).
With the variable unset, the default, none of this runs.
## Skills
Sixteen skills ship in `plugins/memhub/skills/` (the deprecated `commands/`
format is gone; invocation is unchanged). Each is both user-invocable as
`/memhub:<name>` and **model-invocable**: saying "save this spec to memhub" or
"what did we decide about X?" in plain language triggers the right skill.
- `/memhub:companion [on | off | status]` โ turns on the companion, the
pixel animal in the band above the prompt that sleeps while idle, watches
while Claude works, and stands up to announce a rule that fired. Enables
the function-hook runtime it needs and persists it; `/hippo` is the
animal's own command.
- `/memhub:setup [--status | --remove]` โ installs, checks, or removes the
Codex user-hooks compatibility bridge; on Claude Code and Cursor it checks
the native integration without writing Codex configuration.
- `/memhub:login [--status | --force]` โ authenticates capture's own
credential (see Install/How capture works): mints or verifies the personal
access key the background hooks use, distinct from `/mcp`'s connector
login. `--status` reports without opening a browser; `--force` discards the
cached credential and redoes the browser flow.
- `/memhub:onboard [folder-or-file ...]` โ connects a repo: resolves or
creates its agent brain, caches the room so automatic capture routes there,
scans the repo for its own markdown documents wherever it keeps them (no
directory layout assumed โ `scripts/onboard_docs.py`), saves the ones that
look important to the brain as artifacts without asking (arguments override
the choice), shows the brain's Index, says what now works (handoff, sharing,
search), and ends with an optional hint that `/memhub:start-rulebook`
creates the team's rulebook. It imports no session: sessions are captured
automatically from the next turn.
- `/memhub:import-session <id-or-path> [title]` โ terminal upload of a past
session transcript; auto-chunks very large sessions. The ONLY skill that
imports: live sessions are captured per turn, so importing is for backfill โ
sessions that predate capture, or ran while it was dormant. It imports under
the session's own id, the same id capture uses, so a session is one
conversation rather than two competing copies.
- `/memhub:save-artifact <file> [name]` โ terminal upload of a file as an
artifact. Both upload skills exist so the model never re-emits file or
transcript content token by token โ a helper script ships the bytes.
- `/memhub:search-memory <query>` โ read-only recall over facts, episodes,
artifacts, and documents, with tag / time filters. In a repo with a
resolved agent brain (named by session orientation, above), searches that
brain first, then repeats the same query without it and merges the
results โ widening to personal memory rather than replacing it, so neither
side goes silently missing.
- `/memhub:handoff-session <teammate> [title]` โ hand the current session to a
teammate: creates an agent brain holding a composed handoff brief (goal,
state, decisions, next steps, gotchas) and shares it read-only via
`share_agent_brain`, alongside the repo room where per-turn capture already
extracted the session. No re-import: the session's memory exists once, and
the brief points into it.
- `/memhub:spec <work|init|revise|bootstrap|check|resolve|status|setup>` โ the
common entry point for the repository's spec workflow; routes to the focused
skills below and preserves existing `init`, `revise`, `check`, and `status` use.
- `/memhub:spec-work <task>` โ reads governing Git or Brain requirements before
implementation, maps them to acceptance checks, and follows through on the
requested code change without silently rewriting conflicting requirements.
- `/memhub:spec-check [file|topic|PR] [--base <ref>]` โ compares requirements
with the actual diff, including branch, staged, unstaged, untracked, renamed,
and deleted paths. Reports evidence and coverage gaps; historical audits are
context, not a substitute for fresh analysis.
- `/memhub:spec-maintain <init|revise|bootstrap|resolve|status|setup>` โ creates
and revises specs, resolves drift, and diagnoses workflow readiness. Bootstrap
offers `--local` (the coding agent's usage) or `--cloud` (MemHub backend usage,
subject to product credits). With neither a choice nor a supported saved
preference, it asks before discovery. Both modes confirm domains before
generation and never silently fall back to the other execution mode.
Git specs live in the configured directory with `owns` frontmatter; their
brain mirrors are read-only. Brain mode reads explicitly selected governing
documents and prepares revision proposals. Publication requires a supported
authenticated capability and a user request; a local draft isn't published.
Cloud bootstrap currently produces Git spec PRs. These skills do not add a
unified backend settings API or imply Brain parity for Git hooks/audits.
- `/memhub:create-rule` โ creates a situated Rulebook rule from a concrete
failure, correction, or procedure, resolves which rulebook it lands in (and
offers to create one when you have none), and checks it for conflicts across
every rulebook you can see before saving it.
- `/memhub:start-rulebook [--starter | --mine] [--days N | --all]` โ where a
team's Rulebook starts. It asks first which you want: **starter rules** (a
tested set every team running a coding agent wants โ irreversible git,
secrets, tests before push, big files read as slices โ fitted to this repo
from a scan of it; about a minute), **rules from your own work** (your
CLAUDE.md and your last 30 days of Claude Code / Codex / Cursor sessions;
10โ20 minutes, because it reads them), or **both**, de-duplicated into one
list. Every candidate is run through the real hook and replayed over your
sessions, and each proposal says why it exists, what it cost you, and what
changes with it on. Older sessions are read only if you ask (`--days N`,
`--all`). Files survivors as `proposed` into the rulebook you pick; never
activates anything. (Was `/memhub:rules-from-sessions`.)
- `/memhub:pr-babysit [pr-number-or-url]` โ usually **auto-armed**, not typed:
a hook offers to start this as a self-paced loop right after `gh pr
create` (see PR babysitting below). One pass polls the PR's review bots and
CI, fixes real findings, and โ once clean โ saves the fixing process to the
repo's agent brain.
- `/memhub:link-pr [pr] [--session <id>] [--unlink]` โ links a coding session
to a pull request in MemHub, so the PR's session context is published from a
confirmed fact rather than a branch-name guess. This is also how a PR opened
by something the hook cannot see โ a script, a CI helper โ gets linked, and
on Cursor it is the only way to record a PR's work type, and only for a PR
this session itself wrote.
- `/memhub:find-contributing-sessions [pr]` โ scans this machine's session
history (Claude Code, Codex, Cursor) for the sessions that wrote a PR's code,
ranks the candidates by the evidence that matched, and links the ones you
approve. It never links anything without an explicit yes, and it never
records a work type โ a scan cannot say what the work was.
## PR babysitting
A `PostToolUse` hook (`pr_babysit_trigger.py`) watches Bash calls for a
successful `gh pr create` โ it only fires once the tool's own stdout actually
contains a PR URL, so a failed create stays silent โ and injects context
telling the agent to start a self-paced `/loop` running `/memhub:pr-babysit
<url>`. A hook can't call an MCP tool or arm a loop itself; it only injects
the instruction, and the agent decides whether to follow it (a user who said
not to babysit PRs, in this session or in memory, is a signal to skip).
Each `/memhub:pr-babysit` pass resolves the PR and the repo's agent brain,
then collects **new** findings since the last pass: review comments from a
bot reviewer (login containing `cursor`/`bugbot` or `codex`/`chatgpt`) and any
failing required check. It triages each โ bots are wrong often enough that "a
bot said so" isn't a reason to change code โ fixes real findings on the PR's
branch (one commit per finding or coherent batch, never force-pushed), and
replies to false positives with a one-line rationale. A pass that pushed
nothing, found nothing new, and has given the bots a review window (an
existing bot review/comment on the head commit, or ~20 minutes since it was
pushed) is clean. The final clean pass composes a **PR review record** โ
findings, verdicts, fix commits or rejection rationale, and any repo-specific
pattern worth remembering โ and saves it as a versioned artifact
(`save_artifact`, stable `name` so a later babysit of the same PR supersedes
rather than competes) into the repo's room, then ends the loop. It never
imports the session transcript: per-turn capture already ships that into the
same room continuously, so babysit only adds the judgment call a transcript
doesn't record โ which findings were real, which were rejected and why.
### Session โ PR linking
After a call that **addresses GitHub** โ `gh pr โฆ`, a `curl` / `gh api` request
to the REST API, or a GitHub MCP tool โ whose output names exactly one pull
request, a `PostToolUse` hook (`pr_link_trigger.py`) asks the backend one
question and injects one instruction. There are three answers:
- the org has no GitHub integration connected โ the agent mentions once, and
only if it isn't intrusive, that connecting GitHub is what links sessions to
the code that shipped;
- **this call ran `gh pr create`** (or the GitHub MCP create tool), and the
command is one where the returned URL provably came from that create โ the
session links itself, unconditionally. Opening a PR is itself work the
session did, so no authorship question is asked; a PR has many sessions and
linking one displaces none;
- **any other GitHub call naming one PR** โ the agent decides. It links only if
it wrote that code in this session, and otherwise offers
`/memhub:find-contributing-sessions`.
**The agent also says what kind of work the PR is.** When it links a pull
request that has no type yet, the same call carries one of `feat`, `fix`,
`chore`, `docs`, `perf`, `refactor` or `other` โ read off the change itself,
never parsed out of the title. MemHub deleted its own title-and-branch
inference, so a pull request nobody labels simply has no type; there is no
hidden backfill.
**The first label is permanent**, and that is why the type is asked for only
when the server reports the PR as unclassified. A second attempt does not just
lose the type: the server refuses the whole write, so the session would not get
linked either. A PR that already carries a type therefore gets the plain link
instruction, and a genuine race is reported rather than retried. A
multi-purpose PR gets its primary purpose; `other` is a legitimate answer and a
better one than a guess.
Unconditional self-linking is deliberately the **narrow** lane. A hand-rolled
`curl -X POST โฆ/pulls` is not treated as a creation: recognising a write meant
parsing the option grammar of four HTTP clients to find a method and a body,
and getting that wrong makes an authorship claim nobody can withdraw. Measured
across 15,134 tool calls from 150 real sessions, that layer decided nothing โ
every genuine creation was a `gh pr create`. So a `curl` POST lands in the
judged lane instead, which is the safe direction.
The same conservatism applies to the shell around the create. A pipeline hides
a failed `gh pr create` โ whose stderr carries the *existing* PR's URL โ so
`gh pr create โฆ 2>&1 | tail -5` declines to the judged lane rather than
claiming authorship. **Heredoc bodies are removed before any of this is
decided**: `--body-file - <<'EOF'` is how essentially every real PR body is
written, and reading that prose as command text used to break both directions
at once โ a body's apostrophes hid the real `gh pr create` from the parser,
while a `python3 - <<'PY'` script that merely *mentioned* the words looked like
one.
So **linking is never automatic for work this session did not do** โ the agent
judges, and offers the finder when the answer is no. The hook is
stateless and holds no per-PR file: a sessionโPR relationship is many-to-many,
and a dedup file keyed on the PR is exactly what would stop a genuinely new
session from linking itself later. Every path degrades to silence โ a
disconnected org, an unreachable server, no credential, a listing command whose
output names several PRs, or a command that merely *mentions* a PR without
addressing GitHub.
The **one** thing it remembers is a negative: an org that has the feature on but
no GitHub connected cannot change that without an admin acting, so that answer
is cached for **30 minutes** rather than re-asked on every `gh pr` command. The
entry is scoped to the deployment, the repo *and* the credential that earned it,
because which org answers depends on which token is resolved. Two replies are
deliberately never cached โ a connected one (`linked_sessions` and `pr.known`
change constantly) and one that just says the feature is off, since the server
answers that from a flag check without looking at the integration at all, so its
`github_connected` field is a default rather than a finding. Caching that field
once silenced linking for a day on machines whose GitHub was connected the whole
time. Set `MEMHUB_PRLINK_NEGATIVE_TTL_S` to change the window; `/memhub:link-pr`
always asks live.
**A PR opened by some other means โ a script, a Makefile target, a CI helper,
`hub pull-request` โ is not detected, deliberately**: recognising arbitrary
programs that happen to open a PR is the automatic-attribution problem this
design walked away from. `/memhub:link-pr` is one command away.
Per-host coverage differs, because the hosts differ:
| Host | `gh pr create` | GitHub MCP tool | Fallback |
|---|---|---|---|
| Claude Code | detected | detected | โ |
| Codex (plugin hooks) | detected | detected | โ |
| Codex (compatibility bridge) | detected | not detected | `/memhub:link-pr` |
| Cursor | not detected | not detected | `/memhub:link-pr` |
Cursor ships skills-only here: its `afterShellExecution` hook has no output
schema at all โ only `beforeShellExecution` can say anything to the agent, and
that fires before the command has produced a PR URL. The Codex split is a
trust one: the plugin-bundled hook manifest is re-fetched on upgrade and costs
nothing to widen, while the compatibility bridge lives in the user's own
`~/.codex/hooks.json` and widening it would require them to re-approve it.
## Spec ownership reminder
Specs are authored in git under `docs/specs/` (or `MEMHUB_SPEC_DIR`). Their
frontmatter `owns` paths connect each spec to code. `/memhub:spec init` and
`revise` edit these files in the same branch as the implementation. Bootstrap
first proposes domains for human confirmation, then opens a spec-only PR.
After an edit, the hook names the owning spec once per session. Before a push
or PR, the Rulebook's `repo.spec_untouched` predicate checks the branch diff,
including working-tree edits, and names specs whose code changed without a
spec edit. Updating the spec records conversion of the reminder. No hook
uploads spec artifacts; backend merges and audits maintain read-only mirrors.
Retired specs and missing/malformed frontmatter do not fire. The legacy
`.claude/artifact-map.json` is no longer read or written.
## Notes & trade-offs
- **Auth is per-user, and split in two.** Nothing secret travels with the
plugin. Each person authenticates the interactive MCP tools themselves via
`/mcp`, and separately authenticates the background hooks via
`/memhub:login` (see Install) โ being connected in one says nothing about
the other. Capture hooks talk to the MCP server directly (their own
credential, their own connection), so they don't go through Claude Code's
per-tool-call permission prompt the way a model-invoked MCP tool call does.
- **Cost.** Per-turn capture ships only the bytes written since the last
flush, so its cost scales with the turn, not the whole session. The
`SessionEnd` backstop still re-sends the full transcript-so-far once per
session (the server's watermark discards whatever per-turn capture already
sent) โ a non-trivial token cost for a very long session, paid once rather
than per turn.
- **Requires** the MemHub server to expose `import_conversation` (capture)
and, for directive recall, `recall_directives`. If your `/mcp` connection
lists both, you're good.
## Configuration
To point at a different MemHub instance, edit `plugins/memhub/.mcp.json`
(`url` and `oauth.clientId`).
## License
Licensed under the [Apache License, Version 2.0](LICENSE). Copyright 2026 XTrace Inc.
The installable MemHub plugin includes its own copies of `LICENSE` and `NOTICE`.