Skip to content

feat(codex): add native hooks and project install - #47

Merged
ch-liuzhide merged 1 commit into
mainfrom
codex/codex-native-integration-0.3.0
Jun 30, 2026
Merged

feat(codex): add native hooks and project install#47
ch-liuzhide merged 1 commit into
mainfrom
codex/codex-native-integration-0.3.0

Conversation

@ch-liuzhide

@ch-liuzhide ch-liuzhide commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • add Codex-native install/uninstall with project scope by default and user scope support
  • add Codex lifecycle hook commands for recall, prompt recall, and Stop-turn capture
  • add Codex rollout transcript parser and Stop-hook metadata/deduping
  • bump hebb-mind to 0.3.0 and update changelog/docs/tests

Verification

  • PYTHONPATH=src pytest -q tests/unit
  • PYTHONPATH=src pytest -q tests/integration
  • PYTHONPATH=src pytest -q tests/integration/test_facade.py tests/unit/integrations/test_codex_cli.py tests/unit/integrations/test_codex_hooks.py
  • mypy src/hebb/
  • ruff check src tests/unit/integrations/test_codex_cli.py tests/unit/integrations/test_codex_hooks.py
  • npm run docs:build in repo_pages/
  • pre-push hooks: ruff, mypy, pytest

Summary by CodeRabbit

  • New Features

    • Added native Codex integration with project- and user-scoped setup, plus new lifecycle hooks for session recall, prompt context, and turn capture.
    • Improved Codex memory handling with transcript parsing and automatic saving of completed turns.
  • Bug Fixes

    • Uninstall and reinstall flows now preserve unrelated Codex settings while removing only Hebb-managed entries.
    • Updated default Codex install behavior and verification steps for smoother setup.
  • Documentation

    • Refreshed setup guides, quick starts, and CLI references in English and Chinese.

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Version 0.3.0 adds native Codex lifecycle hook integration: new install.py, uninstall.py, recall.py, stop.py, and transcript.py modules implement scoped (project/user) MCP config and hooks.json management, a JSONL rollout transcript parser, and a deduplicating Stop hook that writes memories to Hebb Mind. The CLI gains recall, prompt, and stop subcommands.

Changes

Codex Native Lifecycle Hook Integration

