Skip to content

feat(codex): install session-start hook#149

Open
def324 wants to merge 3 commits intoory:mainfrom
def324:feat/codex-session-start-hooks-upstream
Open

feat(codex): install session-start hook#149
def324 wants to merge 3 commits intoory:mainfrom
def324:feat/codex-session-start-hooks-upstream

Conversation

@def324
Copy link
Copy Markdown

@def324 def324 commented May 5, 2026

Summary

Add a Codex-specific installer that configures the Lumen MCP server, shared skills, Codex hook feature flag, and a user-level SessionStart hook for proactive project index warmup.

  • Add lumen codex install to install or repair Codex MCP, skills, and hooks.
  • Enable [features] codex_hooks = true in Codex config.toml so installed hooks actually run.
  • Add lumen codex doctor to report missing, disabled, stale, duplicate, or malformed Codex integration state.
  • Merge Lumen SessionStart hooks into Codex hooks.json without clobbering unrelated user hooks.
  • Deduplicate and replace stale Lumen-owned hook entries.
  • Refuse to overwrite user-owned skill files or non-Lumen symlinks.
  • Update Codex documentation from manual setup to the installer flow.

Test Plan

  • make test
  • make build-local
  • git diff --check
  • temp HOME/CODEX_HOME install + doctor smoke test with bin/lumen

Summary by CodeRabbit

Release Notes

  • New Features

    • Added codex install command to automate Codex configuration, MCP registration, and skills setup
    • Added codex doctor command for installation diagnostics and status verification
    • Enabled Codex hook integration for proactive index warming on session start
  • Documentation

    • Updated installation and update procedures to use automated installer script
  • Tests

    • Added comprehensive test coverage for Codex integration and configuration management

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

Adds Codex support to Lumen by introducing an installer command that registers the plugin with Codex, configures session-start hooks for index warming, manages skills linking/copying, and registers the MCP server. Includes a doctor command for diagnostics, TOML/JSON hook management utilities, and comprehensive test coverage. Updates documentation and hook host support.

Changes

Codex Installation & Diagnostics

