@suwujs/codex-vault 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env python3
2
+ """Session-start hook for Codex CLI — injects vault context into the agent's prompt.
3
+
4
+ Outputs structured JSON: additionalContext for LLM.
5
+ Banner is written to stderr (Codex CLI does not render systemMessage).
6
+
7
+ Dynamic context: adapts git log window, reads full North Star,
8
+ shows all active work, and includes uncommitted changes.
9
+ """
10
+ import json
11
+ import os
12
+ import re
13
+ import subprocess
14
+ import sys
15
+ from collections import Counter
16
+ from datetime import datetime
17
+ from pathlib import Path
18
+
19
+
20
+ def _find_vault_root():
21
+ """Find vault root from CWD — check for Home.md/brain/, then vault/ subdir."""
22
+ cwd = os.environ.get("CODEX_PROJECT_DIR", os.getcwd())
23
+ if os.path.isfile(os.path.join(cwd, "Home.md")) or os.path.isdir(os.path.join(cwd, "brain")):
24
+ return cwd
25
+ vault_sub = os.path.join(cwd, "vault")
26
+ if os.path.isdir(vault_sub) and (
27
+ os.path.isfile(os.path.join(vault_sub, "Home.md")) or
28
+ os.path.isdir(os.path.join(vault_sub, "brain"))
29
+ ):
30
+ return vault_sub
31
+ return None
32
+
33
+
34
+ def _run_git(args, cwd, timeout=5):
35
+ """Run git command and return stdout lines."""
36
+ try:
37
+ result = subprocess.run(
38
+ ["git"] + args,
39
+ capture_output=True, text=True, cwd=cwd, timeout=timeout,
40
+ )
41
+ return result.stdout.strip().splitlines() if result.stdout.strip() else []
42
+ except Exception:
43
+ return []
44
+
45
+
46
+ def _git_log_oneline(cwd, since=None, max_count=None):
47
+ """Get git log --oneline entries."""
48
+ args = ["log", "--oneline", "--no-merges"]
49
+ if since:
50
+ args.append(f"--since={since}")
51
+ if max_count:
52
+ args.extend(["-n", str(max_count)])
53
+ return _run_git(args, cwd)
54
+
55
+
56
+ def _git_status_short(cwd):
57
+ """Get git status --short output."""
58
+ return _run_git(["status", "--short", "--", "."], cwd)
59
+
60
+
61
+ def _read_file(path):
62
+ """Read file content, return empty string on error."""
63
+ try:
64
+ return Path(path).read_text(encoding="utf-8")
65
+ except Exception:
66
+ return ""
67
+
68
+
69
+ def _find_md_files(vault_dir):
70
+ """Find all .md files in vault, excluding non-vault directories."""
71
+ exclude = {".git", ".obsidian", "thinking", ".claude", ".codex",
72
+ ".codex-vault", ".codex-mem", "node_modules"}
73
+ files = []
74
+ for root, dirs, filenames in os.walk(vault_dir):
75
+ dirs[:] = [d for d in dirs if d not in exclude]
76
+ for f in filenames:
77
+ if f.endswith(".md"):
78
+ rel = os.path.relpath(os.path.join(root, f), vault_dir)
79
+ files.append(f"./{rel}")
80
+ return sorted(files)
81
+
82
+
83
+ def _folder_summary(all_files):
84
+ """Generate folder summary with file counts."""
85
+ folders = Counter()
86
+ for f in all_files:
87
+ parts = f[2:].split("/") # strip ./
88
+ folders[parts[0] if len(parts) > 1 else "."] += 1
89
+ return [f" {folder}/ ({count} files)"
90
+ for folder, count in folders.most_common()]
91
+
92
+
93
+ def _key_files(all_files):
94
+ """Filter for key vault files."""
95
+ pattern = re.compile(
96
+ r"(Home|Index|North Star|Memories|Key Decisions|Patterns|log)\.md$")
97
+ return [f for f in all_files if pattern.search(f)]
98
+
99
+
100
+ def _mtime_ok(vault_dir, rel_path, cutoff):
101
+ """Check if file was modified after cutoff timestamp."""
102
+ try:
103
+ return os.path.getmtime(os.path.join(vault_dir, rel_path.lstrip("./"))) > cutoff
104
+ except Exception:
105
+ return False
106
+
107
+
108
+ def _north_star_goal(vault_dir):
109
+ """Extract first goal from North Star for banner display."""
110
+ ns_path = os.path.join(vault_dir, "brain", "North Star.md")
111
+ if not os.path.isfile(ns_path):
112
+ return None
113
+ content = _read_file(ns_path)
114
+ in_focus = False
115
+ for line in content.splitlines():
116
+ if re.match(r"^## Current Focus", line):
117
+ in_focus = True
118
+ continue
119
+ if in_focus and line.startswith("## "):
120
+ break
121
+ if in_focus and re.match(r"^- .+", line):
122
+ goal = line[2:].strip()
123
+ return goal[:40] if goal else None
124
+ return None
125
+
126
+
127
+ # ── Context builder (→ additionalContext for LLM) ──────────────────────
128
+
129
+
130
+ def _build_context(vault_dir):
131
+ lines = []
132
+ lines.append("## Session Context")
133
+ lines.append("")
134
+
135
+ # Date
136
+ lines.append("### Date")
137
+ now = datetime.now()
138
+ lines.append(f"{now.strftime('%Y-%m-%d')} ({now.strftime('%A')})")
139
+ lines.append("")
140
+
141
+ # North Star — full file
142
+ lines.append("### North Star")
143
+ ns_path = os.path.join(vault_dir, "brain", "North Star.md")
144
+ if os.path.isfile(ns_path):
145
+ lines.append(_read_file(ns_path))
146
+ else:
147
+ lines.append("(No North Star found — create brain/North Star.md to set goals)")
148
+ lines.append("")
149
+
150
+ # Recent changes — adaptive window
151
+ lines.append("### Recent Changes")
152
+ commits_48h = _git_log_oneline(vault_dir, since="48 hours ago")
153
+ if commits_48h:
154
+ lines.append("(last 48 hours)")
155
+ lines.extend(commits_48h[:15])
156
+ else:
157
+ commits_7d = _git_log_oneline(vault_dir, since="7 days ago")
158
+ if commits_7d:
159
+ lines.append("(nothing in 48h — showing last 7 days)")
160
+ lines.extend(commits_7d[:15])
161
+ else:
162
+ lines.append("(nothing recent — showing last 5 commits)")
163
+ commits = _git_log_oneline(vault_dir, max_count=5)
164
+ lines.extend(commits if commits else ["(no git history)"])
165
+ lines.append("")
166
+
167
+ # Recent operations from log.md
168
+ lines.append("### Recent Operations")
169
+ log_path = os.path.join(vault_dir, "log.md")
170
+ if os.path.isfile(log_path):
171
+ entries = [l for l in _read_file(log_path).splitlines()
172
+ if l.startswith("## [")]
173
+ lines.extend(entries[-5:] if entries else ["(no entries in log.md)"])
174
+ else:
175
+ lines.append("(no log.md)")
176
+ lines.append("")
177
+
178
+ # Active work
179
+ lines.append("### Active Work")
180
+ active_dir = os.path.join(vault_dir, "work", "active")
181
+ if os.path.isdir(active_dir):
182
+ work_files = sorted(f for f in os.listdir(active_dir) if f.endswith(".md"))
183
+ if work_files:
184
+ for f in work_files:
185
+ lines.append(f.replace(".md", ""))
186
+ else:
187
+ lines.append("(none)")
188
+ else:
189
+ lines.append("(no work/active/ directory)")
190
+ lines.append("")
191
+
192
+ # Uncommitted changes
193
+ lines.append("### Uncommitted Changes")
194
+ changes = _git_status_short(vault_dir)
195
+ lines.extend(changes[:20] if changes else ["(working tree clean)"])
196
+ lines.append("")
197
+
198
+ # Recently modified brain files
199
+ lines.append("### Recently Modified Brain Files")
200
+ brain_dir = os.path.join(vault_dir, "brain")
201
+ if os.path.isdir(brain_dir):
202
+ cutoff = datetime.now().timestamp() - 7 * 86400
203
+ recent = sorted(
204
+ f.replace(".md", "")
205
+ for f in os.listdir(brain_dir)
206
+ if f.endswith(".md") and os.path.getmtime(os.path.join(brain_dir, f)) > cutoff
207
+ )
208
+ if recent:
209
+ lines.append("(modified in last 7 days)")
210
+ lines.extend(recent)
211
+ else:
212
+ lines.append("(no recent changes)")
213
+ lines.append("")
214
+
215
+ # Vault file listing — tiered
216
+ lines.append("### Vault Files")
217
+ all_files = _find_md_files(vault_dir)
218
+ n = len(all_files)
219
+
220
+ if n <= 20:
221
+ lines.extend(all_files)
222
+
223
+ elif n <= 50:
224
+ hot = [f for f in all_files
225
+ if not re.match(r"\./sources/|\./work/archive/", f)]
226
+ cold = n - len(hot)
227
+ lines.extend(hot)
228
+ if cold > 0:
229
+ lines.append("")
230
+ lines.append(f"(+ {cold} files in sources/ and work/archive/ — use /recall to search)")
231
+
232
+ elif n <= 150:
233
+ lines.append(f"({n} files — showing summary)")
234
+ lines.append("")
235
+ lines.extend(_folder_summary(all_files))
236
+ lines.append("")
237
+ lines.append("Recently modified (7 days):")
238
+ cutoff = datetime.now().timestamp() - 7 * 86400
239
+ recent = [f for f in all_files if _mtime_ok(vault_dir, f, cutoff)]
240
+ lines.extend(recent if recent else [" (none)"])
241
+ lines.append("")
242
+ lines.append("Key files:")
243
+ lines.extend(_key_files(all_files))
244
+
245
+ else:
246
+ lines.append(f"({n} files — showing summary)")
247
+ lines.append("")
248
+ lines.extend(_folder_summary(all_files))
249
+ lines.append("")
250
+ lines.append("Recently modified (3 days):")
251
+ cutoff = datetime.now().timestamp() - 3 * 86400
252
+ recent = [f for f in all_files if _mtime_ok(vault_dir, f, cutoff)]
253
+ lines.extend(recent if recent else [" (none)"])
254
+ lines.append("")
255
+ lines.append("Key files:")
256
+ lines.extend(_key_files(all_files))
257
+ lines.append("")
258
+ lines.append("Use /recall <topic> to search the vault.")
259
+
260
+ return "\n".join(lines)
261
+
262
+
263
+ # ── ANSI colors (oh-my-codex style) ──────────────────────────────────
264
+
265
+ RESET = "\x1b[0m"
266
+ BOLD = "\x1b[1m"
267
+ DIM = "\x1b[2m"
268
+ CYAN = "\x1b[36m"
269
+ GREEN = "\x1b[32m"
270
+ YELLOW = "\x1b[33m"
271
+ SEP = f" {DIM}|{RESET} "
272
+
273
+
274
+ def _c(code, text):
275
+ return f"{code}{text}{RESET}"
276
+
277
+
278
+ # ── Banner builder (→ stderr for terminal) ────────────────────────────
279
+
280
+
281
+ def _build_banner(vault_dir):
282
+ # --- Line 1: statusline ---
283
+ elements = []
284
+
285
+ # Git branch
286
+ branch = _run_git(["rev-parse", "--abbrev-ref", "HEAD"], vault_dir)
287
+ if branch:
288
+ elements.append(_c(CYAN, branch[0]))
289
+
290
+ # North Star goal
291
+ goal = _north_star_goal(vault_dir)
292
+ if goal:
293
+ elements.append(f"\U0001f3af {goal}")
294
+ else:
295
+ elements.append(_c(DIM, "\U0001f3af no goal set"))
296
+
297
+ # Active work
298
+ active_dir = os.path.join(vault_dir, "work", "active")
299
+ work_files = sorted(
300
+ f.replace(".md", "")
301
+ for f in os.listdir(active_dir) if f.endswith(".md")
302
+ ) if os.path.isdir(active_dir) else []
303
+ if work_files:
304
+ names = ", ".join(work_files[:3])
305
+ suffix = f" +{len(work_files) - 3}" if len(work_files) > 3 else ""
306
+ elements.append(_c(GREEN, f"active:{len(work_files)}") + _c(DIM, f" {names}{suffix}"))
307
+ else:
308
+ elements.append(_c(DIM, "active:0"))
309
+
310
+ # Changes
311
+ changes = _git_status_short(vault_dir)
312
+ if changes:
313
+ elements.append(_c(YELLOW, f"changes:{len(changes)}"))
314
+ else:
315
+ elements.append(_c(GREEN, "clean"))
316
+
317
+ label = _c(BOLD, "[Vault]")
318
+ statusline = label + " " + SEP.join(elements)
319
+
320
+ # --- Line 2+: recent commits ---
321
+ lines = ["", statusline]
322
+ commits = _git_log_oneline(vault_dir, since="7 days ago")
323
+ if not commits:
324
+ commits = _git_log_oneline(vault_dir, max_count=3)
325
+ if commits:
326
+ for c in commits[:3]:
327
+ # hash in cyan, message in dim
328
+ parts = c.split(" ", 1)
329
+ if len(parts) == 2:
330
+ lines.append(f" {_c(DIM, parts[0])} {_c(DIM, parts[1])}")
331
+ else:
332
+ lines.append(f" {_c(DIM, c)}")
333
+
334
+ # --- Line: vault file count ---
335
+ all_files = _find_md_files(vault_dir)
336
+ lines.append(_c(DIM, f" {len(all_files)} notes"))
337
+ lines.append("")
338
+
339
+ return "\n".join(lines)
340
+
341
+
342
+ # ── Main ───────────────────────────────────────────────────────────────
343
+
344
+
345
+ def main():
346
+ vault_dir = _find_vault_root()
347
+ if not vault_dir:
348
+ output = {
349
+ "hookSpecificOutput": {
350
+ "hookEventName": "SessionStart",
351
+ "additionalContext": "## Session Context\n\n(No vault found)"
352
+ }
353
+ }
354
+ json.dump(output, sys.stdout)
355
+ sys.exit(0)
356
+
357
+ # Read hook input for session metadata
358
+ try:
359
+ event = json.load(sys.stdin)
360
+ except Exception:
361
+ event = {}
362
+
363
+ context = _build_context(vault_dir)
364
+ banner = _build_banner(vault_dir)
365
+
366
+ # Codex CLI: systemMessage not rendered by TUI,
367
+ # use stderr for terminal visibility (best effort)
368
+ sys.stderr.write(banner + "\n")
369
+
370
+ output = {
371
+ "hookSpecificOutput": {
372
+ "hookEventName": "SessionStart",
373
+ "additionalContext": context
374
+ },
375
+ }
376
+
377
+ json.dump(output, sys.stdout)
378
+ sys.stdout.flush()
379
+ sys.exit(0)
380
+
381
+
382
+ if __name__ == "__main__":
383
+ try:
384
+ main()
385
+ except Exception:
386
+ # Never block session start
387
+ sys.exit(0)
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env python3
2
+ """Post-write validation for vault notes — Codex CLI version.
3
+
4
+ Checks frontmatter and wikilinks on any .md file written to the vault.
5
+ Outputs hookSpecificOutput for Codex CLI. Feedback via stderr
6
+ (Codex CLI does not render systemMessage).
7
+
8
+ Note: Codex CLI PostToolUse only supports Bash matcher, so this script
9
+ will only run when triggered by a Bash tool writing .md files. The
10
+ validation logic is identical to the Claude Code version.
11
+ """
12
+ import json
13
+ import re
14
+ import sys
15
+ import os
16
+ from pathlib import Path
17
+
18
+
19
+ def _check_log_format(content):
20
+ """Validate log.md entry format: ## [YYYY-MM-DD] <type> | <title>"""
21
+ warnings = []
22
+ for i, line in enumerate(content.splitlines(), 1):
23
+ if line.startswith("## ") and not line.startswith("## ["):
24
+ # Heading that looks like a log entry but missing date brackets
25
+ if any(t in line.lower() for t in ["ingest", "session", "query", "maintenance", "decision", "archive"]):
26
+ warnings.append(f"Line {i}: log entry missing date format — expected `## [YYYY-MM-DD] <type> | <title>`")
27
+ elif line.startswith("## ["):
28
+ if not re.match(r"^## \[\d{4}-\d{2}-\d{2}\] \w+", line):
29
+ warnings.append(f"Line {i}: malformed log entry — expected `## [YYYY-MM-DD] <type> | <title>`")
30
+ return warnings
31
+
32
+
33
+ def main():
34
+ try:
35
+ input_data = json.load(sys.stdin)
36
+ except (ValueError, EOFError, OSError):
37
+ sys.exit(0)
38
+
39
+ tool_input = input_data.get("tool_input")
40
+ if not isinstance(tool_input, dict):
41
+ sys.exit(0)
42
+
43
+ file_path = tool_input.get("file_path", "")
44
+ if not isinstance(file_path, str) or not file_path:
45
+ sys.exit(0)
46
+
47
+ if not file_path.endswith(".md"):
48
+ sys.exit(0)
49
+
50
+ normalized = file_path.replace("\\", "/")
51
+ basename = os.path.basename(normalized)
52
+
53
+ # Skip non-vault files
54
+ skip_names = {"README.md", "CHANGELOG.md", "CONTRIBUTING.md", "CLAUDE.md", "AGENTS.md", "LICENSE"}
55
+ if basename in skip_names:
56
+ sys.exit(0)
57
+ if basename.startswith("README.") and basename.endswith(".md"):
58
+ sys.exit(0)
59
+
60
+ skip_paths = [".claude/", ".codex/", ".codex-vault/", ".mind/", "templates/", "thinking/", "node_modules/", "plugin/", "docs/"]
61
+ if any(skip in normalized for skip in skip_paths):
62
+ sys.exit(0)
63
+
64
+ warnings = []
65
+
66
+ try:
67
+ content = Path(file_path).read_text(encoding="utf-8")
68
+
69
+ if not content.startswith("---"):
70
+ warnings.append("Missing YAML frontmatter")
71
+ else:
72
+ parts = content.split("---", 2)
73
+ if len(parts) >= 3:
74
+ fm = parts[1]
75
+ if "date:" not in fm and basename != "log.md":
76
+ warnings.append("Missing `date` in frontmatter")
77
+ if "tags:" not in fm:
78
+ warnings.append("Missing `tags` in frontmatter")
79
+ if "description:" not in fm:
80
+ warnings.append("Missing `description` in frontmatter (~150 chars)")
81
+
82
+ if len(content) > 300 and "[[" not in content:
83
+ warnings.append("No [[wikilinks]] found — every note should link to at least one other note")
84
+
85
+ # Check for unfilled template placeholders
86
+ placeholders = re.findall(r"\{\{[^}]+\}\}", content)
87
+ if placeholders:
88
+ examples = ", ".join(placeholders[:3])
89
+ warnings.append(f"Unfilled template placeholders found: {examples}")
90
+
91
+ # Validate log.md format
92
+ if basename == "log.md":
93
+ log_warnings = _check_log_format(content)
94
+ warnings.extend(log_warnings)
95
+
96
+ except Exception:
97
+ sys.exit(0)
98
+
99
+ if warnings:
100
+ hint_list = "\n".join(f" - {w}" for w in warnings)
101
+ count = len(warnings)
102
+ first = warnings[0]
103
+ if count == 1:
104
+ feedback = f"\u26a0\ufe0f vault: {basename} — {first}"
105
+ else:
106
+ feedback = f"\u26a0\ufe0f vault: {basename} — {first} (+{count - 1} more)"
107
+
108
+ # Codex CLI: use stderr for feedback (no systemMessage rendering)
109
+ sys.stderr.write(f" {feedback}\n")
110
+
111
+ output = {
112
+ "hookSpecificOutput": {
113
+ "hookEventName": "PostToolUse",
114
+ "additionalContext": f"Vault warnings for `{basename}`:\n{hint_list}\nFix these before moving on."
115
+ },
116
+ }
117
+ json.dump(output, sys.stdout)
118
+ sys.stdout.flush()
119
+
120
+ sys.exit(0)
121
+
122
+
123
+ if __name__ == "__main__":
124
+ try:
125
+ main()
126
+ except Exception:
127
+ sys.exit(0)
package/plugin/install.sh CHANGED
@@ -90,13 +90,14 @@ fi
90
90
  # --- Copy hooks into vault (makes vault self-contained) ---