Layer / File(s) Summary
CLI entry points
src/hebb/integrations/codex/cli.py, src/hebb/cli/commands/doctor.py, src/hebb/cli/commands/setup.py
install/uninstall subcommands now accept --scope project|user (defaulting to project) and delegate to handler modules; three new hook subcommands (recall, prompt, stop) are added; doctor and setup next-step hints drop --scope user.
install.py: scoped MCP + hooks
src/hebb/integrations/codex/install.py
New module resolves per-scope config/hooks paths, generates lifecycle hook entries for SessionStart/UserPromptSubmit/Stop, merges them into hooks.json (removing legacy Hebb handlers), upserts [mcp_servers.hebb] TOML for project scope or runs codex mcp add for user scope, and writes files atomically.
uninstall.py: scoped removal
src/hebb/integrations/codex/uninstall.py
Removes Hebb hooks from hooks.json and project MCP from config.toml atomically; for user scope runs codex mcp remove hebb ignoring not-found errors.
recall.py: session/prompt hooks
src/hebb/integrations/codex/recall.py
Delegates handle_session_start() and handle_prompt() to the existing claude_code recall handlers.
transcript.py: JSONL parser
src/hebb/integrations/codex/transcript.py
extract_last_turn() parses Codex rollout JSONL, locates the last user input, selects assistant output with last_assistant_message precedence, extracts tool/MCP call names, and returns a CodexTurn dataclass.
stop.py: Stop hook with deduplication
src/hebb/integrations/codex/stop.py
handle() reads hook input, extracts the last turn via the transcript parser, and calls _record_turn() which checks _already_written() against /api/v1/memories before POSTing with session/turn metadata and source hook:codex-stop.
hooks.json update + tests
.codex/hooks.json, tests/unit/integrations/test_codex_cli.py, tests/unit/integrations/test_codex_hooks.py
Repo's own hooks updated to hebb codex recall/prompt/stop; tests cover install/uninstall idempotency, default project scope, transcript parsing, stop hook posting, and deduplication.
Design doc, changelog, docs, version bumps
reports/design/codex-native-integration-design.md, CHANGELOG.md, pyproject.toml, src/hebb/__init__.py, .claude-plugin/plugin.json, .release-please-manifest.json, README.md, README_ZH.md, repo_pages/**, src/hebb/upgrade/helper.py
Adds design document; bumps version to 0.3.0 across all manifests; updates all EN/ZH docs to reflect scoped install, lifecycle hook behavior, and new CLI commands.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Poem

🐇 Hop, hop! The Codex now recalls each turn,
With hooks that fire—session, prompt, and stop—
No more forgetting what the agents learn!
Project scope or user, configs drop
Into .codex/ files, neat and set.
The rabbit's memory never forgets. 🧠

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.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 clearly reflects the main Codex-native hooks and default project install change, even though it omits other supporting updates.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/codex-native-integration-0.3.0

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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request introduces native Codex integration with Hebb Mind, adding support for native lifecycle hooks (SessionStart, UserPromptSubmit, and Stop) and project-scoped configuration, alongside a Codex transcript parser and updated documentation. Feedback from the review highlights a few critical issues: a parsing bug in remove_project_mcp_table that could corrupt configuration files if trailing comments are present, a logical error in _clean_user_text that incorrectly filters out short useful keywords while retaining greetings, and unnecessary codex CLI presence checks during project-scoped installation and uninstallation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +159 to +178
def remove_project_mcp_table(text: str) -> str:
"""Remove Hebb's MCP TOML table while preserving unrelated config.

Args:
text: Existing Codex TOML source.

Returns:
TOML source without ``mcp_servers.hebb`` tables.
"""
lines = text.splitlines(keepends=True)
output: list[str] = []
skipping = False
for line in lines:
stripped = line.strip()
if stripped.startswith("[") and stripped.endswith("]"):
table = stripped.strip("[]").strip()
skipping = table == "mcp_servers.hebb" or table.startswith("mcp_servers.hebb.")
if not skipping:
output.append(line)
return "".join(output)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

If a user has a comment on the same line as a table header (e.g., [mcp_servers.other] # comment), stripped.endswith("]") will evaluate to False. This prevents the parser from recognizing the new table header, causing it to keep skipping lines and inadvertently delete the entire rest of the configuration file.

We can fix this by stripping trailing comments before checking for table headers, while still preserving the original line in the output.

def remove_project_mcp_table(text: str) -> str:
    """Remove Hebb's MCP TOML table while preserving unrelated config.

    Args:
        text: Existing Codex TOML source.

    Returns:
        TOML source without ``mcp_servers.hebb`` tables.
    """
    lines = text.splitlines(keepends=True)
    output: list[str] = []
    skipping = False
    for line in lines:
        # Strip trailing comments to correctly identify table headers
        content = line.split("#", 1)[0].strip()
        if content.startswith("[") and content.endswith("]"):
            table = content.strip("[]").strip()
            skipping = table == "mcp_servers.hebb" or table.startswith("mcp_servers.hebb.")
        if not skipping:
            output.append(line)
    return "".join(output)

Comment on lines +151 to +158
def _clean_user_text(raw: str) -> str:
"""Apply Hebb's storage filter and length bound to Codex user text."""
cleaned = clean_user_input(raw)
if not cleaned:
return ""
if not is_greeting_only(cleaned) and len(cleaned) < _MIN_USER_LEN:
return ""
return _truncate(cleaned, _MAX_USER_LEN)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The current condition not is_greeting_only(cleaned) and len(cleaned) < _MIN_USER_LEN has a logical bug:

  • If the input is a greeting (e.g., "Hi"), is_greeting_only is True, so the condition is False and the greeting is kept and stored.
  • If the input is a short but useful keyword (e.g., "pnpm"), is_greeting_only is False, so the condition is True and it is discarded.

We should discard the input if it is a greeting OR if it is shorter than the minimum length.

Suggested change
def _clean_user_text(raw: str) -> str:
"""Apply Hebb's storage filter and length bound to Codex user text."""
cleaned = clean_user_input(raw)
if not cleaned:
return ""
if not is_greeting_only(cleaned) and len(cleaned) < _MIN_USER_LEN:
return ""
return _truncate(cleaned, _MAX_USER_LEN)
def _clean_user_text(raw: str) -> str:
"""Apply Hebb's storage filter and length bound to Codex user text."""
cleaned = clean_user_input(raw)
if not cleaned:
return ""
if is_greeting_only(cleaned) or len(cleaned) < _MIN_USER_LEN:
return ""
return _truncate(cleaned, _MAX_USER_LEN)

Comment on lines 26 to +37
def install(scope: str) -> None:
"""Install Hebb Mind MCP into Codex.
"""Install Hebb Mind MCP and lifecycle hooks into Codex.

Codex registers MCP servers globally via ``codex mcp add`` — there is no
per-project scope, so this command is global-only.
Project scope writes ``.codex/config.toml`` and ``.codex/hooks.json``.
User scope registers MCP through ``codex mcp add`` and writes the user
hooks file.
"""
_ensure_codex()

# Resolve absolute path to hebb-mcp — Codex launches the MCP server as a
# subprocess whose PATH may not include `pip install --user` bin dirs.
mcp_argv = hebb_mcp_command()
# Replace any prior entry so a fresh install picks up a moved binary.
subprocess.run(["codex", "mcp", "remove", "hebb"], capture_output=True, check=False)
result = subprocess.run(["codex", "mcp", "add", "hebb", "--", *mcp_argv], check=False)
if result.returncode != 0:
raise click.ClickException("codex mcp add failed")
from hebb.integrations.codex.install import handle

click.secho("Installed hebb MCP server for Codex.", fg="green")
click.echo(f" MCP: {shell_quote(mcp_argv)}")
click.echo("Verify with: codex mcp list")
handle(scope)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

When installing with scope="project" (the default), we only write local configuration files and do not run any codex CLI commands. Therefore, we shouldn't block the installation if the codex CLI is not present on the user's PATH. We should only require and verify the codex CLI when installing with scope="user".

Suggested change
def install(scope: str) -> None:
"""Install Hebb Mind MCP into Codex.
"""Install Hebb Mind MCP and lifecycle hooks into Codex.
Codex registers MCP servers globally via ``codex mcp add``there is no
per-project scope, so this command is global-only.
Project scope writes ``.codex/config.toml`` and ``.codex/hooks.json``.
User scope registers MCP through ``codex mcp add`` and writes the user
hooks file.
"""
_ensure_codex()
# Resolve absolute path to hebb-mcp — Codex launches the MCP server as a
# subprocess whose PATH may not include `pip install --user` bin dirs.
mcp_argv = hebb_mcp_command()
# Replace any prior entry so a fresh install picks up a moved binary.
subprocess.run(["codex", "mcp", "remove", "hebb"], capture_output=True, check=False)
result = subprocess.run(["codex", "mcp", "add", "hebb", "--", *mcp_argv], check=False)
if result.returncode != 0:
raise click.ClickException("codex mcp add failed")
from hebb.integrations.codex.install import handle
click.secho("Installed hebb MCP server for Codex.", fg="green")
click.echo(f" MCP: {shell_quote(mcp_argv)}")
click.echo("Verify with: codex mcp list")
handle(scope)
def install(scope: str) -> None:
"""Install Hebb Mind MCP and lifecycle hooks into Codex.
Project scope writes ``.codex/config.toml`` and ``.codex/hooks.json``.
User scope registers MCP through ``codex mcp add`` and writes the user
hooks file.
"""
if scope == "user":
_ensure_codex()
from hebb.integrations.codex.install import handle
handle(scope)

Comment on lines 51 to +57
def uninstall(scope: str) -> None:
"""Remove Hebb Mind MCP from Codex (global-only)."""
"""Remove Hebb Mind MCP and lifecycle hooks from Codex."""
_ensure_codex()

result = subprocess.run(["codex", "mcp", "remove", "hebb"], check=False)
if result.returncode != 0:
raise click.ClickException("codex mcp remove failed")
from hebb.integrations.codex.uninstall import handle

handle(scope)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Similarly to the install command, we should only require and verify the codex CLI when uninstalling with scope="user".

Suggested change
def uninstall(scope: str) -> None:
"""Remove Hebb Mind MCP from Codex (global-only)."""
"""Remove Hebb Mind MCP and lifecycle hooks from Codex."""
_ensure_codex()
result = subprocess.run(["codex", "mcp", "remove", "hebb"], check=False)
if result.returncode != 0:
raise click.ClickException("codex mcp remove failed")
from hebb.integrations.codex.uninstall import handle
handle(scope)
def uninstall(scope: str) -> None:
"""Remove Hebb Mind MCP and lifecycle hooks from Codex."""
if scope == "user":
_ensure_codex()
from hebb.integrations.codex.uninstall import handle
handle(scope)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 13

🤖 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.

Inline comments:
In `@repo_pages/zh/index.md`:
- Line 40: The section title in the page metadata is still scoped to Claude
Code, but the updated details now cover both Claude Code and Codex workflows.
Update the title used alongside this card so it matches the broader scope, or
split the content into separate entries if needed; locate the label/title in the
same index page section as the details text referencing Claude Code, Codex, and
MCP memory tools.

In `@src/hebb/integrations/codex/cli.py`:
- Around line 11-12: The public CLI handlers in this module, including codex and
the other exported command functions referenced in the diff, have incomplete
docstrings and need the required structured sections. Update each public API
docstring to include Args, Returns, and Raises in the same style used elsewhere
in the repository, making sure the function names and their behavior are clearly
documented. Keep the content aligned with the existing CLI entrypoints and
lifecycle hooks, and apply the same docstring format consistently across all
affected public functions.
- Around line 33-37: The unconditional _ensure_codex() call in the Codex CLI
entrypoints is forcing a binary dependency even when scope is project, which
only operates on local .codex files. Update the codex command flow in cli.py so
_ensure_codex() is only invoked for user-scoped install/uninstall paths, and let
the project-scoped path call handle(scope) without requiring Codex; apply the
same conditional behavior to the matching uninstall entrypoint as well.

In `@src/hebb/integrations/codex/install.py`:
- Around line 36-44: The public API docstrings are incomplete in install.py and
must include the required Args, Returns, and Raises sections. Update the
docstrings for the affected public functions, including hooks_path(),
hooks_config(), handle(), and the other listed APIs, so each one documents its
parameters, return value, and any exceptions it may raise in the same style as
the existing docstring. Keep the documentation consistent across these symbols
and ensure no public function is missing any required section.
- Around line 173-176: The TOML table detection in the install path is too
strict, so headers like [mcp_servers.hebb] with trailing comments are not
recognized and get duplicated. Update the header parsing in the install logic
around the stripping/skipping checks so it identifies table names before any
inline comment text, then use that normalized table name to decide whether to
skip mcp_servers.hebb and its nested tables.
- Around line 101-110: The command matching logic in the hook installer is
relying on raw substring checks, which misses quoted absolute paths and can
cause duplicate reinstall/uninstall behavior. Update the hook detection in the
relevant helper in install.py to parse the command into argv first, then match
on the executable and subcommand values instead of looking for strings like
“hebb codex ” or “/hebb claude-code ” in the raw command text.

In `@src/hebb/integrations/codex/recall.py`:
- Around line 6-17: The public handlers handle_session_start and handle_prompt
in the Codex recall module only have summary docstrings, so update each
docstring to include the required Args, Returns, and Raises sections. Keep the
behavior unchanged, but document the no-argument signature, the None return
value, and any exceptions that may propagate from the delegated recall call
imported from hebb.integrations.claude_code.recall.

In `@src/hebb/integrations/codex/stop.py`:
- Around line 24-25: The public function handle in stop.py has a docstring but
is missing the required Args, Returns, and Raises sections. Update handle’s
docstring to follow the project Python API docstring convention by adding those
sections, even if they are empty or note that no arguments or exceptions are
expected, so the documentation is complete and consistent.

In `@src/hebb/integrations/codex/transcript.py`:
- Around line 49-57: The transcript selection logic in the user-turn parsing
path currently picks the last raw user record and then returns None if
_clean_user_text filters it out, which can hide an earlier valid user message.
Update the selection in the transcript parser so it chooses the last user record
that still has non-empty cleaned text, using the existing helpers like
_raw_user_text and _clean_user_text in the relevant transcript handling flow.
Apply the same fix in both affected parsing sections so the “last valid user
input” behavior is consistent.
- Around line 166-168: The _truncate helper currently exceeds the configured
limit because it appends the ellipsis after slicing to limit; update _truncate
so the returned string always stays within limit, likely by reserving space for
the ellipsis when truncation is needed. Keep the fix localized to _truncate in
transcript.py and preserve the existing behavior for already-short values.
- Around line 21-26: `CodexTurn` is a public dataclass whose docstring is
missing the required API sections; update the class docstring in `CodexTurn` to
include Args, Returns, and Raises sections in the prescribed style. Use the
existing `summary` and `timestamp` fields to describe the arguments, note the
dataclass construction result in Returns, and include any exceptions that may be
raised during initialization or parsing if applicable.

In `@src/hebb/integrations/codex/uninstall.py`:
- Around line 58-66: Complete the public API docstrings for
uninstall_project_mcp and handle by adding the missing sections required by the
coding guidelines. Update uninstall_project_mcp to include a Raises section
describing any exceptions it can propagate, and update handle to include a
Returns section describing its return value. Keep the docstrings consistent with
the existing Args/Returns style used in
src/hebb/integrations/codex/uninstall.py.

In `@tests/unit/integrations/test_codex_hooks.py`:
- Around line 184-190: The test fixture in hook_input hardcodes an absolute /tmp
transcript_path, which should be replaced with a workspace-safe path. Update the
Codex hook tests that build hook_input to use tmp_path / "rollout.jsonl" (or
another relative placeholder) instead of "/tmp/rollout.jsonl", keeping the
change localized to the test data used around extract_last_turn and the related
hook_input setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 146e5fec-b857-46e3-bfc6-8ac7625674d0

📥 Commits

Reviewing files that changed from the base of the PR and between a39f88a and 90c3a33.

📒 Files selected for processing (33)
  • .claude-plugin/plugin.json
  • .codex/hooks.json
  • .release-please-manifest.json
  • CHANGELOG.md
  • README.md
  • README_ZH.md
  • pyproject.toml
  • repo_pages/api/cli.md
  • repo_pages/guide/codex.md
  • repo_pages/guide/installation.md
  • repo_pages/guide/mcp-integration.md
  • repo_pages/index.md
  • repo_pages/public/llms.txt
  • repo_pages/quick-start.md
  • repo_pages/zh/api/cli.md
  • repo_pages/zh/guide/codex.md
  • repo_pages/zh/guide/installation.md
  • repo_pages/zh/guide/mcp-integration.md
  • repo_pages/zh/index.md
  • repo_pages/zh/quick-start.md
  • reports/design/codex-native-integration-design.md
  • src/hebb/__init__.py
  • src/hebb/cli/commands/doctor.py
  • src/hebb/cli/commands/setup.py
  • src/hebb/integrations/codex/cli.py
  • src/hebb/integrations/codex/install.py
  • src/hebb/integrations/codex/recall.py
  • src/hebb/integrations/codex/stop.py
  • src/hebb/integrations/codex/transcript.py
  • src/hebb/integrations/codex/uninstall.py
  • src/hebb/upgrade/helper.py
  • tests/unit/integrations/test_codex_cli.py
  • tests/unit/integrations/test_codex_hooks.py

Comment thread repo_pages/zh/index.md
- icon: 🔌
title: REST + MCP + Claude Code Hooks
details: 三行命令为 Claude Code 启用跨会话记忆;hebb codex install 一键将能力以 MCP 工具形式接入 Codex。REST 文档位于 /docs。
details: 三行命令为 Claude Code 或 Codex 启用自动跨会话召回、回合写入与 MCP 记忆工具。REST 文档位于 /docs。

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the feature label aligned with the updated copy.

The details now describe both Claude Code and Codex flows, but the section title still says “Claude Code Hooks,” so this card reads as Claude-only. Rename the title (or split the card) to match the broadened scope.

🤖 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 `@repo_pages/zh/index.md` at line 40, The section title in the page metadata is
still scoped to Claude Code, but the updated details now cover both Claude Code
and Codex workflows. Update the title used alongside this card so it matches the
broader scope, or split the content into separate entries if needed; locate the
label/title in the same index page section as the details text referencing
Claude Code, Codex, and MCP memory tools.

Comment on lines 11 to +12
def codex() -> None:
"""Codex integration — configure Hebb Mind as an MCP server."""
"""Codex integration — native MCP and lifecycle hooks."""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the required Args / Returns / Raises sections to these public CLI APIs.

These new public handlers have docstrings, but not the structured sections required by the repository rules. As per coding guidelines, "**/*.py: Include docstring with Args, Returns, and Raises sections for all public APIs" and "src/**/*.py: All public APIs in Python MUST have docstrings with Args, Returns, and Raises sections."