Layer / File(s) Summary
Documentation & User Interface
.codex/INSTALL.md, README.md
Installation/update/uninstall procedures updated from manual MCP registration and symlink creation to invoking an installer script (codex install), which owns hook configuration, MCP registration, and skills management. Verification now includes codex doctor command.
Hook Host Support
cmd/hook.go, cmd/hook_test.go
Adds hookHostCodex = "codex" constant and updates normalizeHookHost to recognize and normalize Codex as a supported hook host, generating Claude-style hookSpecificOutput format.
TOML & Hook JSON Utilities
cmd/codex_hooks.go
Implements mergeCodexSessionStartHook to parse, validate, and merge session-start hooks into JSON hook documents with deduplication and ownership tracking via fixed status messages. Implements mergeCodexHooksFeatureFlag and codexHooksFeatureEnabledInConfig to manage the [features].codex_hooks boolean flag in TOML-like config text, with lightweight table/key parsing that preserves formatting and comments. Provides cross-platform launcher path helpers (codexLauncherPathForGOOS) and shell-quoting utilities for Windows cmd and POSIX shells.
Install & Doctor Commands
cmd/codex.go
Implements codex install subcommand that resolves Codex/Lumen filesystem paths, enables the hooks feature flag, installs/merges session-start hook JSON (with symlink-safe atomic writes), ensures skills availability (preferring symlink, falling back to copy with marker file), and registers/updates MCP server configuration by invoking codex mcp get/add/remove. Implements codex doctor subcommand that reports resolved paths, MCP lumen status (with error-type-specific messages), feature flag enablement, session-start hook counts (ok/disabled/duplicate/mismatch/missing), and skills ownership status. Includes helpers for MCP output parsing (both JSON and line-based formats), hook JSON schema validation, skill ownership detection (symlink/dir/marker-based), and idempotent file writes that preserve symlink targets.
Hook & Doctor Tests
cmd/codex_hooks_test.go, cmd/hook_test.go
Test mergeCodexSessionStartHook for missing/empty/malformed documents, hook preservation, replacement/deduplication of stale Lumen hooks, and idempotency. Test mergeCodexHooksFeatureFlag for flag insertion, overwriting, idempotency, and rejection of invalid values. Test launcher path, command quoting, and shell expansion escaping. Test normalizeHookHost("codex") produces MCP tool references with Claude-compatible JSON format.
Integration & End-to-End Tests
cmd/codex_test.go
Test environment-based path resolution. Test install command flow: hook/config/skills/MCP setup when missing, idempotent re-runs (no repeated MCP calls, stable files), failure propagation for MCP add/skills setup, and MCP reconfiguration on mismatch. Test doctor command across installed/missing/disabled hooks, unavailable/mismatched MCP, duplicate hooks, malformed hooks, and skills ownership variants. Test skills handling: copy idempotence with marker detection, rejection of marker-only copies, refusal to overwrite user files or non-Lumen symlinks, replacement of stale/dangling Lumen symlinks. Includes fakeRunner stub for command invocation simulation.
sequenceDiagram
    participant User
    participant Installer as codex install
    participant Paths as Path Resolution
    participant Config as config.toml
    participant Hooks as hooks.json
    participant Skills as skills directory
    participant MCP as MCP Registry

    User->>Installer: Run codex install
    Installer->>Paths: Resolve CODEX_HOME, LUMEN_PLUGIN_ROOT
    Paths-->>Installer: Paths resolved
    
    Installer->>Config: Read config.toml
    Config-->>Installer: Config loaded
    Installer->>Config: Merge codex_hooks = true
    Config-->>Installer: Feature flag enabled
    
    Installer->>Hooks: Read hooks.json
    alt Hooks exist
        Hooks-->>Installer: Hooks loaded
    else Hooks missing
        Installer-->>Hooks: Initialize empty
    end
    Installer->>Hooks: Merge SessionStart hook with launcher command
    Hooks-->>Installer: Hook merged & deduplicated
    Installer->>Hooks: Write with atomic temp-file replace
    Hooks-->>Installer: Written (preserving symlinks)
    
    Installer->>Skills: Check skills destination
    alt Symlink exists and valid
        Skills-->>Installer: Symlink OK (skip)
    else Directory with marker exists
        Skills-->>Installer: Copied skills OK (skip)
    else Missing or stale
        Installer->>Skills: Create symlink to plugin/skills
        Skills-->>Installer: Symlink created
    end
    
    Installer->>MCP: Run codex mcp get lumen
    alt MCP installed
        MCP-->>Installer: Return current config
        alt Config matches launcher & stdio
            Installer-->>Installer: No change needed
        else Config mismatched
            Installer->>MCP: codex mcp remove lumen
            MCP-->>Installer: Removed
            Installer->>MCP: codex mcp add lumen ... stdio
            MCP-->>Installer: Added with correct config
        end
    else MCP not found
        Installer->>MCP: codex mcp add lumen ... stdio
        MCP-->>Installer: Added
    end
    
    Installer-->>User: Install complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(codex): install session-start hook' directly and accurately summarizes the main change: adding Codex session-start hook installation functionality. It is concise, specific, and reflects the primary objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@aeneasr
Copy link
Copy Markdown
Member

aeneasr commented May 5, 2026

Does codex now support hooks?

@def324
Copy link
Copy Markdown
Author

def324 commented May 5, 2026

Yes: https://developers.openai.com/codex/hooks

Looks like it's been there at least since 0.114.0: https://github.com/openai/codex/releases/tag/rust-v0.114.0 They have also been marked stable since 0.124.0.

It's been quite effective in my local setup, I've been using it for a few days now.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
cmd/codex_hooks.go (2)

309-333: 💤 Low value

