Skip to content

Add post-tool and session-end hooks for Codex/Hermes/OpenCode#230

Closed
Gradata wants to merge 3 commits into
mainfrom
gra-1163-post-session-hooks
Closed

Add post-tool and session-end hooks for Codex/Hermes/OpenCode#230
Gradata wants to merge 3 commits into
mainfrom
gra-1163-post-session-hooks

Conversation

@Gradata

@Gradata Gradata commented May 27, 2026

Copy link
Copy Markdown
Owner

Closes GRA-1163 and GRA-1233.

Summary

  • Add shared hook command helpers for post_tool correction capture and session_end close.
  • Install/uninstall post_tool + session_end hooks for Codex, Hermes, and OpenCode adapters.
  • Fix Claude Code adapter: was only installing PreToolUse (inject_brain_rules), missing PostToolUse (auto_correct) and Stop (session_close). New users got injection but no correction capture.
  • Add tests covering install/uninstall behavior for the new hooks.

Verification

  • pytest tests/test_hook_adapters.py (14 passed)
  • pytest tests/test_uninstall_command.py (11 passed)
  • pytest tests/test_adapter_extraction_contracts.py (59 passed)

@greptile-apps greptile-apps 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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough
  • Added shared hook command helpers in gradata/hooks/adapters/_base.py: hook_command_for_module(brain_dir, module), plus exported post_tool_hook_command(brain_dir) and session_end_hook_command(brain_dir).
  • New public API: post_tool_hook_command() and session_end_hook_command() to run auto-correct and session-close hook modules.
  • Codex adapter: installs/uninstalls post_tool and session_end hooks in addition to pre_tool by managing TOML hook tables; idempotent install/uninstall via signature checks across the three events.
  • Hermes adapter: installs/uninstalls across Hermes-native keys (pre_tool_call, post_tool_call, on_session_end), migrates legacy Claude-Code keys during install, and reports added vs already-present based on actual additions.
  • OpenCode adapter: expanded to add/remove preTool, postTool, and sessionEnd entries in JSON/YAML configs; install/uninstall are idempotent and report accurate results.
  • Claude_Code adapter: rewritten to install an idempotent minimal profile of three hooks (PreToolUse, PostToolUse matcher Edit|Write, Stop) via a shared _HOOK_ENTRIES table; uninstall removes matching entries across all listed events.
  • Installer/uninstaller behavior unified and hardened: installs now return "already_present" only when no new hooks were added; uninstalls remove signature-matching entries across all hook types; added helpers and signature checks to ensure idempotence.
  • Tests: added coverage for install/uninstall behavior across adapters (pytest tests/test_hook_adapters.py — 14 passed; tests/test_uninstall_command.py — 11 passed; tests/test_adapter_extraction_contracts.py — 59 passed).
  • Fixes: restores missing post-tool correction capture and session-close hook installation for Claude Code and other adapters (addresses GRA-1163; unblocks GRA-1233/GRA-1215).
  • Breaking changes/security: none—no public function signatures removed or altered; no security fixes that change behavior beyond installing additional hooks.

Walkthrough

Adds a generic hook-command builder and exported post-tool/session-end helpers; updates Codex, Hermes, OpenCode, and Claude‑Code adapters to idempotently install and uninstall hooks across pre-tool, post-tool, and session-end phases; expands tests to cover the new hook placements and uninstall behavior.

Changes

Three-Phase Hook Installation Across All Adapters