Also applies to: 26-32, 51-52, 61-62, 69-70, 77-78

🤖 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 `@src/hebb/integrations/codex/cli.py` around lines 11 - 12, The public CLI
handlers in this module, including codex and the other exported command
functions referenced in the diff, have incomplete docstrings and need the
required structured sections. Update each public API docstring to include Args,
Returns, and Raises in the same style used elsewhere in the repository, making
sure the function names and their behavior are clearly documented. Keep the
content aligned with the existing CLI entrypoints and lifecycle hooks, and apply
the same docstring format consistently across all affected public functions.

Source: Coding guidelines

Comment on lines 33 to +37
_ensure_codex()

# Resolve absolute path to hebb-mcp — Codex launches the MCP server as a
# subprocess whose PATH may not include `pip install --user` bin dirs.
mcp_argv = hebb_mcp_command()
# Replace any prior entry so a fresh install picks up a moved binary.
subprocess.run(["codex", "mcp", "remove", "hebb"], capture_output=True, check=False)
result = subprocess.run(["codex", "mcp", "add", "hebb", "--", *mcp_argv], check=False)
if result.returncode != 0:
raise click.ClickException("codex mcp add failed")
from hebb.integrations.codex.install import handle

click.secho("Installed hebb MCP server for Codex.", fg="green")
click.echo(f" MCP: {shell_quote(mcp_argv)}")
click.echo("Verify with: codex mcp list")
handle(scope)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Don't require codex for project-scoped install/uninstall.