Ownership heuristic is sound; consider documenting it.

isLumenCodexSessionStartCommand recognises a Lumen-owned hook when the launcher path includes /scripts/run.sh or /scripts/run.cmd and the parent dir (case-insensitive) starts with lumen. Combined with the explicit expectedCommand and statusMessage checks in isOwnedLumenCodexSessionStartCommand, this correctly preserves foreign hook session-start lumen commands rooted elsewhere (covered by TestMergeCodexSessionStartHook_PreservesForeignSessionStartCommands). Worth a short doc comment so the "starts with lumen" heuristic isn't accidentally broadened or tightened later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/codex_hooks.go` around lines 309 - 333, Add a short doc comment above
isLumenCodexSessionStartCommand and isOwnedLumenCodexSessionStartCommand that
explains the ownership heuristic: that isLumenCodexSessionStartCommand checks
for launcher paths ending with "/scripts/run.sh" or "/scripts/run.cmd" and then
verifies the parent directory name (case-insensitive) starts with "lumen", and
that isOwnedLumenCodexSessionStartCommand additionally allows an exact
expectedCommand match or a codex-specific statusMessage match before falling
back to the heuristic; include the rationale that this preserves foreign "hook
session-start lumen" commands rooted elsewhere and warn not to broaden or
tighten the "starts with lumen" check without updating tests.

228-247: 💤 Low value

TOML "comment splitter" doesn't handle multiline strings.

splitTomlLineComment correctly handles single-line '…' / "…" plus backslash escaping inside "…", but it has no notion of TOML multiline strings ("""…""", '''…'''). A # inside a multiline literal that spans lines could be misread as a comment if the opening triple-quote is on a prior line. In practice Codex's config.toml is unlikely to contain such constructs around the [features] block, so this is fine for the current scope — worth a one-line comment scoping the parser's intent (per-line, no multiline string awareness) so future maintainers don't repurpose it elsewhere.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/codex_hooks.go` around lines 228 - 247, The splitTomlLineComment function
does not understand TOML multiline string delimiters ('''...''' and """...""")
and can misinterpret a '#' inside a multiline string that began on a prior line
as a comment; add a clear one-line doc comment immediately above
splitTomlLineComment stating that this parser is per-line only and intentionally
does not handle TOML multiline strings (triple-quoted literals) so future
maintainers won't reuse it for multiline-aware parsing, or alternatively
implement multiline detection if you want full TOML compliance.
cmd/codex.go (2)

307-328: ⚡ Quick win

Surface "codex CLI missing" early in install, like doctor does.

runCodexDoctor cleanly maps errors.Is(err, exec.ErrNotFound) from mcp get to "MCP lumen: unavailable: codex command not found". ensureCodexMCP does not — when the codex binary is missing, the mcp get failure silently falls through to mcp add, which also fails with ErrNotFound, and the user sees add Codex MCP server: exec: "codex": executable file not found in $PATH. By that point hooks/skills/feature-flag are already installed, which is fine, but the error message obscures the actual cause. A small early-exit on errors.Is(err, exec.ErrNotFound) here would give the same clear UX as doctor and avoid the redundant mcp add attempt.