91
91
 
92
92
  HOOKS_DIR="$VAULT_DIR/.codex-vault/hooks"
93
- mkdir -p "$HOOKS_DIR"
94
- cp "$REPO_DIR/plugin/hooks/"* "$HOOKS_DIR/"
95
- chmod +x "$HOOKS_DIR/"*.sh "$HOOKS_DIR/"*.py 2>/dev/null || true
96
- echo "[+] Hook scripts copied to vault/.codex-vault/hooks/"
93
+ mkdir -p "$HOOKS_DIR/claude" "$HOOKS_DIR/codex"
94
+ cp "$REPO_DIR/plugin/hooks/claude/"* "$HOOKS_DIR/claude/"
95
+ cp "$REPO_DIR/plugin/hooks/codex/"* "$HOOKS_DIR/codex/"
96
+ chmod +x "$HOOKS_DIR/claude/"*.py "$HOOKS_DIR/codex/"*.py 2>/dev/null || true
97
+ echo "[+] Hook scripts copied to vault/.codex-vault/hooks/{claude,codex}/"
97
98
  echo ""
98
99
 
99
- # --- Helper: merge hooks into existing settings.json ---
100
+ # --- Helper: merge hooks into existing settings.json (Claude Code) ---
100
101
  # Uses python3 for reliable JSON manipulation.