src/hebb/integrations/codex/install.py:181-222 and src/hebb/integrations/codex/uninstall.py:77-109 only invoke the Codex binary for scope == "user". With project now the default, these unconditional _ensure_codex() calls make the default path fail even though those branches only touch local .codex/ files.

Suggested fix
 def install(scope: str) -> None:
@@
-    _ensure_codex()
+    if scope == "user":
+        _ensure_codex()
@@
 def uninstall(scope: str) -> None:
@@
-    _ensure_codex()
+    if scope == "user":
+        _ensure_codex()

Also applies to: 53-57

🤖 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 `@src/hebb/integrations/codex/cli.py` around lines 33 - 37, The unconditional
_ensure_codex() call in the Codex CLI entrypoints is forcing a binary dependency
even when scope is project, which only operates on local .codex files. Update
the codex command flow in cli.py so _ensure_codex() is only invoked for
user-scoped install/uninstall paths, and let the project-scoped path call
handle(scope) without requiring Codex; apply the same conditional behavior to
the matching uninstall entrypoint as well.

Comment on lines +36 to +44
def hooks_path(scope: str) -> Path:
"""Resolve the Codex hooks path for an installation scope.

Args:
scope: Either ``project`` or ``user``.

Returns:
Path to the active ``hooks.json`` layer.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the required public API docstring sections.

Several public functions are missing one or more required Args, Returns, or Raises sections; for example, hooks_config() has no Args/Returns/Raises, and handle() has no Returns.

As per coding guidelines, "**/*.py: Include docstring with Args, Returns, and Raises sections for all public APIs."

Also applies to: 48-49, 92-100, 114-121, 145-151, 181-189, 225-230, 255-261

🤖 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 `@src/hebb/integrations/codex/install.py` around lines 36 - 44, The public API
docstrings are incomplete in install.py and must include the required Args,
Returns, and Raises sections. Update the docstrings for the affected public
functions, including hooks_path(), hooks_config(), handle(), and the other
listed APIs, so each one documents its parameters, return value, and any
exceptions it may raise in the same style as the existing docstring. Keep the
documentation consistent across these symbols and ensure no public function is
missing any required section.

Source: Coding guidelines

Comment on lines +101 to +110
return any(
marker in command
for marker in (
"hebb codex ",
"/hebb codex ",
"hebb.cli.main codex ",
"hebb claude-code ",
"/hebb claude-code ",
"hebb.cli.main claude-code ",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant section
ast-grep outline src/hebb/integrations/codex/install.py --view expanded || true
wc -l src/hebb/integrations/codex/install.py
sed -n '1,220p' src/hebb/integrations/codex/install.py

# Find the hook-related helpers and any quoting logic
rg -n "is_hebb_hook|hooks_config|shlex|quote|codex|claude-code" src/hebb/integrations/codex/install.py src -g '*.py'

Repository: afx-team/hebb-mind

Length of output: 22060


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '220,270p' src/hebb/integrations/codex/install.py
sed -n '1,140p' src/hebb/utils/cli_paths.py
sed -n '1,120p' src/hebb/integrations/claude_code/install.py

Repository: afx-team/hebb-mind

Length of output: 7442


Match hook commands after shell parsing, not raw substrings. shell_quote() will quote an absolute Hebb path with spaces, so commands like '/path with spaces/hebb' codex recall bypass the current "hebb codex " checks. That lets reinstall duplicate hooks and uninstall miss them; parse the command argv before matching.

🤖 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 `@src/hebb/integrations/codex/install.py` around lines 101 - 110, The command
matching logic in the hook installer is relying on raw substring checks, which
misses quoted absolute paths and can cause duplicate reinstall/uninstall
behavior. Update the hook detection in the relevant helper in install.py to
parse the command into argv first, then match on the executable and subcommand
values instead of looking for strings like “hebb codex ” or “/hebb claude-code ”
in the raw command text.

Comment on lines +21 to +26
@dataclass
class CodexTurn:
"""A parsed Codex turn and its source timestamp."""

summary: TurnSummary
timestamp: str | None = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required public API docstring sections.

CodexTurn is a public dataclass but its docstring lacks the required Args/Returns/Raises sections.

Proposed docstring update
 `@dataclass`
 class CodexTurn:
-    """A parsed Codex turn and its source timestamp."""
+    """A parsed Codex turn and its source timestamp.
+
+    Args:
+        summary: Memory-ready turn summary extracted from the rollout.
+        timestamp: Timestamp of the source user message, if present.
+
+    Returns:
+        None.
+
+    Raises:
+        None.
+    """

As per coding guidelines, "src/**/*.py: All public APIs in Python MUST have docstrings with Args, Returns, and Raises sections."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@dataclass
class CodexTurn:
"""A parsed Codex turn and its source timestamp."""
summary: TurnSummary
timestamp: str | None = None
`@dataclass`
class CodexTurn:
"""A parsed Codex turn and its source timestamp.
Args:
summary: Memory-ready turn summary extracted from the rollout.
timestamp: Timestamp of the source user message, if present.
Returns:
None.
Raises:
None.
"""
summary: TurnSummary
timestamp: str | None = None
🤖 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 `@src/hebb/integrations/codex/transcript.py` around lines 21 - 26, `CodexTurn`
is a public dataclass whose docstring is missing the required API sections;
update the class docstring in `CodexTurn` to include Args, Returns, and Raises
sections in the prescribed style. Use the existing `summary` and `timestamp`
fields to describe the arguments, note the dataclass construction result in
Returns, and include any exceptions that may be raised during initialization or
parsing if applicable.

Source: Coding guidelines

Comment on lines +49 to +57
user_indices = [index for index, record in enumerate(records) if _raw_user_text(record)]
if not user_indices:
return None

user_index = user_indices[-1]
user_record = records[user_index]
user_text = _clean_user_text(_raw_user_text(user_record))
if not user_text:
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Select the last clean user turn, not just the last raw user record.

Line 49 records raw user messages, but Lines 55-57 return None if the last raw user message is filtered out. A trailing short/noisy prompt can suppress an earlier complete turn, which breaks the “last valid user input” parser contract.

Proposed fix
-    user_indices = [index for index, record in enumerate(records) if _raw_user_text(record)]
-    if not user_indices:
-        return None
-
-    user_index = user_indices[-1]
-    user_record = records[user_index]
-    user_text = _clean_user_text(_raw_user_text(user_record))
-    if not user_text:
+    user_indices: list[int] = []
+    last_valid_user: tuple[int, str, int] | None = None
+    for index, record in enumerate(records):
+        raw_user_text = _raw_user_text(record)
+        if not raw_user_text:
+            continue
+        turn_number = len(user_indices)
+        user_indices.append(index)
+        user_text = _clean_user_text(raw_user_text)
+        if user_text:
+            last_valid_user = (index, user_text, turn_number)
+    if last_valid_user is None:
         return None
+
+    user_index, user_text, turn_number = last_valid_user
+    user_record = records[user_index]
@@
-        turn=len(user_indices) - 1,
+        turn=turn_number,

Also applies to: 81-87

🤖 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 `@src/hebb/integrations/codex/transcript.py` around lines 49 - 57, The
transcript selection logic in the user-turn parsing path currently picks the
last raw user record and then returns None if _clean_user_text filters it out,
which can hide an earlier valid user message. Update the selection in the
transcript parser so it chooses the last user record that still has non-empty
cleaned text, using the existing helpers like _raw_user_text and
_clean_user_text in the relevant transcript handling flow. Apply the same fix in
both affected parsing sections so the “last valid user input” behavior is
consistent.

Comment on lines +166 to +168
def _truncate(value: str, limit: int) -> str:
"""Truncate text with a visible ellipsis."""
return value if len(value) <= limit else value[:limit] + "…"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Keep truncated output within the configured limit.

The current branch returns limit + 1 characters because the ellipsis is appended after slicing to limit.

Proposed fix
 def _truncate(value: str, limit: int) -> str:
     """Truncate text with a visible ellipsis."""
-    return value if len(value) <= limit else value[:limit] + "…"
+    if len(value) <= limit:
+        return value
+    if limit <= 0:
+        return ""
+    return value[: limit - 1] + "…"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _truncate(value: str, limit: int) -> str:
"""Truncate text with a visible ellipsis."""
return value if len(value) <= limit else value[:limit] + "…"
def _truncate(value: str, limit: int) -> str:
"""Truncate text with a visible ellipsis."""
if len(value) <= limit:
return value
if limit <= 0:
return ""
return value[: limit - 1] + "…"
🤖 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 `@src/hebb/integrations/codex/transcript.py` around lines 166 - 168, The
_truncate helper currently exceeds the configured limit because it appends the
ellipsis after slicing to limit; update _truncate so the returned string always
stays within limit, likely by reserving space for the ellipsis when truncation
is needed. Keep the fix localized to _truncate in transcript.py and preserve the
existing behavior for already-short values.

Comment on lines +58 to +66
def uninstall_project_mcp(path: Path) -> bool:
"""Remove project-scoped Hebb MCP configuration.