♻️ Suggested early-exit
 func ensureCodexMCP(ctx context.Context, paths codexPaths, runner commandRunner, stdout, _ io.Writer) error {
 	out, err := runner.Run(ctx, "codex", "mcp", "get", "lumen")
 	if err == nil && codexMCPOutputMatches(out, paths) {
 		_, _ = fmt.Fprintln(stdout, "Codex MCP server lumen already configured")
 		return nil
 	}
 	if err == nil {
 		if _, removeErr := runner.Run(ctx, "codex", "mcp", "remove", "lumen"); removeErr != nil {
 			return fmt.Errorf("remove mismatched Codex MCP server: %w", removeErr)
 		}
 		if _, addErr := runner.Run(ctx, "codex", "mcp", "add", "lumen", "--", paths.launcher, "stdio"); addErr != nil {
 			return fmt.Errorf("add Codex MCP server: %w", addErr)
 		}
 		_, _ = fmt.Fprintln(stdout, "Updated Codex MCP server lumen")
 		return nil
 	}
+	if errors.Is(err, exec.ErrNotFound) {
+		return fmt.Errorf("codex command not found in PATH; install the Codex CLI and re-run `lumen codex install`")
+	}
+	if !codexMCPGetOutputReportsMissing(out) {
+		return fmt.Errorf("query Codex MCP server: %w", err)
+	}
 	if _, addErr := runner.Run(ctx, "codex", "mcp", "add", "lumen", "--", paths.launcher, "stdio"); addErr != nil {
 		return fmt.Errorf("add Codex MCP server: %w", addErr)
 	}
 	_, _ = fmt.Fprintln(stdout, "Added Codex MCP server lumen")
 	return nil
 }

If you take this, double-check TestRunCodexInstall_WritesHookAndAddsMCPWhenMissing — it currently uses getErr: exec.ErrNotFound to simulate "no server yet" rather than "no CLI". Switching that fixture to a non-ErrNotFound error or to codexMCPGetOutputReportsMissing-style stdout would keep the test's intent intact.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/codex.go` around lines 307 - 328, ensureCodexMCP currently treats a
missing codex binary the same as other runner errors and falls through to
attempting mcp add; update ensureCodexMCP to check the error returned by the
first runner.Run(ctx, "codex", "mcp", "get", "lumen") and if errors.Is(err,
exec.ErrNotFound) return a clear, early error (same UX as runCodexDoctor: e.g.
"MCP lumen: unavailable: codex command not found") instead of proceeding to mcp
add; locate this logic in ensureCodexMCP, use the existing runner.Run result and
codexMCPOutputMatches to distinguish the paths, and adjust any tests that
assumed getErr == exec.ErrNotFound to simulate a different "no server"
condition.

599-602: 💤 Low value

Ownership detection couples to two literal SKILL.md headings.

lumenSkillsDirLooksOwned requires the exact strings # Lumen Doctor and # Lumen Reindex to consider the destination "installer-managed". If those headings are ever edited (refactor, branding, frontmatter rearrangement), the installer would silently treat its own previous output as user-owned and refuse to update or uninstall it. Consider keying ownership off the .lumen-skills-source marker alone (which copyCodexSkills already writes) and reserving the heading check as a redundant signal, so renames to SKILL.md content don't strand existing users.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/codex.go` around lines 599 - 602, The ownership check in
lumenSkillsDirLooksOwned currently requires exact SKILL.md headings and is
fragile; change lumenSkillsDirLooksOwned to first check for the presence of the
marker file that copyCodexSkills writes (the ".lumen-skills-source" file in the
given path) and return true if that marker exists, and only fall back to the
current fileContains heading checks as a redundant signal; update
lumenSkillsDirLooksOwned (and any uses of fileContains within it) to prefer the
marker-based test so renaming/editing SKILL.md headings won't break installer
ownership detection.
cmd/codex_test.go (1)

50-87: 💤 Low value

Launcher assertions are Unix-shaped; consider GOOS-gating or a build tag if Windows testing is added.