101
102
 
102
103
  merge_hooks_json() {
@@ -153,6 +154,60 @@ with open(target_file, "w") as f:
153
154
  PYEOF
154
155
  }
155
156
 
157
+ # --- Helper: merge hooks into existing hooks.json (Codex CLI) ---
158
+ # Codex CLI differences:
159
+ # - SessionStart matcher: "startup|resume" (no compact)
160
+ # - No PostToolUse hook (Codex only supports Bash matcher, not Write|Edit)
161
+
162
+ merge_codex_hooks_json() {
163
+ local target_file="$1"
164
+ local hooks_rel="$2"
165
+
166
+ CVAULT_TARGET_FILE="$target_file" CVAULT_HOOKS_REL="$hooks_rel" python3 <<'PYEOF'
167
+ import json, os
168
+
169
+ target_file = os.environ["CVAULT_TARGET_FILE"]
170
+ hooks_rel = os.environ["CVAULT_HOOKS_REL"]
171
+
172
+ new_hooks = {
173
+ "SessionStart": [{
174
+ "matcher": "startup|resume",
175
+ "hooks": [{"type": "command", "command": f"python3 {hooks_rel}/session-start.py", "timeout": 30,
176
+ "statusMessage": "\U0001f4da Codex-Vault loading..."}]
177
+ }],
178
+ "UserPromptSubmit": [{
179
+ "hooks": [{"type": "command", "command": f"python3 {hooks_rel}/classify-message.py", "timeout": 15}]
180
+ }],
181
+ }
182
+
183
+ if os.path.isfile(target_file):
184
+ with open(target_file) as f:
185
+ existing = json.load(f)
186
+ else:
187
+ existing = {}
188
+
189
+ if "hooks" not in existing:
190
+ existing["hooks"] = {}
191
+
192
+ for event, entries in new_hooks.items():
193
+ if event not in existing["hooks"]:
194
+ existing["hooks"][event] = entries
195
+ else:
196
+ existing_cmds = set()
197
+ for rule in existing["hooks"][event]:
198
+ for h in rule.get("hooks", []):
199
+ existing_cmds.add(h.get("command", ""))
200
+ for entry in entries:
201
+ cmds = [h.get("command", "") for h in entry.get("hooks", [])]
202
+ if not any(c in existing_cmds for c in cmds):
203
+ existing["hooks"][event].append(entry)
204
+
205
+ with open(target_file, "w") as f:
206
+ json.dump(existing, f, indent=2)
207
+ f.write("\n")
208
+ PYEOF
209
+ }
210
+
156
211
  # --- Helper: append codex-vault section to instruction file ---