Args:
path: Project ``config.toml`` path.

Returns:
Whether the file changed.
"""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Complete the required public API docstring sections.

uninstall_project_mcp() is missing a Raises section, and handle() is missing a Returns section.

As per coding guidelines, "**/*.py: Include docstring with Args, Returns, and Raises sections for all public APIs."

Also applies to: 77-85

🤖 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 `@src/hebb/integrations/codex/uninstall.py` around lines 58 - 66, Complete the
public API docstrings for uninstall_project_mcp and handle by adding the missing
sections required by the coding guidelines. Update uninstall_project_mcp to
include a Raises section describing any exceptions it can propagate, and update
handle to include a Returns section describing its return value. Keep the
docstrings consistent with the existing Args/Returns style used in
src/hebb/integrations/codex/uninstall.py.

Source: Coding guidelines

Comment on lines +184 to +190
hook_input = {
"session_id": "session-1",
"turn_id": "turn-3",
"cwd": "/workspace/project",
"transcript_path": "/tmp/rollout.jsonl",
"last_assistant_message": "Recorded.",
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the hardcoded /tmp transcript path.

These tests never read that file because extract_last_turn is mocked, so baking in "/tmp/rollout.jsonl" just adds a POSIX-only absolute path and violates the repo rule against absolute paths outside the workspace. Use tmp_path / "rollout.jsonl" (or a relative placeholder string) instead. As per coding guidelines, "**/*.{py,json,yaml,yml,env,toml,txt}: MUST NOT hardcode API keys, secrets, or absolute paths outside the user's workspace".

Also applies to: 221-224

🧰 Tools
🪛 ast-grep (0.44.0)

[info] 187-187: Do not hardcode temporary file or directory names
Context: "/tmp/rollout.jsonl"
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)

🤖 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 `@tests/unit/integrations/test_codex_hooks.py` around lines 184 - 190, The test
fixture in hook_input hardcodes an absolute /tmp transcript_path, which should
be replaced with a workspace-safe path. Update the Codex hook tests that build
hook_input to use tmp_path / "rollout.jsonl" (or another relative placeholder)
instead of "/tmp/rollout.jsonl", keeping the change localized to the test data
used around extract_last_turn and the related hook_input setup.

Sources: Coding guidelines, Linters/SAST tools

@ch-liuzhide
ch-liuzhide merged commit 39e9f62 into main Jun 30, 2026
19 checks passed
@ch-liuzhide
ch-liuzhide deleted the codex/codex-native-integration-0.3.0 branch June 30, 2026 09:58
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.

1 participant