makeCodexPluginRoot only creates scripts/run.sh, and tests like this one assert paths.launcher == filepath.Join(pluginRoot, "scripts", "run.sh"). On Windows, codexLauncherPath returns the run.cmd form, so these assertions would fail. Currently, Go tests only run on ubuntu-latest in CI, so this isn't an issue in practice. However, if Windows testing is added to the Go test matrix, gate these assertions with if runtime.GOOS == "windows" { t.Skip(...) } or compute the expected launcher via codexLauncherPath(pluginRoot) instead of hardcoding run.sh.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/codex_test.go` around lines 50 - 87, TestCodexPathsFromEnv hardcodes a
Unix launcher path which will fail on Windows; change the assertion to compute
the expected launcher using codexLauncherPath(pluginRoot) (or skip the assertion
on windows via runtime.GOOS == "windows") so it matches the platform-specific
value returned by resolveCodexPaths; update the test (TestCodexPathsFromEnv) to
call codexLauncherPath(pluginRoot) for the expected launcher (or conditionally
t.Skip) and keep other assertions the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cmd/codex_hooks.go`:
- Around line 309-333: Add a short doc comment above
isLumenCodexSessionStartCommand and isOwnedLumenCodexSessionStartCommand that
explains the ownership heuristic: that isLumenCodexSessionStartCommand checks
for launcher paths ending with "/scripts/run.sh" or "/scripts/run.cmd" and then
verifies the parent directory name (case-insensitive) starts with "lumen", and
that isOwnedLumenCodexSessionStartCommand additionally allows an exact
expectedCommand match or a codex-specific statusMessage match before falling
back to the heuristic; include the rationale that this preserves foreign "hook
session-start lumen" commands rooted elsewhere and warn not to broaden or
tighten the "starts with lumen" check without updating tests.
- Around line 228-247: The splitTomlLineComment function does not understand
TOML multiline string delimiters ('''...''' and """...""") and can misinterpret
a '#' inside a multiline string that began on a prior line as a comment; add a
clear one-line doc comment immediately above splitTomlLineComment stating that
this parser is per-line only and intentionally does not handle TOML multiline
strings (triple-quoted literals) so future maintainers won't reuse it for
multiline-aware parsing, or alternatively implement multiline detection if you
want full TOML compliance.

In `@cmd/codex_test.go`:
- Around line 50-87: TestCodexPathsFromEnv hardcodes a Unix launcher path which
will fail on Windows; change the assertion to compute the expected launcher
using codexLauncherPath(pluginRoot) (or skip the assertion on windows via
runtime.GOOS == "windows") so it matches the platform-specific value returned by
resolveCodexPaths; update the test (TestCodexPathsFromEnv) to call
codexLauncherPath(pluginRoot) for the expected launcher (or conditionally
t.Skip) and keep other assertions the same.

In `@cmd/codex.go`:
- Around line 307-328: ensureCodexMCP currently treats a missing codex binary
the same as other runner errors and falls through to attempting mcp add; update
ensureCodexMCP to check the error returned by the first runner.Run(ctx, "codex",
"mcp", "get", "lumen") and if errors.Is(err, exec.ErrNotFound) return a clear,
early error (same UX as runCodexDoctor: e.g. "MCP lumen: unavailable: codex
command not found") instead of proceeding to mcp add; locate this logic in
ensureCodexMCP, use the existing runner.Run result and codexMCPOutputMatches to
distinguish the paths, and adjust any tests that assumed getErr ==
exec.ErrNotFound to simulate a different "no server" condition.
- Around line 599-602: The ownership check in lumenSkillsDirLooksOwned currently
requires exact SKILL.md headings and is fragile; change lumenSkillsDirLooksOwned
to first check for the presence of the marker file that copyCodexSkills writes
(the ".lumen-skills-source" file in the given path) and return true if that
marker exists, and only fall back to the current fileContains heading checks as
a redundant signal; update lumenSkillsDirLooksOwned (and any uses of
fileContains within it) to prefer the marker-based test so renaming/editing
SKILL.md headings won't break installer ownership detection.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f0a6d3b6-aedb-4473-8479-77a75247ea32

📥 Commits

Reviewing files that changed from the base of the PR and between 5206287 and 629da25.

📒 Files selected for processing (8)
  • .codex/INSTALL.md
  • README.md
  • cmd/codex.go
  • cmd/codex_hooks.go
  • cmd/codex_hooks_test.go
  • cmd/codex_test.go
  • cmd/hook.go
  • cmd/hook_test.go

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.

2 participants