Layer / File(s) Summary
Hook command infrastructure
Gradata/src/gradata/hooks/adapters/_base.py
Introduces hook_command_for_module(brain_dir, module) to build quoted BRAIN_DIR=... <python> -m <module> runner strings; hook_command() delegates to it; adds post_tool_hook_command() and session_end_hook_command() wrappers.
Codex adapter three-phase hooks
Gradata/src/gradata/hooks/adapters/codex.py
Adds _hook_table_has_signature() and updates install()/uninstall() to handle pre_tool, post_tool, and session_end TOML tables with per-table signature checks and generalized removal messaging.
Hermes adapter with legacy migration
Gradata/src/gradata/hooks/adapters/hermes.py
Installs hooks across Hermes-native keys (pre_tool_call, post_tool_call, on_session_end) while migrating legacy Claude‑Code keys; install()/uninstall() operate idempotently across current and legacy keys.
OpenCode adapter three-phase hooks
Gradata/src/gradata/hooks/adapters/opencode.py
Extends install() to add preTool, postTool, and sessionEnd entries with signature deduplication; rewrites uninstall() to remove matching entries across all three, validate/prune config, and report removals.
Claude-Code minimal hook profile
Gradata/src/gradata/hooks/adapters/claude_code.py
Adds _HOOK_ENTRIES defining PreToolUse/PostToolUse/Stop entries; install() appends missing profile entries idempotently; uninstall() removes signature-matching entries across those events and prunes empty containers.
Test coverage for three-phase adapters
Gradata/tests/test_hook_adapters.py
Adds tests: Codex install writes post_tool and session_end hooks; parameterized tests assert Hermes/OpenCode configs contain expected hook-module strings; uninstall tests verify removal of auto_correct and session_close modules and agent prefix absence.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Gradata/gradata#193: Hermes event-key migration and uninstall expansion across legacy Claude‑Code and Hermes-native event-name keys.
  • Gradata/gradata#242: Changes to shared hook command construction in hooks/adapters/_base.py.
  • Gradata/gradata#258: Overlapping Codex/OpenCode post-tool and session-end hook installation updates.

Suggested labels

feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.17% 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
Title check ✅ Passed The title accurately summarizes the main changes: adding post-tool and session-end hooks for Codex/Hermes/OpenCode adapters, which aligns with the core refactoring across multiple adapter files.
Description check ✅ Passed The description is directly related to the changeset, clearly explaining the addition of hook command helpers, the installation/uninstall of hooks across adapters, and fixes to the Claude Code adapter with test verification.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gra-1163-post-session-hooks
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch gra-1163-post-session-hooks

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.22.0)

OpenGrep fatal error (exit code 2):
┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m
[00.16][ERROR]: Error: exception Glob.Lexer.Syntax_error("malformed glob pattern: missing ']'")
Raised at Glob__Lexer.syntax_error in file "libs/glob/Lexer.mll", line 8, characters 2-26
Called from Glob__Lexer.__ocaml_lex_token_rec in file "libs/glob/Lexer.mll", line 29, characters 26-53
Cal


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

@coderabbitai coderabbitai Bot added the feature label May 27, 2026

@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: 2

🤖 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 `@Gradata/src/gradata/hooks/adapters/_base.py`:
- Around line 132-136: The hook_command_for_module function currently embeds
brain_dir verbatim which can be relative; change it to serialize an
absolute/canonical path (e.g., call brain_dir.resolve() or convert to an
absolute string before quoting) so the generated BRAIN_DIR is always an absolute
path; update hook_command_for_module to use the resolved/absolute Path when
building the f-string passed to shlex.quote(sys.executable) and BRAIN_DIR.

In `@Gradata/src/gradata/hooks/adapters/hermes.py`:
- Around line 166-181: The migration currently skips writing the config when no
new hook is appended because it only checks `added`; update the logic around the
loop that calls `_migrate_legacy_event(hooks, legacy_key, current_key)` to
detect whether any legacy->current migrations occurred (e.g., set a boolean flag
like `migrated = True` when `_migrate_legacy_event` moves entries or compare
pre/post state of `hooks`), and if either `added` is non-empty or `migrated` is
true, call `atomic_write_text(agent_config_path, _dump_simple_yaml(data))` and
return the appropriate InstallResult; this ensures migrations performed by
`_migrate_legacy_event` are persisted even when no new hook (`sig`) was
appended.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4f364644-089b-4897-914c-1ac13562952d

📥 Commits

Reviewing files that changed from the base of the PR and between a197bff and 623e1b2.