157
212
 
158
213
  append_instructions() {
@@ -208,7 +263,7 @@ setup_claude() {
208
263
  if [ "$MODE" = "integrated" ]; then
209
264
  # Integrated: merge hooks into project root .claude/settings.json
210
265
  mkdir -p "$CONFIG_DIR/.claude"
211
- merge_hooks_json "$CONFIG_DIR/.claude/settings.json" "$HOOKS_REL"
266
+ merge_hooks_json "$CONFIG_DIR/.claude/settings.json" "$HOOKS_REL/claude"
212
267
  echo " [+] .claude/settings.json (hooks merged at project root)"
213
268
 
214
269
  # Append instructions to project root CLAUDE.md
@@ -219,7 +274,7 @@ setup_claude() {
219
274
  install_skills "$CONFIG_DIR" ".claude"
220
275
  else
221
276
  # Standalone: write directly into vault/ (original behavior)
222
- merge_hooks_json "$VAULT_DIR/.claude/settings.json" "$HOOKS_REL"
277
+ merge_hooks_json "$VAULT_DIR/.claude/settings.json" "$HOOKS_REL/claude"
223
278
  echo " [+] .claude/settings.json (3 hooks)"
224
279
 
225
280
  # Install skills into vault/.claude/skills/
@@ -270,7 +325,7 @@ setup_codex() {
270
325
  if [ "$MODE" = "integrated" ]; then
271
326
  # Integrated: merge hooks into project root .codex/hooks.json
272
327
  mkdir -p "$CONFIG_DIR/.codex"
273
- merge_hooks_json "$CONFIG_DIR/.codex/hooks.json" "$HOOKS_REL"
328
+ merge_codex_hooks_json "$CONFIG_DIR/.codex/hooks.json" "$HOOKS_REL/codex"
274
329
  echo " [+] .codex/hooks.json (hooks merged at project root)"
275
330
 
276
331
  if [ "$has_claude" = true ]; then
@@ -290,8 +345,8 @@ setup_codex() {
290
345
  # Standalone: write directly into vault/ (original behavior)
291
346
  mkdir -p "$VAULT_DIR/.codex"
292
347
 
293
- merge_hooks_json "$VAULT_DIR/.codex/hooks.json" "$HOOKS_REL"
294
- echo " [+] .codex/hooks.json (3 hooks)"
348
+ merge_codex_hooks_json "$VAULT_DIR/.codex/hooks.json" "$HOOKS_REL/codex"
349
+ echo " [+] .codex/hooks.json (2 hooks)"
295
350
 
296
351
  # Enable hooks feature flag
297
352
  enable_codex_hooks "$VAULT_DIR"
@@ -6,7 +6,7 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "python3 .codex-vault/hooks/session-start.py",
9
+ "command": "python3 .codex-vault/hooks/claude/session-start.py",
10
10
  "timeout": 30
11
11
  }
12
12
  ]
@@ -17,7 +17,7 @@
17
17
  "hooks": [
18
18
  {
19
19
  "type": "command",
20
- "command": "python3 .codex-vault/hooks/classify-message.py",
20
+ "command": "python3 .codex-vault/hooks/claude/classify-message.py",
21
21
  "timeout": 15
22
22
  }
23
23
  ]
@@ -29,7 +29,7 @@
29
29
  "hooks": [
30
30
  {
31
31
  "type": "command",
32
- "command": "python3 .codex-vault/hooks/validate-write.py",
32
+ "command": "python3 .codex-vault/hooks/claude/validate-write.py",
33
33
  "timeout": 15
34
34
  }
35
35
  ]
@@ -6,9 +6,9 @@
6
6
  "hooks": [
7
7
  {
8
8
  "type": "command",
9
- "command": "python3 .codex-vault/hooks/session-start.py",
9
+ "command": "python3 .codex-vault/hooks/codex/session-start.py",
10
10
  "timeout": 30,
11
- "statusMessage": "📚 Codex-Vault loading..."
11
+ "statusMessage": "\ud83d\udcda Codex-Vault loading..."
12
12
  }
13
13
  ]
14
14
  }
@@ -18,19 +18,7 @@
18
18
  "hooks": [
19
19
  {
20
20
  "type": "command",
21
- "command": "python3 .codex-vault/hooks/classify-message.py",
22
- "timeout": 15
23
- }
24
- ]
25
- }
26
- ],
27
- "PostToolUse": [
28
- {
29
- "matcher": "Write|Edit",
30
- "hooks": [
31
- {
32
- "type": "command",
33
- "command": "python3 .codex-vault/hooks/validate-write.py",
21
+ "command": "python3 .codex-vault/hooks/codex/classify-message.py",
34
22
  "timeout": 15
35
23
  }
36
24
  ]