How It Works¶
What Happens Automatically¶
| Event | What memsearch does |
|---|---|
| Session starts | Clean up orphaned processes, start watch (Server) or one-time index (Lite), write session heading, inject recent memories, check for updates |
| Each prompt | Memory-recall skill hint displayed via systemMessage |
| Each turn ends | Conversation summarized via codex exec (async) and saved to daily .md |
Hook Architecture¶
The Codex plugin uses 3 shell hooks (Codex does not have a SessionEnd hook):
| Hook | Type | Async | Timeout | What It Does |
|---|---|---|---|---|
| SessionStart | command | no | 30s | Cleanup orphans, bootstrap memsearch, start watch/index, write session heading, inject memories, display status |
| UserPromptSubmit | command | no | 10s | Return systemMessage hint "[memsearch] Memory available" |
| Stop | command | yes | 30s | Summarize the last turn via codex exec, using a rollout transcript when available and history.jsonl + last_assistant_message otherwise |
Hook Lifecycle¶
stateDiagram-v2
[*] --> SessionStart
SessionStart --> CleanOrphans: cleanup_orphaned_processes()
CleanOrphans --> Bootstrap: detect/install memsearch
Bootstrap --> ConfigSetup: default to ONNX provider
ConfigSetup --> StartWatch: Server mode only
ConfigSetup --> OneTimeIndex: Lite mode (background)
state Prompting {
[*] --> UserInput
UserInput --> Hint: UserPromptSubmit hook
Hint --> CodexProcesses: "[memsearch] Memory available"
CodexProcesses --> MemoryRecall: needs context?
MemoryRecall --> SkillRun: $memory-recall skill
SkillRun --> CodexResponds: search + expand results
CodexProcesses --> CodexResponds: no memory needed
CodexResponds --> UserInput: next turn
CodexResponds --> Capture: Stop hook (async)
Capture --> WriteMD: append to YYYY-MM-DD.md
}
StartWatch --> Prompting
OneTimeIndex --> Prompting
note right of Prompting
No SessionEnd hook.
Orphans cleaned at next SessionStart.
end note
SessionStart -- Bootstrap and Inject¶
The SessionStart hook handles several Codex-specific concerns:
-
Orphan cleanup -- since Codex has no
SessionEndhook, orphanedmemsearch watchandmemsearch indexprocesses from previous sessions are cleaned up here. Also sweeps orphanedmilvus_liteprocesses. -
Bootstrap -- if
memsearchis not found in PATH, the hook auto-installsuvand warms up theuvxcache withmemsearch[onnx]. -
Config setup -- defaults to
onnxprovider if no config file exists (no API key needed). -
Watch vs. one-time index -- detects the Milvus backend:
- Server mode (
http://ortcp://URI): startsmemsearch watchas a persistent background process viasetsid - Lite mode (local
.dbfile): runs a one-timememsearch indexin a background subshell (watch would fail due to Milvus Lite's file lock)
- Server mode (
-
Cold-start injection -- injects memory file count and date range as
additionalContext, with a hint to use$memory-recall. -
Update check -- queries PyPI (2s timeout) and shows update banner if newer version exists.
Milvus Lite Lock Handling¶
Milvus Lite uses a file-level lock that prevents concurrent access. This means memsearch watch (which runs continuously) would block memsearch search (which runs on-demand). The plugin handles this by:
- Not starting watch in Lite mode -- the SessionStart hook detects the URI format and skips
start_watch()for non-HTTP/TCP URIs - Skipping re-index in Stop hook for Lite mode -- the Stop hook only runs
memsearch indexwhen using a Server backend - One-time index at session start -- a single background index run at SessionStart ensures existing memories are searchable
- Dimension mismatch auto-recovery -- if indexing fails with "dimension mismatch" (e.g., after switching embedding providers), the hook auto-resets and re-indexes
For real-time indexing without lock issues, use Milvus Server or Zilliz Cloud.
Stop Hook -- Capture¶
The Stop hook is the core capture mechanism. It runs asynchronously after each Codex response, returning {} immediately so the user can continue working.
graph TD
A[Stop hook fires] --> B{Recursion guard}
B -->|"stop_hook_active=true"| Z[Skip — return empty JSON]
B -->|First call| C{API key available?}
C -->|No| Z
C -->|Yes| D{transcript_path available?}
D -->|Yes| E["parse-rollout.sh<br/>Extract last turn"]
D -->|No| E2["history.jsonl + last_assistant_message<br/>Recover latest user turn"]
E --> F{"codex exec available?"}
E2 --> F
F -->|Yes| G["codex exec --ephemeral<br/>-s read-only -c features.hooks=false<br/>-m gpt-5.1-codex-mini"]
F -->|No| H["Local fallback<br/>Truncate raw text"]
G --> I["Append to YYYY-MM-DD.md<br/>with rollout anchors"]
H --> I
I --> J{Server mode?}
J -->|Yes| K["memsearch index"]
J -->|No (Lite)| L[Skip re-index]
Payload Compatibility¶
Current Codex builds do not reliably provide transcript_path in the Stop hook payload. On current CLI builds the hook receives session_id, turn_id, cwd, model, permission_mode, stop_hook_active, and last_assistant_message, while transcript_path may be null.
The plugin handles both cases:
- If
transcript_pathexists, it parses the rollout withparse-rollout.shand keeps the richer rollout anchor for L3 drill-down. - If
transcript_pathis missing, it falls back to the latest matching user prompt in~/.codex/history.jsonlpluslast_assistant_message.
This keeps memory capture working on current Codex releases, but rollout drill-down is now best-effort rather than guaranteed.
Codex exec Isolation¶
The Stop hook calls codex exec for LLM summarization. To prevent hook recursion (the summarization call triggering another Stop hook), it disables hooks in the child process:
MEMSEARCH_NO_WATCH=1 \
codex exec --ephemeral --skip-git-repo-check -s read-only \
-c features.hooks=false \
-c model_reasoning_effort='"low"' \
-m gpt-5.1-codex-mini "$LLM_PROMPT"
This avoids assuming ~/.codex/auth.json exists. Installations that authenticate through Codex's default keyring flow still work, and the child codex exec cannot recurse because hooks are disabled explicitly.
Set plugins.codex.summarize.model to override only this capture model. Empty or unset keeps the built-in Codex plugin default, and this setting does not fall back to llm.model.
Local Fallback¶
If codex exec is unavailable or returns empty output, the hook falls back to raw text truncation:
# Fallback: use raw user question + Codex response (truncated to 800 chars)
SUMMARY="- User asked: ${USER_QUESTION}
- Codex: ${TRUNCATED_MSG}"
This ensures memory capture works even when the summarization model is unavailable or Codex omits the rollout transcript path.
hooks.json Format¶
Codex CLI uses a hooks.json file (at ~/.codex/hooks.json) to define hook scripts. The installer updates the nested hooks object, replacing only older memsearch Codex entries and preserving unrelated hooks:
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash /path/to/plugins/codex/hooks/session-start.sh",
"timeout": 30
}
]
}
],
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash /path/to/plugins/codex/hooks/user-prompt-submit.sh",
"timeout": 10
}
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "bash /path/to/plugins/codex/hooks/stop.sh",
"timeout": 30
}
]
}
]
}
}
Installer behavior
The installer updates the nested hooks object in ~/.codex/hooks.json, replacing only older memsearch Codex hook entries and preserving unrelated hooks.
Memory Files¶
Example Memory File¶
# 2026-03-25
## Session 10:30
### 10:30
<!-- session:abc123 rollout:~/.codex/sessions/abc123.rollout.jsonl -->
- User asked about database migration strategy for the new preferences feature
- Codex implemented Alembic migration for new user_preferences table with 4 columns
- Added rollback script and tested migration on staging database
- Created index on user_id column for query performance
### 11:15
<!-- session:abc123 rollout:~/.codex/sessions/abc123.rollout.jsonl -->
- User asked to add validation for the preferences API endpoint
- Codex added pydantic models for request/response validation
- Implemented custom validators for preference value types
- Added unit tests covering edge cases (empty values, invalid types)
## Session 15:00
### 15:00
<!-- session:def456 rollout:~/.codex/sessions/def456.rollout.jsonl -->
- User reported 500 error when saving preferences with Unicode characters
- Codex traced issue to missing UTF-8 encoding in the SQLAlchemy column definition
- Fixed by adding `String(collation='utf8mb4_unicode_ci')` to the model
- Added regression test with emoji and CJK characters
When the rollout: anchor is populated, the memory-recall skill can use parse-rollout.sh for L3 drill-down. On current Codex builds the Stop payload may omit transcript_path, so some memories keep only the session: anchor plus the summarized bullets.
Differences from Claude Code Plugin¶
| Aspect | Codex Plugin | Claude Code Plugin |
|---|---|---|
| SessionEnd hook | Not available -- orphans cleaned at next SessionStart | Available -- clean shutdown |
| Summarizer | codex exec -m gpt-5.1-codex-mini |
claude -p --model haiku |
| Recursion prevention | features.hooks=false on child codex exec |
stop_hook_active flag + CLAUDECODE= |
| Skill context | Main context (no context: fork) |
Forked subagent (context: fork) |
| Milvus Lite | One-time index + skip re-index in Stop | Same approach via start_watch() logic |
| Auto-install | Bootstrap installs uv if missing |
Requires pre-installed memsearch |
| hooks.json | Installer updates only memsearch Codex hook entries and preserves unrelated hooks | Part of plugin manifest |
Plugin Files¶
plugins/codex/
├── hooks/
│ ├── common.sh # Shared setup: JSON helpers, process management, orphan cleanup
│ ├── session-start.sh # SessionStart: bootstrap, watch/index, cold-start injection
│ ├── stop.sh # Stop: async capture via codex exec, local fallback
│ └── user-prompt-submit.sh # UserPromptSubmit: memory availability hint
├── skills/
│ └── memory-recall/
│ └── SKILL.md # Memory recall skill ($memory-recall)
└── scripts/
├── derive-collection.sh # Per-project collection name
├── install.sh # One-click installer (skill, hooks.json, feature flag)
└── parse-rollout.sh # Codex rollout JSONL parser for L3 drill-down
| File | Purpose |
|---|---|
common.sh |
Shared library sourced by all hooks. Includes JSON helpers (_json_val, _json_encode_str), memsearch detection, watch/index singleton management, and cleanup_orphaned_processes() for Codex's missing SessionEnd. |
session-start.sh |
Bootstrap memsearch, start watch (Server) or one-time index (Lite), write session heading, inject cold-start context, check for updates. |
stop.sh |
Async capture: summarize via codex exec with hooks disabled, using parse-rollout.sh when Codex provides a rollout path and history.jsonl + last_assistant_message otherwise. Falls back to raw text if codex exec fails. |
user-prompt-submit.sh |
Return lightweight systemMessage hint about memory availability. |
SKILL.md |
Memory recall skill with __INSTALL_DIR__ placeholder (resolved at install time). Includes direct file read fallback for L2 in case memsearch expand hits sandbox restrictions. |
install.sh |
One-click installer: checks/installs memsearch, copies the skill, installs or updates memsearch hook entries in hooks.json, and enables the experimental hooks feature flag. |
parse-rollout.sh |
Parses Codex rollout JSONL files, extracting the last user message through EOF with role labels. |