📒 Files selected for processing (5)
  • Gradata/src/gradata/hooks/adapters/_base.py
  • Gradata/src/gradata/hooks/adapters/codex.py
  • Gradata/src/gradata/hooks/adapters/hermes.py
  • Gradata/src/gradata/hooks/adapters/opencode.py
  • Gradata/tests/test_hook_adapters.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: pytest macos-latest / py3.12
  • GitHub Check: pytest ubuntu-latest / py3.12
  • GitHub Check: pytest macos-latest / py3.11
  • GitHub Check: pytest windows-latest / py3.12
  • GitHub Check: pytest (py3.11)
  • GitHub Check: pytest (py3.12)
  • GitHub Check: pytest windows-latest / py3.11
  • GitHub Check: pytest ubuntu-latest / py3.11
🧰 Additional context used
📓 Path-based instructions (2)
Gradata/tests/**/*.py

📄 CodeRabbit inference engine (Gradata/AGENTS.md)

Gradata/tests/**/*.py: Set BRAIN_DIR environment variable via tmp_path in conftest.py for test isolation — ensure _paths.py module cache refreshes when calling Brain.init() directly inside tests
Add unit tests in tests/test_*.py for every CI push without LLM calls (deterministic); mark integration tests with @pytest.mark.integration and skip them by default (they hit real LLM APIs)

Files:

  • Gradata/tests/test_hook_adapters.py
Gradata/src/**/*.py

📄 CodeRabbit inference engine (Gradata/AGENTS.md)

Gradata/src/**/*.py: Prefer sentence-transformers for local embeddings, google-genai for Gemini embeddings, cryptography for AES-GCM encrypted system.db, bm25s for BM25 rule ranking, and mem0ai for external memory adapters — guard all optional dependency imports with try / except ImportError at the call site, never at module level
Maintain strict layering: Layer 0 (Primitives: _types.py, _db.py, _events.py, _paths.py, _file_lock.py; Patterns: contrib/patterns/) must never import from Layer 1 (Enhancements: enhancements/, rules/) or Layer 2 (Public API: brain.py, cli.py, daemon.py, mcp_server.py)
Never use bare except: pass — use typed exceptions or at minimum logger.warning(...) with exc_info=True to avoid silent failure in a memory product
Never import from out-of-scope sibling directories ../Sprites/ or ../Hausgem/ within gradata/* code — that is a layering bug
Never leak private-sibling paths into public docs/code — no references to ../Sprites/, ../Hausgem/, email addresses, OneDrive paths, or Sprites-specific examples from inside gradata/*
Use atomic-write helper when writing JSON files to prevent corruption from mid-write crashes

Files:

  • Gradata/src/gradata/hooks/adapters/_base.py
  • Gradata/src/gradata/hooks/adapters/opencode.py
  • Gradata/src/gradata/hooks/adapters/hermes.py
  • Gradata/src/gradata/hooks/adapters/codex.py
🔇 Additional comments (4)
Gradata/tests/test_hook_adapters.py (1)

55-68: LGTM!

Also applies to: 70-105, 107-123

Gradata/src/gradata/hooks/adapters/codex.py (1)

74-91: LGTM!

Also applies to: 100-123, 128-188

Gradata/src/gradata/hooks/adapters/hermes.py (1)

245-295: LGTM!

Gradata/src/gradata/hooks/adapters/opencode.py (1)

49-78: LGTM!

Also applies to: 83-120

Comment thread Gradata/src/gradata/hooks/adapters/_base.py
Comment thread Gradata/src/gradata/hooks/adapters/hermes.py
The 'gradata install --agent claude-code' was only installing the PreToolUse
hook for inject_brain_rules. It was missing the PostToolUse hook for
auto_correct (capture corrections from Edit|Write) and the Stop hook for
session_close (graduation sweep).

This means new users following the README got rule injection but no
correction capture — the core value proposition was broken at install.

_fix:
  - Added post_tool_hook_command + session_end_hook_command imports
  - Extracted _HOOK_ENTRIES list (event, matcher, cmd_fn)
  - install() now writes all 3 hooks
  - uninstall() scans all events (not just PreToolUse)
  - Idempotent: signature check across all events

  Blocks: GRA-1233 (critical), GRA-1215 (README publication), HN launch

@greptile-apps greptile-apps 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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@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: 2

🤖 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 `@Gradata/src/gradata/hooks/adapters/claude_code.py`:
- Line 81: The current signature match uses a substring search on the entire
entry string ("if sig in str(existing)" and the analogous check at uninstall)
which can hit unrelated fields; update both places to inspect the hook id
directly by retrieving entry.get("hooks", [{}])[0].get("id") (or iterate hooks
if multiple) and compare that value to sig, then proceed with install/uninstall
logic using that equality check to avoid false positives.
- Around line 78-84: The current install() logic iterates over _HOOK_ENTRIES and
returns InstallResult("already_present") as soon as the signature is found in
any event, causing all-or-nothing idempotency and preventing repair of partially
missing hooks; change this to check each (_event, _matcher, _cmd_fn, _desc) in
_HOOK_ENTRIES against hooks.get(_event, []) and only skip installation for that
specific event when the signature exists, otherwise proceed to add the missing
hook entries for that event, and only return a final InstallResult summarizing
which hooks were added (or none) instead of early-returning when any single
match is found (references: _HOOK_ENTRIES, hooks, InstallResult, AGENT,
agent_config_path).
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a4ae7df0-a64d-44f7-bcfc-1fdf31da499d

📥 Commits

Reviewing files that changed from the base of the PR and between 623e1b2 and 7b3d9ba.

📒 Files selected for processing (1)
  • Gradata/src/gradata/hooks/adapters/claude_code.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
Gradata/src/**/*.py

