1#!/usr/bin/env python3
2"""Codex SessionStart/SubagentStart hook: load Claude Code's per-project memory."""
3
4import json
5import os
6import re
7import sys
8from pathlib import Path
9
10PROJECTS = Path.home() / ".claude" / "projects"
11MAX_BYTES = 16_384
12
13# Mirrors the memory contract in Claude Code's system prompt so both agents
14# write files the other can read.
15CONTRACT = """\
16# Shared memory (Claude Code + Codex)
17
18Persistent memory for this project lives in `{dir}`, shared with Claude Code.
19`MEMORY.md` is the index, loaded below; read a linked file when it looks relevant.
20
21Each memory is one file holding one fact, with frontmatter:
22
23```markdown
24---
25name: <short-kebab-case-slug>
26description: <one-line summary, used to decide relevance during recall>
27metadata:
28 type: user | feedback | project | reference
29---
30
31<the fact; for feedback/project, follow with **Why:** and **How to apply:** lines. Link related memories with [[their-name]].>
32```
33
34After writing a file, add a one-line pointer to `MEMORY.md`
35(`- [Title](file.md) — hook`); never put memory content in the index. Update an
36existing file instead of duplicating it, and delete memories that turn out
37wrong. Save who the user is, guidance they gave on how to work, and project
38context not derivable from the repo; skip anything the code, history, or
39AGENTS.md already records. Convert relative dates to absolute. Never store secrets.
40"""
41
42
43def memory_dir(cwd: Path) -> Path:
44 # Claude keys memory by the path as opened, while Codex reports cwd with
45 # symlinks resolved; $PWD keeps the path as typed.
46 roots = [Path(os.environ.get("PWD", cwd)), cwd]
47 dirs = [
48 PROJECTS / re.sub(r"[^a-zA-Z0-9_-]", "-", str(path)) / "memory"
49 for root in roots
50 if root.resolve() == cwd.resolve()
51 for path in (root, *root.parents)
52 ]
53 return next((d for d in dirs if (d / "MEMORY.md").is_file()), dirs[0])
54
55
56def main():
57 event = json.load(sys.stdin)
58 directory = memory_dir(Path(event["cwd"]))
59 context = CONTRACT.format(dir=directory)
60 index = directory / "MEMORY.md"
61 if index.is_file():
62 raw = index.read_bytes()
63 if len(raw) > MAX_BYTES:
64 context += f"\nThe index exceeds {MAX_BYTES} bytes and is truncated; consolidate it before adding more.\n"
65 context += "\n## MEMORY.md\n\n" + raw[:MAX_BYTES].decode(errors="ignore")
66 json.dump(
67 {
68 "hookSpecificOutput": {
69 "hookEventName": event["hook_event_name"],
70 "additionalContext": context,
71 }
72 },
73 sys.stdout,
74 )
75
76
77main()