📄 CodeRabbit inference engine (Gradata/AGENTS.md)

Gradata/src/**/*.py: Prefer sentence-transformers for local embeddings, google-genai for Gemini embeddings, cryptography for AES-GCM encrypted system.db, bm25s for BM25 rule ranking, and mem0ai for external memory adapters — guard all optional dependency imports with try / except ImportError at the call site, never at module level
Maintain strict layering: Layer 0 (Primitives: _types.py, _db.py, _events.py, _paths.py, _file_lock.py; Patterns: contrib/patterns/) must never import from Layer 1 (Enhancements: enhancements/, rules/) or Layer 2 (Public API: brain.py, cli.py, daemon.py, mcp_server.py)
Never use bare except: pass — use typed exceptions or at minimum logger.warning(...) with exc_info=True to avoid silent failure in a memory product
Never import from out-of-scope sibling directories ../Sprites/ or ../Hausgem/ within gradata/* code — that is a layering bug
Never leak private-sibling paths into public docs/code — no references to ../Sprites/, ../Hausgem/, email addresses, OneDrive paths, or Sprites-specific examples from inside gradata/*
Use atomic-write helper when writing JSON files to prevent corruption from mid-write crashes

Files:

  • Gradata/src/gradata/hooks/adapters/claude_code.py
🔇 Additional comments (5)
Gradata/src/gradata/hooks/adapters/claude_code.py (5)

15-15: LGTM!

Also applies to: 17-17


57-63: LGTM!


132-159: LGTM!


101-101: ⚡ Quick win

Update: write_json() already performs atomic JSON writes via atomic_write_text()
write_json() delegates to atomic_write_text(), which writes to a sibling temp file (with a pid-based .tmp suffix), flushes+fsyncs the file, then uses os.replace() to atomically replace the target and fsyncs the parent directory—meeting the atomic-write guideline.


62-62: ⚡ Quick win

Confirm Claude Code event name: using "Stop" for session_close is correct.

Gradata/src/gradata/hooks/adapters/claude_code.py registers the session-end hook under event "Stop", and Gradata/src/gradata/hooks/session_close.py explicitly sets HOOK_META["event"] = "Stop" and documents that it fires on every Stop (end of every turn) since Claude Code lacks a real session-end signal; the hook gates the expensive work accordingly.

Comment thread Gradata/src/gradata/hooks/adapters/claude_code.py Outdated
Comment thread Gradata/src/gradata/hooks/adapters/claude_code.py Outdated

@greptile-apps greptile-apps 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.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@Gradata

Gradata commented Jun 5, 2026

Copy link
Copy Markdown
Owner Author

Superseded: origin/main already contains the verified hook implementation for GRA-1163 via merged commits d958235 (Codex/OpenCode) and 7000062 (Hermes). Verified on origin/main: python3 -m pytest tests/test_hook_adapters.py => 21 passed.

@Gradata Gradata closed this Jun 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant